From 498b57de13243d8bc86ae36b18da119c8ec4997f Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Wed, 3 Sep 2025 23:56:03 +0800 Subject: [PATCH 01/69] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=97=A0=E7=94=A8?= =?UTF-8?q?=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat_loop/cycle_processor.py | 25 ------ src/chat/chat_loop/heartFC_chat.py | 118 +------------------------- src/chat/chat_loop/hfc_utils.py | 37 -------- 3 files changed, 1 insertion(+), 179 deletions(-) diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index 637c90c85..b35905361 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -629,31 +629,6 @@ class CycleProcessor: traceback.print_exc() return False, "", "" - def _get_direct_reply_plan(self, loop_start_time): - """ - 获取直接回复的规划结果 - - Args: - loop_start_time: 循环开始时间 - - Returns: - dict: 包含直接回复动作的规划结果 - - 功能说明: - - 在某些情况下跳过复杂规划,直接返回回复动作 - - 主要用于NORMAL模式下没有其他可用动作时的简化处理 - """ - return { - "action_result": { - "action_type": "reply", - "action_data": {"loop_start_time": loop_start_time}, - "reasoning": "", - "timestamp": time.time(), - "is_parallel": False, - }, - "action_prompt": "", - } - def _build_final_loop_info(self, reply_loop_info, action_success, action_reply_text, action_command, plan_result): """ 构建最终的循环信息 diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index c7fd0c2ee..4d1878320 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -261,13 +261,7 @@ class HeartFChatting: # 重置累积兴趣值,因为消息已经被成功处理 self.context.breaking_accumulated_interest = 0.0 logger.info(f"{self.context.log_prefix} 能量值增加,当前能量值:{self.context.energy_value:.1f},重置累积兴趣值") - - self._check_focus_exit() - - else: - # 无新消息时,只进行模式检查,不进行思考循环 - self._check_focus_exit() - + # 更新上一帧的睡眠状态 self.context.was_sleeping = is_sleeping @@ -288,62 +282,6 @@ class HeartFChatting: return has_new_messages - def _check_focus_exit(self): - """ - 检查是否应该退出FOCUS模式 - - 功能说明: - - 区分私聊和群聊环境 - - 在强制私聊focus模式下,能量值低于1时重置为5但不退出 - - 在群聊focus模式下,如果配置为focus则不退出 - - 其他情况下,能量值低于1时退出到NORMAL模式 - """ - is_private_chat = self.context.chat_stream.group_info is None if self.context.chat_stream else False - is_group_chat = not is_private_chat - - if global_config.chat.force_focus_private and is_private_chat: - if self.context.energy_value <= 1: - self.context.energy_value = 5 - return - - if is_group_chat and global_config.chat.group_chat_mode == "focus": - return - - if self.context.energy_value <= 1: # 如果能量值小于等于1(非强制情况) - self.context.energy_value = 1 # 将能量值设置为1 - - def _check_focus_entry(self, new_message_count: int): - """ - 检查是否应该进入FOCUS模式 - - Args: - new_message_count: 新消息数量 - - 功能说明: - - 区分私聊和群聊环境 - - 强制私聊focus模式:直接进入FOCUS模式并设置能量值为10 - - 群聊normal模式:不进入FOCUS模式 - - 根据focus_value配置和消息数量决定是否进入FOCUS模式 - - 当消息数量超过阈值或能量值达到30时进入FOCUS模式 - """ - is_private_chat = self.context.chat_stream.group_info is None if self.context.chat_stream else False - is_group_chat = not is_private_chat - - if global_config.chat.force_focus_private and is_private_chat: - self.context.energy_value = 10 - return - - if is_group_chat and global_config.chat.group_chat_mode == "normal": - return - - if global_config.chat.focus_value != 0: # 如果专注值配置不为0(启用自动专注) - if new_message_count > 3 / pow( - global_config.chat.focus_value, 0.5 - ): # 如果新消息数超过阈值(基于专注值计算) - self.context.energy_value = ( - 10 + (new_message_count / (3 / pow(global_config.chat.focus_value, 0.5))) * 10 - ) # 根据消息数量计算能量值 - return # 返回,不再检查其他条件 def _handle_wakeup_messages(self, messages): """ @@ -463,57 +401,3 @@ class HeartFChatting: await asyncio.sleep(0.5) return False,0.0 - - async def _execute_no_reply(self, new_message: List[Dict[str, Any]]) -> bool: - """执行breaking形式的no_reply(原有逻辑)""" - new_message_count = len(new_message) - # 检查消息数量是否达到阈值 - talk_frequency = global_config.chat.get_current_talk_frequency(self.context.stream_id) - modified_exit_count_threshold = self.context.focus_energy / talk_frequency - - if new_message_count >= modified_exit_count_threshold: - # 记录兴趣度到列表 - total_interest = 0.0 - for msg_dict in new_message: - interest_value = msg_dict.get("interest_value", 0.0) - if msg_dict.get("processed_plain_text", ""): - total_interest += interest_value - - self.recent_interest_records.append(total_interest) - - logger.info( - f"{self.context.log_prefix} 累计消息数量达到{new_message_count}条(>{modified_exit_count_threshold}),结束等待" - ) - - return True - - # 检查累计兴趣值 - if new_message_count > 0: - accumulated_interest = 0.0 - for msg_dict in new_message: - text = msg_dict.get("processed_plain_text", "") - interest_value = msg_dict.get("interest_value", 0.0) - if text: - accumulated_interest += interest_value - - # 只在兴趣值变化时输出log - if not hasattr(self, "_last_accumulated_interest") or accumulated_interest != self._last_accumulated_interest: - logger.info(f"{self.context.log_prefix} breaking形式当前累计兴趣值: {accumulated_interest:.2f}, 当前聊天频率: {talk_frequency:.2f}") - self._last_accumulated_interest = accumulated_interest - - if accumulated_interest >= 3 / talk_frequency: - # 记录兴趣度到列表 - self.recent_interest_records.append(accumulated_interest) - - logger.info( - f"{self.context.log_prefix} 累计兴趣值达到{accumulated_interest:.2f}(>{3 / talk_frequency}),结束等待" - ) - return True - - # 每10秒输出一次等待状态 - if int(time.time() - self.context.last_read_time) > 0 and int(time.time() - self.context.last_read_time) % 10 == 0: - logger.info( - f"{self.context.log_prefix} 已等待{time.time() - self.context.last_read_time:.0f}秒,累计{new_message_count}条消息,继续等待..." - ) - - return False diff --git a/src/chat/chat_loop/hfc_utils.py b/src/chat/chat_loop/hfc_utils.py index 0fab83cb6..eeaac70f5 100644 --- a/src/chat/chat_loop/hfc_utils.py +++ b/src/chat/chat_loop/hfc_utils.py @@ -122,43 +122,6 @@ class CycleDetail: self.loop_plan_info = loop_info["loop_plan_info"] self.loop_action_info = loop_info["loop_action_info"] - -def get_recent_message_stats(minutes: float = 30, chat_id: Optional[str] = None) -> dict: - """ - 获取最近消息统计信息 - - Args: - minutes: 检索的分钟数,默认30分钟 - chat_id: 指定的chat_id,仅统计该chat下的消息。为None时统计全部 - - Returns: - dict: {"bot_reply_count": int, "total_message_count": int} - - 功能说明: - - 统计指定时间范围内的消息数量 - - 区分机器人回复和总消息数 - - 可以针对特定聊天或全局统计 - - 用于分析聊天活跃度和机器人参与度 - """ - - now = time.time() - start_time = now - minutes * 60 - bot_id = global_config.bot.qq_account - - filter_base: Dict[str, Any] = {"time": {"$gte": start_time}} - if chat_id is not None: - filter_base["chat_id"] = chat_id - - # 总消息数 - total_message_count = count_messages(filter_base) - # bot自身回复数 - bot_filter = filter_base.copy() - bot_filter["user_id"] = bot_id - bot_reply_count = count_messages(bot_filter) - - return {"bot_reply_count": bot_reply_count, "total_message_count": total_message_count} - - async def send_typing(): """ 发送打字状态指示 From f4b8bf5904c5b3b795fb6f479f86beaee3fd2e35 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Thu, 4 Sep 2025 00:09:43 +0800 Subject: [PATCH 02/69] =?UTF-8?q?=E5=8A=A0=E5=85=A5breaking=E5=90=AF?= =?UTF-8?q?=E7=94=A8=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat_loop/heartFC_chat.py | 6 ++++++ template/bot_config_template.toml | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index 4d1878320..41b072332 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -320,6 +320,12 @@ class HeartFChatting: def _determine_form_type(self) -> str: """判断使用哪种形式的no_reply""" + # 检查是否启用breaking模式 + if not global_config.chat.enable_breaking_mode: + logger.info(f"{self.context.log_prefix} breaking模式已禁用,使用waiting形式") + self.context.focus_energy = 1 + return + # 如果连续no_reply次数少于3次,使用waiting形式 if self.context.no_reply_consecutive <= 3: self.context.focus_energy = 1 diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 196ff5ff7..84833a6c8 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.7.0" +version = "6.7.1" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -108,9 +108,12 @@ talk_frequency = 1 # MoFox-Bot活跃度,越高,麦麦回复越多 # 专注时能更好把握发言时机,能够进行持久的连续对话 -focus_value = 1 +focus_value = 1 # MoFox-Bot的专注思考能力,越高越容易持续连续对话 +# breaking模式配置 +enable_breaking_mode = true # 是否启用自动进入breaking模式,关闭后不会自动进入breaking形式 + # 强制私聊专注模式 force_focus_private = false # 是否强制私聊进入专注模式,开启后私聊将始终保持专注状态 From 4d03a2fafef08889435ae2cf895728a738044536 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:24:25 +0800 Subject: [PATCH 03/69] =?UTF-8?q?fix(api):=20=E4=BF=AE=E5=A4=8D=20`reply?= =?UTF-8?q?=5Fto=5Fplatform=5Fid`=20=E5=8F=AF=E8=83=BD=E6=9C=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugin_system/apis/send_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugin_system/apis/send_api.py b/src/plugin_system/apis/send_api.py index c652383fb..5e24cb1b0 100644 --- a/src/plugin_system/apis/send_api.py +++ b/src/plugin_system/apis/send_api.py @@ -195,6 +195,7 @@ async def _send_to_target( ) else: anchor_message = None + reply_to_platform_id = None # 构建发送消息对象 bot_message = MessageSending( From 7b537e96d0160524d87ce57d84fddf9502362c78 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:44:35 +0800 Subject: [PATCH 04/69] =?UTF-8?q?fix(qzone):=20=E4=BF=AE=E5=A4=8D=E5=AD=90?= =?UTF-8?q?=E8=AF=84=E8=AE=BA=E5=9B=9E=E5=A4=8D=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除手动构造的@提醒内容,改为通过传递 `parent_tid` 参数来正确指定父评论。 这利用了平台原生的回复机制,可以更可靠地实现回复和提醒功能。 --- .../maizone_refactored/services/qzone_service.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/plugins/built_in/maizone_refactored/services/qzone_service.py b/src/plugins/built_in/maizone_refactored/services/qzone_service.py index 2a36f822c..6960dd5ab 100644 --- a/src/plugins/built_in/maizone_refactored/services/qzone_service.py +++ b/src/plugins/built_in/maizone_refactored/services/qzone_service.py @@ -791,10 +791,11 @@ class QZoneService: try: # 修复回复逻辑:确保能正确提醒被回复的人 data = { - "topicId": f"{host_qq}_{fid}__1", # 使用标准评论格式,而不是针对特定评论 + "topicId": f"{host_qq}_{fid}__1", + "parent_tid": comment_tid, "uin": uin, "hostUin": host_qq, - "content": f"回复@{target_name}:{content}", # 内容中明确标示回复对象 + "content": content, "format": "fs", "plat": "qzone", "source": "ic", @@ -802,12 +803,14 @@ class QZoneService: "ref": "feeds", "richtype": "", "richval": "", - "paramstr": f"@{target_name}", # 确保触发@提醒机制 + "paramstr": "", } - + # 记录详细的请求参数用于调试 - logger.info(f"子回复请求参数: topicId={data['topicId']}, parent_tid={data['parent_tid']}, content='{content[:50]}...'") - + logger.info( + f"子回复请求参数: topicId={data['topicId']}, parent_tid={data['parent_tid']}, content='{content[:50]}...'" + ) + await _request("POST", self.REPLY_URL, params={"g_tk": gtk}, data=data) return True except Exception as e: From ac1253acc5208300b6f4eba81fe1616bb739d7da Mon Sep 17 00:00:00 2001 From: Furina-1013-create <189647097+Furina-1013-create@users.noreply.github.com> Date: Thu, 4 Sep 2025 20:57:59 +0800 Subject: [PATCH 05/69] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20graph=5Fnodes?= =?UTF-8?q?=20=E8=A1=A8=20weight=20=E5=AD=97=E6=AE=B5=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 GraphNodes 模型中添加 weight 字段,设置默认值为 1.0 - 修复 Hippocampus.py 中插入 GraphNodes 时缺少 weight 字段的问题 - 解决 sqlite3.IntegrityError: NOT NULL constraint failed: graph_nodes.weight 错误 --- src/chat/memory_system/Hippocampus.py | 2 ++ src/common/database/sqlalchemy_models.py | 1 + 2 files changed, 3 insertions(+) diff --git a/src/chat/memory_system/Hippocampus.py b/src/chat/memory_system/Hippocampus.py index 6c61cfb2b..af5078caf 100644 --- a/src/chat/memory_system/Hippocampus.py +++ b/src/chat/memory_system/Hippocampus.py @@ -928,6 +928,7 @@ class EntorhinalCortex: "concept": concept, "memory_items": memory_items_json, "hash": memory_hash, + "weight": 1.0, # 默认权重为1.0 "created_time": created_time, "last_modified": last_modified, } @@ -1084,6 +1085,7 @@ class EntorhinalCortex: "concept": concept, "memory_items": memory_items_json, "hash": self.hippocampus.calculate_node_hash(concept, memory_items), + "weight": 1.0, # 默认权重为1.0 "created_time": data.get("created_time", current_time), "last_modified": data.get("last_modified", current_time), } diff --git a/src/common/database/sqlalchemy_models.py b/src/common/database/sqlalchemy_models.py index d9948408a..a5b431a1c 100644 --- a/src/common/database/sqlalchemy_models.py +++ b/src/common/database/sqlalchemy_models.py @@ -361,6 +361,7 @@ class GraphNodes(Base): concept = Column(get_string_field(255), nullable=False, unique=True, index=True) memory_items = Column(Text, nullable=False) hash = Column(Text, nullable=False) + weight = Column(Float, nullable=False, default=1.0) created_time = Column(Float, nullable=False) last_modified = Column(Float, nullable=False) From fc68958c8fadba15075c889d20ad4bc538fc7b76 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 5 Sep 2025 19:51:33 +0800 Subject: [PATCH 06/69] =?UTF-8?q?feat(cross=5Fcontext):=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E6=8C=89=E4=BA=92=E9=80=9A=E7=BB=84=E5=90=8D=E7=A7=B0?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E8=81=8A=E5=A4=A9=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 `get_chat_history_by_group_name` API,允许插件按名称获取指定互通组的聊天记录。此功能增强了插件的上下文感知能力,使其能够利用跨群聊的对话历史。 主要变更: - 在 `cross_context_api.py` 中添加了 `get_chat_history_by_group_name` 函数。 - Maizone 插件现在利用此 API 来获取跨群聊的上下文,以生成更相关的说说内容。 - 调整了配置文件模板,以反映新的互通组配置方式。 --- src/plugin_system/apis/cross_context_api.py | 70 ++++++++++++++++++- .../services/content_service.py | 8 ++- template/bot_config_template.toml | 8 +-- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/src/plugin_system/apis/cross_context_api.py b/src/plugin_system/apis/cross_context_api.py index 5a0b896df..31a9f0e53 100644 --- a/src/plugin_system/apis/cross_context_api.py +++ b/src/plugin_system/apis/cross_context_api.py @@ -25,9 +25,11 @@ def get_context_groups(chat_id: str) -> Optional[List[List[str]]]: return None is_group = current_stream.group_info is not None - current_chat_raw_id = ( - current_stream.group_info.group_id if is_group else current_stream.user_info.user_id - ) + if is_group: + assert current_stream.group_info is not None + current_chat_raw_id = current_stream.group_info.group_id + else: + current_chat_raw_id = current_stream.user_info.user_id current_type = "group" if is_group else "private" for group in global_config.cross_context.groups: @@ -129,4 +131,66 @@ async def build_cross_context_s4u( if not cross_context_messages: return "" + return "# 跨上下文参考\n" + "\n\n".join(cross_context_messages) + "\n" + + +async def get_chat_history_by_group_name(group_name: str) -> str: + """ + 根据互通组名字获取聊天记录 + """ + target_group = None + for group in global_config.cross_context.groups: + if group.name == group_name: + target_group = group + break + + if not target_group: + return f"找不到名为 {group_name} 的互通组。" + + if not target_group.chat_ids: + return f"互通组 {group_name} 中没有配置任何聊天。" + + chat_infos = target_group.chat_ids + chat_manager = get_chat_manager() + + cross_context_messages = [] + for chat_type, chat_raw_id in chat_infos: + is_group = chat_type == "group" + + found_stream = None + for stream in chat_manager.streams.values(): + if is_group: + if stream.group_info and stream.group_info.group_id == chat_raw_id: + found_stream = stream + break + else: # private + if stream.user_info and stream.user_info.user_id == chat_raw_id and not stream.group_info: + found_stream = stream + break + + if not found_stream: + logger.warning(f"在已加载的聊天流中找不到ID为 {chat_raw_id} 的聊天。") + continue + + stream_id = found_stream.stream_id + + try: + messages = get_raw_msg_before_timestamp_with_chat( + chat_id=stream_id, + timestamp=time.time(), + limit=5, # 可配置 + ) + if messages: + chat_name = get_chat_manager().get_stream_name(stream_id) or chat_raw_id + formatted_messages, _ = build_readable_messages_with_id( + messages, timestamp_mode="relative" + ) + cross_context_messages.append(f'[以下是来自"{chat_name}"的近期消息]\n{formatted_messages}') + except Exception as e: + logger.error(f"获取聊天 {chat_raw_id} 的消息失败: {e}") + continue + + if not cross_context_messages: + return f"无法从互通组 {group_name} 中获取任何聊天记录。" + return "# 跨上下文参考\n" + "\n\n".join(cross_context_messages) + "\n" \ No newline at end of file diff --git a/src/plugins/built_in/maizone_refactored/services/content_service.py b/src/plugins/built_in/maizone_refactored/services/content_service.py index 46f6017f0..27f2a0ee9 100644 --- a/src/plugins/built_in/maizone_refactored/services/content_service.py +++ b/src/plugins/built_in/maizone_refactored/services/content_service.py @@ -13,6 +13,7 @@ from src.common.logger import get_logger import imghdr import asyncio from src.plugin_system.apis import llm_api, config_api, generator_api +from src.plugin_system.apis.cross_context_api import get_chat_history_by_group_name from src.chat.message_receive.chat_stream import get_chat_manager from maim_message import UserInfo from src.llm_models.utils_model import LLMRequest @@ -87,6 +88,11 @@ class ContentService: if context: prompt += f"\n作为参考,这里有一些最近的聊天记录:\n---\n{context}\n---" + # 添加跨群聊上下文 + cross_context = await get_chat_history_by_group_name("maizone_context_group") + if cross_context and "找不到名为" not in cross_context: + prompt += f"\n\n---跨群聊参考---\n{cross_context}\n---" + # 添加历史记录以避免重复 prompt += "\n\n---历史说说记录---\n" history_block = await get_send_history(qq_account) @@ -232,7 +238,7 @@ class ContentService: for i in range(3): # 重试3次 try: async with aiohttp.ClientSession() as session: - async with session.get(image_url, timeout=30) as resp: + async with session.get(image_url, timeout=aiohttp.ClientTimeout(total=30)) as resp: if resp.status != 200: logger.error(f"下载图片失败: {image_url}, status: {resp.status}") await asyncio.sleep(2) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 4d0257688..fceec7d8b 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.7.1" +version = "6.7.2" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -490,13 +490,9 @@ chat_ids = [ ["group", "1025509724"], # 假设这是“产品群”的ID ["private", "123456789"] # 假设这是某个用户的私聊 ] -[maizone_intercom] -# QQ空间互通组配置 -# 启用后,发布说说时会读取指定互通组的上下文 -enable = false # 定义QQ空间互通组 # 同一个组的chat_id会共享上下文,用于生成更相关的说说 -[[maizone_intercom.groups]] +[[cross_context.maizone_context_group]] name = "Maizone默认互通组" chat_ids = [ ["group", "111111"], # 示例群聊1 From 488e959577ecf66eee5666b65ceebe6e464c2bc4 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 5 Sep 2025 20:30:35 +0800 Subject: [PATCH 07/69] =?UTF-8?q?refactor(chat):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=B8=BB=E5=8A=A8=E6=80=9D=E8=80=83=E9=80=BB=E8=BE=91=E5=B9=B6?= =?UTF-8?q?=E5=B0=86=E5=85=B6=E9=9B=86=E6=88=90=E5=88=B0=E4=B8=BB=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原有的 `ProactiveThinker` 类中的逻辑直接整合进 `HeartFChatting` 类中。此举简化了整体架构,减少了类之间的耦合,并使得主动思考的触发机制与主聊天循环的状态管理更加统一。 主要变更: - 删除了独立的 `proactive_thinker.py` 文件。 - 将主动思考的监控循环、条件检查、动态间隔计算等功能实现为 `HeartFChatting` 的私有方法。 - 引入了 `ProactiveTriggerEvent` 事件,使触发源更加明确。 - 调整了相关模块的导入路径和配置项的调用方式,以适应新的结构。 --- src/chat/chat_loop/heartFC_chat.py | 124 +++++- src/chat/chat_loop/hfc_context.py | 3 +- src/chat/chat_loop/proactive/events.py | 11 + .../chat_loop/proactive/proactive_thinker.py | 125 +++++++ src/chat/chat_loop/proactive_thinker.py | 353 ------------------ src/chat/message_receive/chat_stream.py | 6 +- 6 files changed, 255 insertions(+), 367 deletions(-) create mode 100644 src/chat/chat_loop/proactive/events.py create mode 100644 src/chat/chat_loop/proactive/proactive_thinker.py delete mode 100644 src/chat/chat_loop/proactive_thinker.py diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index 41b072332..f172cf993 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -15,11 +15,12 @@ from src.plugin_system.apis import message_api from .hfc_context import HfcContext from .energy_manager import EnergyManager -from .proactive_thinker import ProactiveThinker +from .proactive.proactive_thinker import ProactiveThinker from .cycle_processor import CycleProcessor from .response_handler import ResponseHandler from .cycle_tracker import CycleTracker from .wakeup_manager import WakeUpManager +from .proactive.events import ProactiveTriggerEvent logger = get_logger("hfc") @@ -54,6 +55,7 @@ class HeartFChatting: self.context.chat_instance = self self._loop_task: Optional[asyncio.Task] = None + self._proactive_monitor_task: Optional[asyncio.Task] = None # 记录最近3次的兴趣度 self.recent_interest_records: deque = deque(maxlen=3) @@ -93,8 +95,12 @@ class HeartFChatting: self.context.relationship_builder = relationship_builder_manager.get_or_create_builder(self.context.stream_id) self.context.expression_learner = expression_learner_manager.get_expression_learner(self.context.stream_id) - #await self.energy_manager.start() - await self.proactive_thinker.start() + # 启动主动思考监视器 + if global_config.chat.enable_proactive_thinking: + self._proactive_monitor_task = asyncio.create_task(self._proactive_monitor_loop()) + self._proactive_monitor_task.add_done_callback(self._handle_proactive_monitor_completion) + logger.info(f"{self.context.log_prefix} 主动思考监视器已启动") + await self.wakeup_manager.start() self._loop_task = asyncio.create_task(self._main_chat_loop()) @@ -116,8 +122,12 @@ class HeartFChatting: return self.context.running = False - #await self.energy_manager.stop() - await self.proactive_thinker.stop() + # 停止主动思考监视器 + if self._proactive_monitor_task and not self._proactive_monitor_task.done(): + self._proactive_monitor_task.cancel() + await asyncio.sleep(0) + logger.info(f"{self.context.log_prefix} 主动思考监视器已停止") + await self.wakeup_manager.stop() if self._loop_task and not self._loop_task.done(): @@ -147,6 +157,92 @@ class HeartFChatting: except asyncio.CancelledError: logger.info(f"{self.context.log_prefix} HeartFChatting: 结束了聊天") + def _handle_proactive_monitor_completion(self, task: asyncio.Task): + try: + if exception := task.exception(): + logger.error(f"{self.context.log_prefix} 主动思考监视器异常: {exception}") + else: + logger.info(f"{self.context.log_prefix} 主动思考监视器正常结束") + except asyncio.CancelledError: + logger.info(f"{self.context.log_prefix} 主动思考监视器被取消") + + async def _proactive_monitor_loop(self): + while self.context.running: + await asyncio.sleep(15) + + if not self._should_enable_proactive_thinking(): + continue + + current_time = time.time() + silence_duration = current_time - self.context.last_message_time + target_interval = self._get_dynamic_thinking_interval() + + if silence_duration >= target_interval: + try: + formatted_time = self._format_duration(silence_duration) + event = ProactiveTriggerEvent( + source="silence_monitor", + reason=f"聊天已沉默 {formatted_time}", + metadata={"silence_duration": silence_duration} + ) + await self.proactive_thinker.think(event) + self.context.last_message_time = current_time + except Exception as e: + logger.error(f"{self.context.log_prefix} 主动思考触发执行出错: {e}") + logger.error(traceback.format_exc()) + + def _should_enable_proactive_thinking(self) -> bool: + if not self.context.chat_stream: + return False + + is_group_chat = self.context.chat_stream.group_info is not None + + if is_group_chat and not global_config.chat.proactive_thinking_in_group: + return False + if not is_group_chat and not global_config.chat.proactive_thinking_in_private: + return False + + stream_parts = self.context.stream_id.split(":") + current_chat_identifier = f"{stream_parts}:{stream_parts}" if len(stream_parts) >= 2 else self.context.stream_id + + enable_list = getattr(global_config.chat, "proactive_thinking_enable_in_groups" if is_group_chat else "proactive_thinking_enable_in_private", []) + return not enable_list or current_chat_identifier in enable_list + + def _get_dynamic_thinking_interval(self) -> float: + try: + from src.utils.timing_utils import get_normal_distributed_interval + base_interval = global_config.chat.proactive_thinking_interval + delta_sigma = getattr(global_config.chat, "delta_sigma", 120) + + if base_interval <= 0: base_interval = abs(base_interval) + if delta_sigma < 0: delta_sigma = abs(delta_sigma) + + if base_interval == 0 and delta_sigma == 0: return 300 + if delta_sigma == 0: return base_interval + + sigma_percentage = delta_sigma / base_interval if base_interval > 0 else delta_sigma / 1000 + return get_normal_distributed_interval(base_interval, sigma_percentage, 1, 86400, use_3sigma_rule=True) + + except ImportError: + logger.warning(f"{self.context.log_prefix} timing_utils不可用,使用固定间隔") + return max(300, abs(global_config.chat.proactive_thinking_interval)) + except Exception as e: + logger.error(f"{self.context.log_prefix} 动态间隔计算出错: {e},使用固定间隔") + return max(300, abs(global_config.chat.proactive_thinking_interval)) + + def _format_duration(self, seconds: float) -> str: + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + secs = int(seconds % 60) + parts = [] + if hours > 0: + parts.append(f"{hours}小时") + if minutes > 0: + parts.append(f"{minutes}分") + if secs > 0 or not parts: + parts.append(f"{secs}秒") + return "".join(parts) + async def _main_chat_loop(self): """ 主聊天循环 @@ -239,7 +335,7 @@ class HeartFChatting: # 根据聊天模式处理新消息 # 统一使用 _should_process_messages 判断是否应该处理 - should_process,interest_value = await self._should_process_messages(recent_messages if has_new_messages else None) + should_process,interest_value = await self._should_process_messages(recent_messages) if should_process: self.context.last_read_time = time.time() await self.cycle_processor.observe(interest_value = interest_value) @@ -248,7 +344,7 @@ class HeartFChatting: await asyncio.sleep(0.5) return True - if not await self._should_process_messages(recent_messages if has_new_messages else None): + if not await self._should_process_messages(recent_messages): return has_new_messages # 处理新消息 @@ -321,14 +417,15 @@ class HeartFChatting: def _determine_form_type(self) -> str: """判断使用哪种形式的no_reply""" # 检查是否启用breaking模式 - if not global_config.chat.enable_breaking_mode: + if not getattr(global_config.chat, "enable_breaking_mode", False): logger.info(f"{self.context.log_prefix} breaking模式已禁用,使用waiting形式") self.context.focus_energy = 1 - return + return "waiting" # 如果连续no_reply次数少于3次,使用waiting形式 if self.context.no_reply_consecutive <= 3: self.context.focus_energy = 1 + return "waiting" else: # 使用累积兴趣值而不是最近3次的记录 total_interest = self.context.breaking_accumulated_interest @@ -342,18 +439,23 @@ class HeartFChatting: if total_interest < adjusted_threshold: logger.info(f"{self.context.log_prefix} 累积兴趣度不足,进入breaking形式") self.context.focus_energy = random.randint(3, 6) + return "breaking" else: logger.info(f"{self.context.log_prefix} 累积兴趣度充足,使用waiting形式") self.context.focus_energy = 1 + return "waiting" async def _should_process_messages(self, new_message: List[Dict[str, Any]]) -> tuple[bool,float]: """ 统一判断是否应该处理消息的函数 根据当前循环模式和消息内容决定是否继续处理 """ + if not new_message: + return False, 0.0 + new_message_count = len(new_message) - talk_frequency = global_config.chat.get_current_talk_frequency(self.context.chat_stream.stream_id) + talk_frequency = global_config.chat.get_current_talk_frequency(self.context.stream_id) modified_exit_count_threshold = self.context.focus_energy * 0.5 / talk_frequency modified_exit_interest_threshold = 1.5 / talk_frequency @@ -402,7 +504,7 @@ class HeartFChatting: # 每10秒输出一次等待状态 if int(time.time() - self.context.last_read_time) > 0 and int(time.time() - self.context.last_read_time) % 10 == 0: logger.info( - f"{self.context.log_prefix} 已等待{time.time() - self.last_read_time:.0f}秒,累计{new_message_count}条消息,累积兴趣{total_interest:.1f},继续等待..." + f"{self.context.log_prefix} 已等待{time.time() - self.context.last_read_time:.0f}秒,累计{new_message_count}条消息,累积兴趣{total_interest:.1f},继续等待..." ) await asyncio.sleep(0.5) diff --git a/src/chat/chat_loop/hfc_context.py b/src/chat/chat_loop/hfc_context.py index c924f713d..8fc7d0dd0 100644 --- a/src/chat/chat_loop/hfc_context.py +++ b/src/chat/chat_loop/hfc_context.py @@ -11,6 +11,7 @@ from src.chat.chat_loop.hfc_utils import CycleDetail if TYPE_CHECKING: from .wakeup_manager import WakeUpManager from .energy_manager import EnergyManager + from .heartFC_chat import HeartFChatting class HfcContext: @@ -69,7 +70,7 @@ class HfcContext: # breaking形式下的累积兴趣值 self.breaking_accumulated_interest = 0.0 # 引用HeartFChatting实例,以便其他组件可以调用其方法 - self.chat_instance = None + self.chat_instance: Optional["HeartFChatting"] = None def save_context_state(self): """将当前状态保存到聊天流""" diff --git a/src/chat/chat_loop/proactive/events.py b/src/chat/chat_loop/proactive/events.py new file mode 100644 index 000000000..a81ece6c3 --- /dev/null +++ b/src/chat/chat_loop/proactive/events.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass, field +from typing import Optional, Dict, Any + +@dataclass +class ProactiveTriggerEvent: + """ + 主动思考触发事件的数据类 + """ + source: str # 触发源的标识,例如 "silence_monitor", "insomnia_manager" + reason: str # 触发的具体原因,例如 "聊天已沉默10分钟", "深夜emo" + metadata: Optional[Dict[str, Any]] = field(default_factory=dict) # 可选的元数据,用于传递额外信息 \ No newline at end of file diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py new file mode 100644 index 000000000..1df61cb4a --- /dev/null +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -0,0 +1,125 @@ +import time +import traceback +from typing import TYPE_CHECKING + +from src.common.logger import get_logger +from src.plugin_system.base.component_types import ChatMode +from ..hfc_context import HfcContext +from .events import ProactiveTriggerEvent + +if TYPE_CHECKING: + from ..cycle_processor import CycleProcessor + +logger = get_logger("hfc") + + +class ProactiveThinker: + def __init__(self, context: HfcContext, cycle_processor: "CycleProcessor"): + """ + 初始化主动思考器 + + Args: + context: HFC聊天上下文对象 + cycle_processor: 循环处理器,用于执行主动思考的结果 + + 功能说明: + - 接收主动思考事件并执行思考流程 + - 根据事件类型执行不同的前置操作(如修改情绪) + - 调用planner进行决策并由cycle_processor执行 + """ + self.context = context + self.cycle_processor = cycle_processor + + async def think(self, trigger_event: ProactiveTriggerEvent): + """ + 统一的API入口,用于触发主动思考 + + Args: + trigger_event: 描述触发上下文的事件对象 + """ + logger.info(f"{self.context.log_prefix} 接收到主动思考事件: " + f"来源='{trigger_event.source}', 原因='{trigger_event.reason}'") + + try: + # 1. 根据事件类型执行前置操作 + await self._prepare_for_thinking(trigger_event) + + # 2. 执行核心思考逻辑 + await self._execute_proactive_thinking(trigger_event) + + except Exception as e: + logger.error(f"{self.context.log_prefix} 主动思考 think 方法执行异常: {e}") + logger.error(traceback.format_exc()) + + async def _prepare_for_thinking(self, trigger_event: ProactiveTriggerEvent): + """ + 根据事件类型,执行思考前的准备工作,例如修改情绪 + + Args: + trigger_event: 触发事件 + """ + if trigger_event.source != "insomnia_manager": + return + + try: + from src.mood.mood_manager import mood_manager + mood_obj = mood_manager.get_mood_by_chat_id(self.context.stream_id) + new_mood = None + + if trigger_event.reason == "low_pressure": + new_mood = "精力过剩,毫无睡意" + elif trigger_event.reason == "random": + new_mood = "深夜emo,胡思乱想" + elif trigger_event.reason == "goodnight": + new_mood = "有点困了,准备睡觉了" + + if new_mood: + mood_obj.mood_state = new_mood + mood_obj.last_change_time = time.time() + logger.info(f"{self.context.log_prefix} 因 '{trigger_event.reason}'," + f"情绪状态被强制更新为: {mood_obj.mood_state}") + + except Exception as e: + logger.error(f"{self.context.log_prefix} 设置失眠情绪时出错: {e}") + + async def _execute_proactive_thinking(self, trigger_event: ProactiveTriggerEvent): + """ + 执行主动思考的核心逻辑 + + Args: + trigger_event: 触发事件 + """ + try: + # 直接调用 planner 的 PROACTIVE 模式 + actions, target_message = await self.cycle_processor.action_planner.plan( + mode=ChatMode.PROACTIVE + ) + + # 获取第一个规划出的动作作为主要决策 + action_result = actions[0] if actions else {} + + # 如果决策不是 do_nothing,则执行 + if action_result and action_result.get("action_type") != "do_nothing": + + # 在主动思考时,如果 target_message 为 None,则默认选取最新 message 作为 target_message + if target_message is None and self.context.chat_stream and self.context.chat_stream.context: + from src.chat.message_receive.message import MessageRecv + latest_message = self.context.chat_stream.context.get_last_message() + if isinstance(latest_message, MessageRecv): + user_info = latest_message.message_info.user_info + target_message = { + "chat_info_platform": latest_message.message_info.platform, + "user_platform": user_info.platform if user_info else None, + "user_id": user_info.user_id if user_info else None, + "processed_plain_text": latest_message.processed_plain_text, + "is_mentioned": latest_message.is_mentioned, + } + + # 将决策结果交给 cycle_processor 的后续流程处理 + await self.cycle_processor.execute_plan(action_result, target_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()) diff --git a/src/chat/chat_loop/proactive_thinker.py b/src/chat/chat_loop/proactive_thinker.py deleted file mode 100644 index 74816451f..000000000 --- a/src/chat/chat_loop/proactive_thinker.py +++ /dev/null @@ -1,353 +0,0 @@ -import asyncio -import time -import traceback -from typing import Optional, TYPE_CHECKING - -from src.common.logger import get_logger -from src.config.config import global_config -from src.plugin_system.base.component_types import ChatMode -from .hfc_context import HfcContext - -if TYPE_CHECKING: - from .cycle_processor import CycleProcessor - -logger = get_logger("hfc") - - -class ProactiveThinker: - def __init__(self, context: HfcContext, cycle_processor: "CycleProcessor"): - """ - 初始化主动思考器 - - Args: - context: HFC聊天上下文对象 - cycle_processor: 循环处理器,用于执行主动思考的结果 - - 功能说明: - - 管理机器人的主动发言功能 - - 根据沉默时间和配置触发主动思考 - - 提供私聊和群聊不同的思考提示模板 - - 使用3-sigma规则计算动态思考间隔 - """ - self.context = context - self.cycle_processor = cycle_processor - self._proactive_thinking_task: Optional[asyncio.Task] = None - - self.proactive_thinking_prompts = { - "private": """现在你和你朋友的私聊里面已经隔了{time}没有发送消息了,请你结合上下文以及你和你朋友之前聊过的话题和你的人设来决定要不要主动发送消息,你可以选择: - - 1. 继续保持沉默(当{time}以前已经结束了一个话题并且你不想挑起新话题时) - 2. 选择回复(当{time}以前你发送了一条消息且没有人回复你时、你想主动挑起一个话题时) - - 请根据当前情况做出选择。如果选择回复,请直接发送你想说的内容;如果选择保持沉默,请只回复"沉默"(注意:这个词不会被发送到群聊中)。""", - "group": """现在群里面已经隔了{time}没有人发送消息了,请你结合上下文以及群聊里面之前聊过的话题和你的人设来决定要不要主动发送消息,你可以选择: - - 1. 继续保持沉默(当{time}以前已经结束了一个话题并且你不想挑起新话题时) - 2. 选择回复(当{time}以前你发送了一条消息且没有人回复你时、你想主动挑起一个话题时) - - 请根据当前情况做出选择。如果选择回复,请直接发送你想说的内容;如果选择保持沉默,请只回复"沉默"(注意:这个词不会被发送到群聊中)。""", - } - - async def start(self): - """ - 启动主动思考器 - - 功能说明: - - 检查运行状态和配置,避免重复启动 - - 只有在启用主动思考功能时才启动 - - 创建主动思考循环异步任务 - - 设置任务完成回调处理 - - 记录启动日志 - """ - if self.context.running and not self._proactive_thinking_task and global_config.chat.enable_proactive_thinking: - self._proactive_thinking_task = asyncio.create_task(self._proactive_thinking_loop()) - self._proactive_thinking_task.add_done_callback(self._handle_proactive_thinking_completion) - logger.info(f"{self.context.log_prefix} 主动思考器已启动") - - async def stop(self): - """ - 停止主动思考器 - - 功能说明: - - 取消正在运行的主动思考任务 - - 等待任务完全停止 - - 记录停止日志 - """ - if self._proactive_thinking_task and not self._proactive_thinking_task.done(): - self._proactive_thinking_task.cancel() - await asyncio.sleep(0) - logger.info(f"{self.context.log_prefix} 主动思考器已停止") - - def _handle_proactive_thinking_completion(self, task: asyncio.Task): - """ - 处理主动思考任务完成 - - Args: - task: 完成的异步任务对象 - - 功能说明: - - 处理任务正常完成或异常情况 - - 记录相应的日志信息 - - 区分取消和异常终止的情况 - """ - try: - if exception := task.exception(): - logger.error(f"{self.context.log_prefix} 主动思考循环异常: {exception}") - else: - logger.info(f"{self.context.log_prefix} 主动思考循环正常结束") - except asyncio.CancelledError: - logger.info(f"{self.context.log_prefix} 主动思考循环被取消") - - async def _proactive_thinking_loop(self): - """ - 主动思考的主循环 - - 功能说明: - - 每15秒检查一次是否需要主动思考 - - 只在FOCUS模式下进行主动思考 - - 检查是否启用主动思考功能 - - 计算沉默时间并与动态间隔比较 - - 达到条件时执行主动思考并更新最后消息时间 - - 处理执行过程中的异常 - """ - while self.context.running: - await asyncio.sleep(15) - - if self.context.loop_mode != ChatMode.FOCUS: - continue - - if not self._should_enable_proactive_thinking(): - continue - - current_time = time.time() - silence_duration = current_time - self.context.last_message_time - - target_interval = self._get_dynamic_thinking_interval() - - if silence_duration >= target_interval: - try: - await self._execute_proactive_thinking(silence_duration) - self.context.last_message_time = current_time - except Exception as e: - logger.error(f"{self.context.log_prefix} 主动思考执行出错: {e}") - logger.error(traceback.format_exc()) - - def _should_enable_proactive_thinking(self) -> bool: - """ - 检查是否应该启用主动思考 - - Returns: - bool: 如果应该启用主动思考则返回True - - 功能说明: - - 检查聊天流是否存在 - - 检查当前聊天是否在启用列表中(按平台和类型分别检查) - - 根据聊天类型(群聊/私聊)和配置决定是否启用 - - 群聊需要proactive_thinking_in_group为True - - 私聊需要proactive_thinking_in_private为True - """ - if not self.context.chat_stream: - return False - - is_group_chat = self.context.chat_stream.group_info is not None - - # 检查基础开关 - if is_group_chat and not global_config.chat.proactive_thinking_in_group: - return False - if not is_group_chat and not global_config.chat.proactive_thinking_in_private: - return False - - # 获取当前聊天的完整标识 (platform:chat_id) - stream_parts = self.context.stream_id.split(":") - if len(stream_parts) >= 2: - platform = stream_parts[0] - chat_id = stream_parts[1] - current_chat_identifier = f"{platform}:{chat_id}" - else: - # 如果无法解析,则使用原始stream_id - current_chat_identifier = self.context.stream_id - - # 检查是否在启用列表中 - if is_group_chat: - # 群聊检查 - enable_list = getattr(global_config.chat, "proactive_thinking_enable_in_groups", []) - if enable_list and current_chat_identifier not in enable_list: - return False - else: - # 私聊检查 - enable_list = getattr(global_config.chat, "proactive_thinking_enable_in_private", []) - if enable_list and current_chat_identifier not in enable_list: - return False - - return True - - def _get_dynamic_thinking_interval(self) -> float: - """ - 获取动态思考间隔 - - Returns: - float: 计算得出的思考间隔时间(秒) - - 功能说明: - - 使用3-sigma规则计算正态分布的思考间隔 - - 基于base_interval和delta_sigma配置计算 - - 处理特殊情况(为0或负数的配置) - - 如果timing_utils不可用则使用固定间隔 - - 间隔范围被限制在1秒到86400秒(1天)之间 - """ - try: - from src.utils.timing_utils import get_normal_distributed_interval - - base_interval = global_config.chat.proactive_thinking_interval - delta_sigma = getattr(global_config.chat, "delta_sigma", 120) - - if base_interval < 0: - base_interval = abs(base_interval) - if delta_sigma < 0: - delta_sigma = abs(delta_sigma) - - if base_interval == 0 and delta_sigma == 0: - return 300 - elif base_interval == 0: - sigma_percentage = delta_sigma / 1000 - return get_normal_distributed_interval(0, sigma_percentage, 1, 86400, use_3sigma_rule=True) - elif delta_sigma == 0: - return base_interval - - sigma_percentage = delta_sigma / base_interval - return get_normal_distributed_interval(base_interval, sigma_percentage, 1, 86400, use_3sigma_rule=True) - - except ImportError: - logger.warning(f"{self.context.log_prefix} timing_utils不可用,使用固定间隔") - return max(300, abs(global_config.chat.proactive_thinking_interval)) - except Exception as e: - logger.error(f"{self.context.log_prefix} 动态间隔计算出错: {e},使用固定间隔") - return max(300, abs(global_config.chat.proactive_thinking_interval)) - - def _format_duration(self, seconds: float) -> str: - """ - 格式化持续时间为中文描述 - - Args: - seconds: 持续时间(秒) - - Returns: - str: 格式化后的时间字符串,如"1小时30分45秒" - - 功能说明: - - 将秒数转换为小时、分钟、秒的组合 - - 只显示非零的时间单位 - - 如果所有单位都为0则显示"0秒" - - 用于主动思考日志的时间显示 - """ - hours = int(seconds // 3600) - minutes = int((seconds % 3600) // 60) - secs = int(seconds % 60) - - parts = [] - if hours > 0: - parts.append(f"{hours}小时") - if minutes > 0: - parts.append(f"{minutes}分") - if secs > 0 or not parts: - parts.append(f"{secs}秒") - - return "".join(parts) - - async def _execute_proactive_thinking(self, silence_duration: float): - """ - 执行主动思考 - - Args: - silence_duration: 沉默持续时间(秒) - """ - formatted_time = self._format_duration(silence_duration) - logger.info(f"{self.context.log_prefix} 触发主动思考,已沉默{formatted_time}") - - try: - # 直接调用 planner 的 PROACTIVE 模式 - action_result_tuple, target_message = await self.cycle_processor.action_planner.plan( - mode=ChatMode.PROACTIVE - ) - action_result = action_result_tuple.get("action_result") - - # 如果决策不是 do_nothing,则执行 - if action_result and action_result.get("action_type") != "do_nothing": - logger.info(f"{self.context.log_prefix} 主动思考决策: {action_result.get('action_type')}, 原因: {action_result.get('reasoning')}") - # 在主动思考时,如果 target_message 为 None,则默认选取最新 message 作为 target_message - if target_message is None and self.context.chat_stream and self.context.chat_stream.context: - from src.chat.message_receive.message import MessageRecv - latest_message = self.context.chat_stream.context.get_last_message() - if isinstance(latest_message, MessageRecv): - user_info = latest_message.message_info.user_info - target_message = { - "chat_info_platform": latest_message.message_info.platform, - "user_platform": user_info.platform if user_info else None, - "user_id": user_info.user_id if user_info else None, - "processed_plain_text": latest_message.processed_plain_text, - "is_mentioned": latest_message.is_mentioned, - } - - # 将决策结果交给 cycle_processor 的后续流程处理 - await self.cycle_processor.execute_plan(action_result, target_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 trigger_insomnia_thinking(self, reason: str): - """ - 由外部事件(如失眠)触发的一次性主动思考 - - Args: - reason: 触发的原因 (e.g., "low_pressure", "random") - """ - logger.info(f"{self.context.log_prefix} 因“{reason}”触发失眠,开始深夜思考...") - - # 1. 根据原因修改情绪 - try: - from src.mood.mood_manager import mood_manager - - mood_obj = mood_manager.get_mood_by_chat_id(self.context.stream_id) - if reason == "low_pressure": - mood_obj.mood_state = "精力过剩,毫无睡意" - elif reason == "random": - mood_obj.mood_state = "深夜emo,胡思乱想" - mood_obj.last_change_time = time.time() # 更新时间戳以允许后续的情绪回归 - logger.info(f"{self.context.log_prefix} 因失眠,情绪状态被强制更新为: {mood_obj.mood_state}") - except Exception as e: - logger.error(f"{self.context.log_prefix} 设置失眠情绪时出错: {e}") - - # 2. 直接执行主动思考逻辑 - try: - # 传入一个象征性的silence_duration,因为它在这里不重要 - await self._execute_proactive_thinking(silence_duration=1) - except Exception as e: - logger.error(f"{self.context.log_prefix} 失眠思考执行出错: {e}") - logger.error(traceback.format_exc()) - - async def trigger_goodnight_thinking(self): - """ - 在失眠状态结束后,触发一次准备睡觉的主动思考 - """ - logger.info(f"{self.context.log_prefix} 失眠状态结束,准备睡觉,触发告别思考...") - - # 1. 设置一个准备睡觉的特定情绪 - try: - from src.mood.mood_manager import mood_manager - - mood_obj = mood_manager.get_mood_by_chat_id(self.context.stream_id) - mood_obj.mood_state = "有点困了,准备睡觉了" - mood_obj.last_change_time = time.time() - logger.info(f"{self.context.log_prefix} 情绪状态更新为: {mood_obj.mood_state}") - except Exception as e: - logger.error(f"{self.context.log_prefix} 设置睡前情绪时出错: {e}") - - # 2. 直接执行主动思考逻辑 - try: - await self._execute_proactive_thinking(silence_duration=1) - except Exception as e: - logger.error(f"{self.context.log_prefix} 睡前告别思考执行出错: {e}") - logger.error(traceback.format_exc()) diff --git a/src/chat/message_receive/chat_stream.py b/src/chat/message_receive/chat_stream.py index 29b03815c..fe0fb90b2 100644 --- a/src/chat/message_receive/chat_stream.py +++ b/src/chat/message_receive/chat_stream.py @@ -85,6 +85,7 @@ class ChatStream: self.context: ChatMessageContext = None # type: ignore # 用于存储该聊天的上下文信息 self.focus_energy = 1 self.no_reply_consecutive = 0 + self.breaking_accumulated_interest = 0.0 def to_dict(self) -> dict: """转换为字典格式""" @@ -97,6 +98,7 @@ class ChatStream: "last_active_time": self.last_active_time, "energy_value": self.energy_value, "sleep_pressure": self.sleep_pressure, + "breaking_accumulated_interest": self.breaking_accumulated_interest, } @classmethod @@ -257,7 +259,7 @@ class ChatManager: "user_cardname": model_instance.user_cardname or "", } group_info_data = None - if model_instance.group_id: + if model_instance and getattr(model_instance, 'group_id', None): group_info_data = { "platform": model_instance.group_platform, "group_id": model_instance.group_id, @@ -403,7 +405,7 @@ class ChatManager: "user_cardname": model_instance.user_cardname or "", } group_info_data = None - if model_instance.group_id: + if model_instance and getattr(model_instance, 'group_id', None): group_info_data = { "platform": model_instance.group_platform, "group_id": model_instance.group_id, From 513757a8ee33a4970d92d1135f1dbee93dc1d40e Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 5 Sep 2025 20:58:03 +0800 Subject: [PATCH 08/69] chore: format code and remove redundant blank lines This commit applies automated code formatting across the project. The changes primarily involve removing unnecessary blank lines and ensuring consistent code style, improving readability and maintainability without altering functionality. --- plugins/napcat_adapter_plugin/plugin.py | 49 +++---- .../src/config/official_configs.py | 6 +- .../src/message_chunker.py | 114 ++++++++-------- .../src/recv_handler/message_handler.py | 33 ++--- .../src/recv_handler/message_sending.py | 22 ++-- src/chat/chat_loop/cycle_processor.py | 94 ++++++------- src/chat/chat_loop/cycle_tracker.py | 13 +- src/chat/chat_loop/heartFC_chat.py | 84 +++++++----- src/chat/chat_loop/hfc_context.py | 10 +- src/chat/chat_loop/hfc_utils.py | 1 + src/chat/chat_loop/proactive/events.py | 4 +- .../chat_loop/proactive/proactive_thinker.py | 21 +-- src/chat/chat_loop/response_handler.py | 2 +- src/chat/emoji_system/emoji_manager.py | 4 +- src/chat/message_receive/chat_stream.py | 4 +- src/chat/message_receive/message.py | 2 +- src/chat/message_receive/storage.py | 2 +- src/chat/planner_actions/action_modifier.py | 6 +- src/chat/planner_actions/planner.py | 123 +++++++----------- src/chat/replyer/default_generator.py | 16 ++- src/chat/utils/chat_message_builder.py | 2 +- src/chat/utils/prompt_utils.py | 43 +++--- src/chat/utils/smart_prompt.py | 8 +- src/chat/utils/utils_image.py | 8 +- src/common/database/monthly_plan_db.py | 10 +- src/config/config.py | 4 +- src/config/official_configs.py | 4 +- src/main.py | 44 +++++-- src/plugin_system/apis/cross_context_api.py | 32 ++--- src/plugin_system/apis/generator_api.py | 4 +- src/plugin_system/apis/send_api.py | 29 +++-- src/plugin_system/core/event_manager.py | 2 +- .../built_in/maizone_refactored/plugin.py | 18 ++- .../services/qzone_service.py | 6 +- .../services/reply_tracker_service.py | 20 +-- src/schedule/monthly_plan_manager.py | 8 +- src/schedule/schedule_manager.py | 6 +- 37 files changed, 439 insertions(+), 419 deletions(-) diff --git a/plugins/napcat_adapter_plugin/plugin.py b/plugins/napcat_adapter_plugin/plugin.py index b5f31e167..a87eeb437 100644 --- a/plugins/napcat_adapter_plugin/plugin.py +++ b/plugins/napcat_adapter_plugin/plugin.py @@ -37,6 +37,7 @@ def get_classes_in_module(module): classes.append(member) return classes + async def message_recv(server_connection: Server.ServerConnection): await message_handler.set_server_connection(server_connection) asyncio.create_task(notice_handler.set_server_connection(server_connection)) @@ -47,7 +48,7 @@ async def message_recv(server_connection: Server.ServerConnection): try: # 首先尝试解析原始消息 decoded_raw_message: dict = json.loads(raw_message) - + # 检查是否是切片消息 (来自 MMC) if chunker.is_chunk_message(decoded_raw_message): logger.debug("接收到切片消息,尝试重组") @@ -61,14 +62,14 @@ async def message_recv(server_connection: Server.ServerConnection): # 切片尚未完整,继续等待更多切片 logger.debug("等待更多切片...") continue - + # 处理完整消息(可能是重组后的,也可能是原本就完整的) post_type = decoded_raw_message.get("post_type") if post_type in ["meta_event", "message", "notice"]: await message_queue.put(decoded_raw_message) elif post_type is None: await put_response(decoded_raw_message) - + except json.JSONDecodeError as e: logger.error(f"消息解析失败: {e}") logger.debug(f"原始消息: {raw_message[:500]}...") @@ -76,6 +77,7 @@ async def message_recv(server_connection: Server.ServerConnection): logger.error(f"处理消息时出错: {e}") logger.debug(f"原始消息: {raw_message[:500]}...") + async def message_process(): """消息处理主循环""" logger.info("消息处理器已启动") @@ -84,7 +86,7 @@ async def message_process(): try: # 使用超时等待,以便能够响应取消请求 message = await asyncio.wait_for(message_queue.get(), timeout=1.0) - + post_type = message.get("post_type") if post_type == "message": await message_handler.handle_raw_message(message) @@ -94,10 +96,10 @@ async def message_process(): await notice_handler.handle_notice(message) else: logger.warning(f"未知的post_type: {post_type}") - + message_queue.task_done() await asyncio.sleep(0.05) - + except asyncio.TimeoutError: # 超时是正常的,继续循环 continue @@ -112,7 +114,7 @@ async def message_process(): except ValueError: pass await asyncio.sleep(0.1) - + except asyncio.CancelledError: logger.info("消息处理器已停止") raise @@ -132,6 +134,7 @@ async def message_process(): except Exception as e: logger.debug(f"清理消息队列时出错: {e}") + async def napcat_server(): """启动 Napcat WebSocket 连接(支持正向和反向连接)""" mode = global_config.napcat_server.mode @@ -143,63 +146,61 @@ async def napcat_server(): logger.error(f"启动 WebSocket 连接失败: {e}") raise + async def graceful_shutdown(): """优雅关闭所有组件""" try: logger.info("正在关闭adapter...") - + # 停止消息重组器的清理任务 try: await reassembler.stop_cleanup_task() except Exception as e: logger.warning(f"停止消息重组器清理任务时出错: {e}") - + # 停止功能管理器文件监控 try: await features_manager.stop_file_watcher() except Exception as e: logger.warning(f"停止功能管理器文件监控时出错: {e}") - + # 关闭消息处理器(包括消息缓冲器) try: await message_handler.shutdown() except Exception as e: logger.warning(f"关闭消息处理器时出错: {e}") - + # 关闭 WebSocket 连接 try: await websocket_manager.stop_connection() except Exception as e: logger.warning(f"关闭WebSocket连接时出错: {e}") - + # 关闭 MaiBot 连接 try: await mmc_stop_com() except Exception as e: logger.warning(f"关闭MaiBot连接时出错: {e}") - + # 取消所有剩余任务 current_task = asyncio.current_task() tasks = [t for t in asyncio.all_tasks() if t is not current_task and not t.done()] - + if tasks: logger.info(f"正在取消 {len(tasks)} 个剩余任务...") for task in tasks: task.cancel() - + # 等待任务取消完成,忽略 CancelledError try: - await asyncio.wait_for( - asyncio.gather(*tasks, return_exceptions=True), - timeout=10 - ) + await asyncio.wait_for(asyncio.gather(*tasks, return_exceptions=True), timeout=10) except asyncio.TimeoutError: logger.warning("部分任务取消超时") except Exception as e: logger.debug(f"任务取消过程中的异常(可忽略): {e}") - + logger.info("Adapter已成功关闭") - + except Exception as e: logger.error(f"Adapter关闭中出现错误: {e}") finally: @@ -214,6 +215,7 @@ async def graceful_shutdown(): except Exception: pass + class LauchNapcatAdapterHandler(BaseEventHandler): """自动启动Adapter""" @@ -245,6 +247,7 @@ class LauchNapcatAdapterHandler(BaseEventHandler): asyncio.create_task(message_process()) asyncio.create_task(check_timeout_response()) + class StopNapcatAdapterHandler(BaseEventHandler): """关闭Adapter""" @@ -257,7 +260,7 @@ class StopNapcatAdapterHandler(BaseEventHandler): async def execute(self, kwargs): await graceful_shutdown() return - + @register_plugin class NapcatAdapterPlugin(BasePlugin): @@ -295,7 +298,7 @@ class NapcatAdapterPlugin(BasePlugin): def get_plugin_components(self): self.register_events() - + components = [] components.append((LauchNapcatAdapterHandler.get_handler_info(), LauchNapcatAdapterHandler)) components.append((StopNapcatAdapterHandler.get_handler_info(), StopNapcatAdapterHandler)) diff --git a/plugins/napcat_adapter_plugin/src/config/official_configs.py b/plugins/napcat_adapter_plugin/src/config/official_configs.py index ba2c19e12..35bac1d57 100644 --- a/plugins/napcat_adapter_plugin/src/config/official_configs.py +++ b/plugins/napcat_adapter_plugin/src/config/official_configs.py @@ -58,14 +58,16 @@ class VoiceConfig(ConfigBase): use_tts: bool = False """是否启用TTS功能""" + @dataclass class SlicingConfig(ConfigBase): max_frame_size: int = 64 """WebSocket帧的最大大小,单位为字节,默认64KB""" - + delay_ms: int = 10 """切片发送间隔时间,单位为毫秒""" - + + @dataclass class DebugConfig(ConfigBase): level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO" diff --git a/plugins/napcat_adapter_plugin/src/message_chunker.py b/plugins/napcat_adapter_plugin/src/message_chunker.py index 9ba27f3c0..f4e150711 100644 --- a/plugins/napcat_adapter_plugin/src/message_chunker.py +++ b/plugins/napcat_adapter_plugin/src/message_chunker.py @@ -3,6 +3,7 @@ 用于在 Ada 发送给 MMC 时进行消息切片,利用 WebSocket 协议的自动重组特性 仅在 Ada -> MMC 方向进行切片,其他方向(MMC -> Ada,Ada <-> Napcat)不切片 """ + import json import uuid import asyncio @@ -15,10 +16,9 @@ from src.common.logger import get_logger logger = get_logger("napcat_adapter") - class MessageChunker: """消息切片器,用于处理大消息的分片发送""" - + def __init__(self): self.max_chunk_size = global_config.slicing.max_frame_size * 1024 @@ -29,19 +29,21 @@ class MessageChunker: message_str = json.dumps(message, ensure_ascii=False) else: message_str = message - return len(message_str.encode('utf-8')) > self.max_chunk_size + return len(message_str.encode("utf-8")) > self.max_chunk_size except Exception as e: logger.error(f"检查消息大小时出错: {e}") return False - - def chunk_message(self, message: Union[str, Dict[str, Any]], chunk_id: Optional[str] = None) -> List[Dict[str, Any]]: + + def chunk_message( + self, message: Union[str, Dict[str, Any]], chunk_id: Optional[str] = None + ) -> List[Dict[str, Any]]: """ 将消息切片 - + Args: message: 要切片的消息(字符串或字典) chunk_id: 切片组ID,如果不提供则自动生成 - + Returns: 切片后的消息字典列表 """ @@ -51,30 +53,30 @@ class MessageChunker: message_str = json.dumps(message, ensure_ascii=False) else: message_str = message - + if not self.should_chunk_message(message_str): # 不需要切片的情况,如果输入是字典则返回字典,如果是字符串则包装成非切片标记的字典 if isinstance(message, dict): return [message] else: return [{"_original_message": message_str}] - + if chunk_id is None: chunk_id = str(uuid.uuid4()) - - message_bytes = message_str.encode('utf-8') + + message_bytes = message_str.encode("utf-8") total_size = len(message_bytes) - + # 计算需要多少个切片 num_chunks = (total_size + self.max_chunk_size - 1) // self.max_chunk_size - + chunks = [] for i in range(num_chunks): start_pos = i * self.max_chunk_size end_pos = min(start_pos + self.max_chunk_size, total_size) - + chunk_data = message_bytes[start_pos:end_pos] - + # 构建切片消息 chunk_message = { "__mmc_chunk_info__": { @@ -83,17 +85,17 @@ class MessageChunker: "total_chunks": num_chunks, "chunk_size": len(chunk_data), "total_size": total_size, - "timestamp": time.time() + "timestamp": time.time(), }, - "__mmc_chunk_data__": chunk_data.decode('utf-8', errors='ignore'), - "__mmc_is_chunked__": True + "__mmc_chunk_data__": chunk_data.decode("utf-8", errors="ignore"), + "__mmc_is_chunked__": True, } - + chunks.append(chunk_message) - + logger.debug(f"消息切片完成: {total_size} bytes -> {num_chunks} chunks (ID: {chunk_id})") return chunks - + except Exception as e: logger.error(f"消息切片时出错: {e}") # 出错时返回原消息 @@ -101,7 +103,7 @@ class MessageChunker: return [message] else: return [{"_original_message": message}] - + def is_chunk_message(self, message: Union[str, Dict[str, Any]]) -> bool: """判断是否是切片消息""" try: @@ -109,12 +111,12 @@ class MessageChunker: data = json.loads(message) else: data = message - + return ( - isinstance(data, dict) and - "__mmc_chunk_info__" in data and - "__mmc_chunk_data__" in data and - "__mmc_is_chunked__" in data + isinstance(data, dict) + and "__mmc_chunk_info__" in data + and "__mmc_chunk_data__" in data + and "__mmc_is_chunked__" in data ) except (json.JSONDecodeError, TypeError): return False @@ -122,17 +124,17 @@ class MessageChunker: class MessageReassembler: """消息重组器,用于重组接收到的切片消息""" - + def __init__(self, timeout: int = 30): self.timeout = timeout self.chunk_buffers: Dict[str, Dict[str, Any]] = {} self._cleanup_task = None - + async def start_cleanup_task(self): """启动清理任务""" if self._cleanup_task is None: self._cleanup_task = asyncio.create_task(self._cleanup_expired_chunks()) - + async def stop_cleanup_task(self): """停止清理任务""" if self._cleanup_task: @@ -142,35 +144,35 @@ class MessageReassembler: except asyncio.CancelledError: pass self._cleanup_task = None - + async def _cleanup_expired_chunks(self): """清理过期的切片缓冲区""" while True: try: await asyncio.sleep(10) # 每10秒检查一次 current_time = time.time() - + expired_chunks = [] for chunk_id, buffer_info in self.chunk_buffers.items(): - if current_time - buffer_info['timestamp'] > self.timeout: + if current_time - buffer_info["timestamp"] > self.timeout: expired_chunks.append(chunk_id) - + for chunk_id in expired_chunks: logger.warning(f"清理过期的切片缓冲区: {chunk_id}") del self.chunk_buffers[chunk_id] - + except asyncio.CancelledError: break except Exception as e: logger.error(f"清理过期切片时出错: {e}") - + async def add_chunk(self, message: Union[str, Dict[str, Any]]) -> Optional[Dict[str, Any]]: """ 添加切片,如果切片完整则返回重组后的消息 - + Args: message: 切片消息(字符串或字典) - + Returns: 如果切片完整则返回重组后的原始消息字典,否则返回None """ @@ -180,7 +182,7 @@ class MessageReassembler: chunk_data = json.loads(message) else: chunk_data = message - + # 检查是否是切片消息 if not chunker.is_chunk_message(chunk_data): # 不是切片消息,直接返回 @@ -192,38 +194,38 @@ class MessageReassembler: return {"text_message": chunk_data["_original_message"]} else: return chunk_data - + chunk_info = chunk_data["__mmc_chunk_info__"] chunk_content = chunk_data["__mmc_chunk_data__"] - + chunk_id = chunk_info["chunk_id"] chunk_index = chunk_info["chunk_index"] total_chunks = chunk_info["total_chunks"] chunk_timestamp = chunk_info.get("timestamp", time.time()) - + # 初始化缓冲区 if chunk_id not in self.chunk_buffers: self.chunk_buffers[chunk_id] = { "chunks": {}, "total_chunks": total_chunks, "received_chunks": 0, - "timestamp": chunk_timestamp + "timestamp": chunk_timestamp, } - + buffer = self.chunk_buffers[chunk_id] - + # 检查切片是否已经接收过 if chunk_index in buffer["chunks"]: logger.warning(f"重复接收切片: {chunk_id}#{chunk_index}") return None - + # 添加切片 buffer["chunks"][chunk_index] = chunk_content buffer["received_chunks"] += 1 buffer["timestamp"] = time.time() # 更新时间戳 - + logger.debug(f"接收切片: {chunk_id}#{chunk_index} ({buffer['received_chunks']}/{total_chunks})") - + # 检查是否接收完整 if buffer["received_chunks"] == total_chunks: # 重组消息 @@ -233,25 +235,25 @@ class MessageReassembler: logger.error(f"切片 {chunk_id}#{i} 缺失,无法重组") return None reassembled_message += buffer["chunks"][i] - + # 清理缓冲区 del self.chunk_buffers[chunk_id] - + logger.debug(f"消息重组完成: {chunk_id} ({len(reassembled_message)} chars)") - + # 尝试反序列化重组后的消息 try: return json.loads(reassembled_message) except json.JSONDecodeError: # 如果不能反序列化为JSON,则作为文本消息返回 return {"text_message": reassembled_message} - + return None - + except (json.JSONDecodeError, KeyError, TypeError) as e: logger.error(f"处理切片消息时出错: {e}") return None - + def get_pending_chunks_info(self) -> Dict[str, Any]: """获取待处理切片信息""" info = {} @@ -260,11 +262,11 @@ class MessageReassembler: "received": buffer["received_chunks"], "total": buffer["total_chunks"], "progress": f"{buffer['received_chunks']}/{buffer['total_chunks']}", - "age_seconds": time.time() - buffer["timestamp"] + "age_seconds": time.time() - buffer["timestamp"], } return info # 全局实例 chunker = MessageChunker() -reassembler = MessageReassembler() \ No newline at end of file +reassembler = MessageReassembler() diff --git a/plugins/napcat_adapter_plugin/src/recv_handler/message_handler.py b/plugins/napcat_adapter_plugin/src/recv_handler/message_handler.py index aad211106..1bd34ceac 100644 --- a/plugins/napcat_adapter_plugin/src/recv_handler/message_handler.py +++ b/plugins/napcat_adapter_plugin/src/recv_handler/message_handler.py @@ -743,31 +743,31 @@ class MessageHandler: """ message_data: dict = raw_message.get("data", {}) json_data = message_data.get("data", "") - + # 检查JSON消息格式 if not message_data or "data" not in message_data: logger.warning("JSON消息格式不正确") return Seg(type="json", data=json.dumps(message_data)) - + try: nested_data = json.loads(json_data) - + # 检查是否是QQ小程序分享消息 if "app" in nested_data and "com.tencent.miniapp" in str(nested_data.get("app", "")): logger.debug("检测到QQ小程序分享消息,开始提取信息") - + # 提取目标字段 extracted_info = {} - + # 提取 meta.detail_1 中的信息 meta = nested_data.get("meta", {}) detail_1 = meta.get("detail_1", {}) - + if detail_1: extracted_info["title"] = detail_1.get("title", "") extracted_info["desc"] = detail_1.get("desc", "") qqdocurl = detail_1.get("qqdocurl", "") - + # 从qqdocurl中提取b23.tv短链接 if qqdocurl and "b23.tv" in qqdocurl: # 查找b23.tv链接的起始位置 @@ -785,26 +785,29 @@ class MessageHandler: extracted_info["short_url"] = qqdocurl else: extracted_info["short_url"] = qqdocurl - + # 如果成功提取到关键信息,返回格式化的文本 if extracted_info.get("title") or extracted_info.get("desc") or extracted_info.get("short_url"): content_parts = [] - + if extracted_info.get("title"): content_parts.append(f"来源: {extracted_info['title']}") - + if extracted_info.get("desc"): content_parts.append(f"标题: {extracted_info['desc']}") - + if extracted_info.get("short_url"): content_parts.append(f"链接: {extracted_info['short_url']}") - + formatted_content = "\n".join(content_parts) - return Seg(type="text", data=f"这是一条小程序分享消息,可以根据来源,考虑使用对应解析工具\n{formatted_content}") - + return Seg( + type="text", + data=f"这是一条小程序分享消息,可以根据来源,考虑使用对应解析工具\n{formatted_content}", + ) + # 如果没有提取到关键信息,返回None return None - + except json.JSONDecodeError as e: logger.error(f"解析JSON消息失败: {e}") return None diff --git a/plugins/napcat_adapter_plugin/src/recv_handler/message_sending.py b/plugins/napcat_adapter_plugin/src/recv_handler/message_sending.py index b08b8a77e..ff9b8def1 100644 --- a/plugins/napcat_adapter_plugin/src/recv_handler/message_sending.py +++ b/plugins/napcat_adapter_plugin/src/recv_handler/message_sending.py @@ -28,36 +28,36 @@ class MessageSending: try: # 检查是否需要切片发送 message_dict = message_base.to_dict() - + if chunker.should_chunk_message(message_dict): logger.info(f"消息过大,进行切片发送到 MaiBot") - + # 切片消息 chunks = chunker.chunk_message(message_dict) - + # 逐个发送切片 for i, chunk in enumerate(chunks): - logger.debug(f"发送切片 {i+1}/{len(chunks)} 到 MaiBot") - + logger.debug(f"发送切片 {i + 1}/{len(chunks)} 到 MaiBot") + # 获取对应的客户端并发送切片 platform = message_base.message_info.platform if platform not in self.maibot_router.clients: logger.error(f"平台 {platform} 未连接") return False - + client = self.maibot_router.clients[platform] send_status = await client.send_message(chunk) - + if not send_status: - logger.error(f"发送切片 {i+1}/{len(chunks)} 失败") + logger.error(f"发送切片 {i + 1}/{len(chunks)} 失败") return False - + # 使用配置中的延迟时间 if i < len(chunks) - 1: delay_seconds = global_config.slicing.delay_ms / 1000.0 logger.debug(f"切片发送延迟: {global_config.slicing.delay_ms}毫秒") await asyncio.sleep(delay_seconds) - + logger.debug("所有切片发送完成") return True else: @@ -66,7 +66,7 @@ class MessageSending: if not send_status: raise RuntimeError("可能是路由未正确配置或连接异常") return send_status - + except Exception as e: logger.error(f"发送消息失败: {str(e)}") logger.error("请检查与MaiBot之间的连接") diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index b35905361..975b6134b 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -32,7 +32,7 @@ class CycleProcessor: context: HFC聊天上下文对象,包含聊天流、能量值等信息 response_handler: 响应处理器,负责生成和发送回复 cycle_tracker: 循环跟踪器,负责记录和管理每次思考循环的信息 - """ + """ self.context = context self.response_handler = response_handler self.cycle_tracker = cycle_tracker @@ -57,12 +57,12 @@ class CycleProcessor: # 存储reply action信息 person_info_manager = get_person_info_manager() - + # 获取 platform,如果不存在则从 chat_stream 获取,如果还是 None 则使用默认值 platform = action_message.get("chat_info_platform") if platform is None: platform = getattr(self.context.chat_stream, "platform", "unknown") - + person_id = person_info_manager.get_person_id( platform, action_message.get("user_id", ""), @@ -94,8 +94,8 @@ class CycleProcessor: } return loop_info, reply_text, cycle_timers - - async def observe(self,interest_value:float = 0.0) -> bool: + + async def observe(self, interest_value: float = 0.0) -> bool: """ 观察和处理单次思考循环的核心方法 @@ -114,7 +114,7 @@ class CycleProcessor: """ action_type = "no_action" reply_text = "" # 初始化reply_text变量,避免UnboundLocalError - + # 使用sigmoid函数将interest_value转换为概率 # 当interest_value为0时,概率接近0(使用Focus模式) # 当interest_value很高时,概率接近1(使用Normal模式) @@ -127,16 +127,24 @@ class CycleProcessor: k = 2.0 # 控制曲线陡峭程度 x0 = 1.0 # 控制曲线中心点 return 1.0 / (1.0 + math.exp(-k * (interest_val - x0))) - - normal_mode_probability = calculate_normal_mode_probability(interest_value) * 0.5 / global_config.chat.get_current_talk_frequency(self.context.stream_id) - + + normal_mode_probability = ( + calculate_normal_mode_probability(interest_value) + * 0.5 + / global_config.chat.get_current_talk_frequency(self.context.stream_id) + ) + # 根据概率决定使用哪种模式 if random.random() < normal_mode_probability: mode = ChatMode.NORMAL - logger.info(f"{self.log_prefix} 基于兴趣值 {interest_value:.2f},概率 {normal_mode_probability:.2f},选择Normal planner模式") + logger.info( + f"{self.log_prefix} 基于兴趣值 {interest_value:.2f},概率 {normal_mode_probability:.2f},选择Normal planner模式" + ) else: mode = ChatMode.FOCUS - logger.info(f"{self.log_prefix} 基于兴趣值 {interest_value:.2f},概率 {normal_mode_probability:.2f},选择Focus planner模式") + logger.info( + f"{self.log_prefix} 基于兴趣值 {interest_value:.2f},概率 {normal_mode_probability:.2f},选择Focus planner模式" + ) cycle_timers, thinking_id = self.cycle_tracker.start_cycle() logger.info(f"{self.log_prefix} 开始第{self.context.cycle_counter}次思考") @@ -165,12 +173,14 @@ class CycleProcessor: from src.plugin_system.core.event_manager import event_manager from src.plugin_system import EventType - result = await event_manager.trigger_event(EventType.ON_PLAN,plugin_name="SYSTEM", stream_id=self.context.chat_stream) + result = await event_manager.trigger_event( + EventType.ON_PLAN, plugin_name="SYSTEM", stream_id=self.context.chat_stream + ) if not result.all_continue_process(): raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于规划前中断了内容生成") - + with Timer("规划器", cycle_timers): - actions, _= await self.action_planner.plan( + actions, _ = await self.action_planner.plan( mode=mode, loop_start_time=loop_start_time, available_actions=available_actions, @@ -183,7 +193,7 @@ class CycleProcessor: # 直接处理no_reply逻辑,不再通过动作系统 reason = action_info.get("reasoning", "选择不回复") logger.info(f"{self.log_prefix} 选择不回复,原因: {reason}") - + # 存储no_reply信息到数据库 await database_api.store_action_info( chat_stream=self.context.chat_stream, @@ -194,13 +204,8 @@ class CycleProcessor: action_data={"reason": reason}, action_name="no_reply", ) - - return { - "action_type": "no_reply", - "success": True, - "reply_text": "", - "command": "" - } + + return {"action_type": "no_reply", "success": True, "reply_text": "", "command": ""} elif action_info["action_type"] != "reply": # 执行普通动作 with Timer("动作执行", cycle_timers): @@ -210,40 +215,32 @@ class CycleProcessor: action_info["action_data"], cycle_timers, thinking_id, - action_info["action_message"] + action_info["action_message"], ) return { "action_type": action_info["action_type"], "success": success, "reply_text": reply_text, - "command": command + "command": command, } else: try: success, response_set, _ = await generator_api.generate_reply( chat_stream=self.context.chat_stream, - reply_message = action_info["action_message"], + reply_message=action_info["action_message"], available_actions=available_actions, enable_tool=global_config.tool.enable_tool, request_type="chat.replyer", from_plugin=False, - ) + ) if not success or not response_set: - logger.info(f"对 {action_info['action_message'].get('processed_plain_text')} 的回复生成失败") - return { - "action_type": "reply", - "success": False, - "reply_text": "", - "loop_info": None - } + logger.info( + f"对 {action_info['action_message'].get('processed_plain_text')} 的回复生成失败" + ) + return {"action_type": "reply", "success": False, "reply_text": "", "loop_info": None} except asyncio.CancelledError: logger.debug(f"{self.log_prefix} 并行执行:回复生成任务已被取消") - return { - "action_type": "reply", - "success": False, - "reply_text": "", - "loop_info": None - } + return {"action_type": "reply", "success": False, "reply_text": "", "loop_info": None} loop_info, reply_text, cycle_timers_reply = await self._send_and_store_reply( response_set, @@ -253,12 +250,7 @@ class CycleProcessor: thinking_id, actions, ) - return { - "action_type": "reply", - "success": True, - "reply_text": reply_text, - "loop_info": loop_info - } + return {"action_type": "reply", "success": True, "reply_text": reply_text, "loop_info": loop_info} except Exception as e: logger.error(f"{self.log_prefix} 执行动作时出错: {e}") logger.error(f"{self.log_prefix} 错误信息: {traceback.format_exc()}") @@ -267,9 +259,9 @@ class CycleProcessor: "success": False, "reply_text": "", "loop_info": None, - "error": str(e) + "error": str(e), } - + # 创建所有动作的后台任务 action_tasks = [asyncio.create_task(execute_action(action)) for action in actions] @@ -282,12 +274,12 @@ class CycleProcessor: action_success = False action_reply_text = "" action_command = "" - + for i, result in enumerate(results): if isinstance(result, BaseException): logger.error(f"{self.log_prefix} 动作执行异常: {result}") continue - + action_info = actions[i] if result["action_type"] != "reply": action_success = result["success"] @@ -327,7 +319,7 @@ class CycleProcessor: }, } reply_text = action_reply_text - + if ENABLE_S4U: await stop_typing() @@ -342,7 +334,7 @@ class CycleProcessor: self.context.no_reply_consecutive = 0 logger.debug(f"{self.log_prefix} 执行了{action_type}动作,重置no_reply计数器") return True - + if action_type == "no_reply": self.context.no_reply_consecutive += 1 self.context.chat_instance._determine_form_type() diff --git a/src/chat/chat_loop/cycle_tracker.py b/src/chat/chat_loop/cycle_tracker.py index 2647bb7c6..9f276383b 100644 --- a/src/chat/chat_loop/cycle_tracker.py +++ b/src/chat/chat_loop/cycle_tracker.py @@ -91,25 +91,24 @@ class CycleTracker: # 获取动作类型,兼容新旧格式 action_type = "未知动作" - if hasattr(self, '_current_cycle_detail') and self._current_cycle_detail: + 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', {}) + action_result = loop_plan_info.get("action_result", {}) if isinstance(action_result, dict): # 旧格式:action_result是字典 - action_type = action_result.get('action_type', '未知动作') + 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', '未知动作') + 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', '未知动作') + action_type = loop_plan_info[0].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 logger.info( f"{self.context.log_prefix} 第{self.context.current_cycle_detail.cycle_id}次思考," f"耗时: {duration:.1f}秒, " - f"选择动作: {action_type}" - + (f"\n详情: {'; '.join(timer_strings)}" if timer_strings else "") + f"选择动作: {action_type}" + (f"\n详情: {'; '.join(timer_strings)}" if timer_strings else "") ) diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index f172cf993..130dec4e7 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -56,7 +56,7 @@ class HeartFChatting: self._loop_task: Optional[asyncio.Task] = None self._proactive_monitor_task: Optional[asyncio.Task] = None - + # 记录最近3次的兴趣度 self.recent_interest_records: deque = deque(maxlen=3) self._initialize_chat_mode() @@ -183,7 +183,7 @@ class HeartFChatting: event = ProactiveTriggerEvent( source="silence_monitor", reason=f"聊天已沉默 {formatted_time}", - metadata={"silence_duration": silence_duration} + metadata={"silence_duration": silence_duration}, ) await self.proactive_thinker.think(event) self.context.last_message_time = current_time @@ -205,21 +205,30 @@ class HeartFChatting: stream_parts = self.context.stream_id.split(":") current_chat_identifier = f"{stream_parts}:{stream_parts}" if len(stream_parts) >= 2 else self.context.stream_id - enable_list = getattr(global_config.chat, "proactive_thinking_enable_in_groups" if is_group_chat else "proactive_thinking_enable_in_private", []) + enable_list = getattr( + global_config.chat, + "proactive_thinking_enable_in_groups" if is_group_chat else "proactive_thinking_enable_in_private", + [], + ) return not enable_list or current_chat_identifier in enable_list def _get_dynamic_thinking_interval(self) -> float: try: from src.utils.timing_utils import get_normal_distributed_interval + base_interval = global_config.chat.proactive_thinking_interval delta_sigma = getattr(global_config.chat, "delta_sigma", 120) - if base_interval <= 0: base_interval = abs(base_interval) - if delta_sigma < 0: delta_sigma = abs(delta_sigma) + if base_interval <= 0: + base_interval = abs(base_interval) + if delta_sigma < 0: + delta_sigma = abs(delta_sigma) + + if base_interval == 0 and delta_sigma == 0: + return 300 + if delta_sigma == 0: + return base_interval - if base_interval == 0 and delta_sigma == 0: return 300 - if delta_sigma == 0: return base_interval - sigma_percentage = delta_sigma / base_interval if base_interval > 0 else delta_sigma / 1000 return get_normal_distributed_interval(base_interval, sigma_percentage, 1, 86400, use_3sigma_rule=True) @@ -335,29 +344,30 @@ class HeartFChatting: # 根据聊天模式处理新消息 # 统一使用 _should_process_messages 判断是否应该处理 - should_process,interest_value = await self._should_process_messages(recent_messages) + should_process, interest_value = await self._should_process_messages(recent_messages) if should_process: self.context.last_read_time = time.time() - await self.cycle_processor.observe(interest_value = interest_value) + await self.cycle_processor.observe(interest_value=interest_value) else: # Normal模式:消息数量不足,等待 await asyncio.sleep(0.5) return True - + if not await self._should_process_messages(recent_messages): return has_new_messages - + # 处理新消息 for message in recent_messages: - await self.cycle_processor.observe(interest_value = interest_value) - + await self.cycle_processor.observe(interest_value=interest_value) + # 如果成功观察,增加能量值并重置累积兴趣值 if has_new_messages: self.context.energy_value += 1 / global_config.chat.focus_value # 重置累积兴趣值,因为消息已经被成功处理 self.context.breaking_accumulated_interest = 0.0 - logger.info(f"{self.context.log_prefix} 能量值增加,当前能量值:{self.context.energy_value:.1f},重置累积兴趣值") - + logger.info( + f"{self.context.log_prefix} 能量值增加,当前能量值:{self.context.energy_value:.1f},重置累积兴趣值" + ) # 更新上一帧的睡眠状态 self.context.was_sleeping = is_sleeping @@ -378,7 +388,6 @@ class HeartFChatting: return has_new_messages - def _handle_wakeup_messages(self, messages): """ 处理休眠状态下的消息,累积唤醒度 @@ -421,7 +430,7 @@ class HeartFChatting: logger.info(f"{self.context.log_prefix} breaking模式已禁用,使用waiting形式") self.context.focus_energy = 1 return "waiting" - + # 如果连续no_reply次数少于3次,使用waiting形式 if self.context.no_reply_consecutive <= 3: self.context.focus_energy = 1 @@ -429,12 +438,14 @@ class HeartFChatting: else: # 使用累积兴趣值而不是最近3次的记录 total_interest = self.context.breaking_accumulated_interest - + # 计算调整后的阈值 adjusted_threshold = 1 / global_config.chat.get_current_talk_frequency(self.context.stream_id) - - logger.info(f"{self.context.log_prefix} 累积兴趣值: {total_interest:.2f}, 调整后阈值: {adjusted_threshold:.2f}") - + + logger.info( + f"{self.context.log_prefix} 累积兴趣值: {total_interest:.2f}, 调整后阈值: {adjusted_threshold:.2f}" + ) + # 如果累积兴趣值小于阈值,进入breaking形式 if total_interest < adjusted_threshold: logger.info(f"{self.context.log_prefix} 累积兴趣度不足,进入breaking形式") @@ -445,7 +456,7 @@ class HeartFChatting: self.context.focus_energy = 1 return "waiting" - async def _should_process_messages(self, new_message: List[Dict[str, Any]]) -> tuple[bool,float]: + async def _should_process_messages(self, new_message: List[Dict[str, Any]]) -> tuple[bool, float]: """ 统一判断是否应该处理消息的函数 根据当前循环模式和消息内容决定是否继续处理 @@ -459,37 +470,39 @@ class HeartFChatting: modified_exit_count_threshold = self.context.focus_energy * 0.5 / talk_frequency modified_exit_interest_threshold = 1.5 / talk_frequency - + # 计算当前批次消息的兴趣值 batch_interest = 0.0 for msg_dict in new_message: interest_value = msg_dict.get("interest_value", 0.0) if msg_dict.get("processed_plain_text", ""): batch_interest += interest_value - + # 在breaking形式下累积所有消息的兴趣值 if new_message_count > 0: self.context.breaking_accumulated_interest += batch_interest total_interest = self.context.breaking_accumulated_interest else: total_interest = self.context.breaking_accumulated_interest - + if new_message_count >= modified_exit_count_threshold: # 记录兴趣度到列表 self.recent_interest_records.append(total_interest) # 重置累积兴趣值,因为已经达到了消息数量阈值 self.context.breaking_accumulated_interest = 0.0 - + logger.info( f"{self.context.log_prefix} 累计消息数量达到{new_message_count}条(>{modified_exit_count_threshold:.1f}),结束等待,累积兴趣值: {total_interest:.2f}" ) - return True,total_interest/new_message_count + return True, total_interest / new_message_count # 检查累计兴趣值 if new_message_count > 0: # 只在兴趣值变化时输出log if not hasattr(self, "_last_accumulated_interest") or total_interest != self._last_accumulated_interest: - logger.info(f"{self.context.log_prefix} breaking形式当前累积兴趣值: {total_interest:.2f}, 专注度: {global_config.chat.focus_value:.1f}") + logger.info( + f"{self.context.log_prefix} breaking形式当前累积兴趣值: {total_interest:.2f}, 专注度: {global_config.chat.focus_value:.1f}" + ) self._last_accumulated_interest = total_interest if total_interest >= modified_exit_interest_threshold: # 记录兴趣度到列表 @@ -499,13 +512,16 @@ class HeartFChatting: logger.info( f"{self.context.log_prefix} 累计兴趣值达到{total_interest:.2f}(>{modified_exit_interest_threshold:.1f}),结束等待" ) - return True,total_interest/new_message_count - + return True, total_interest / new_message_count + # 每10秒输出一次等待状态 - if int(time.time() - self.context.last_read_time) > 0 and int(time.time() - self.context.last_read_time) % 10 == 0: + if ( + int(time.time() - self.context.last_read_time) > 0 + and int(time.time() - self.context.last_read_time) % 10 == 0 + ): logger.info( f"{self.context.log_prefix} 已等待{time.time() - self.context.last_read_time:.0f}秒,累计{new_message_count}条消息,累积兴趣{total_interest:.1f},继续等待..." ) await asyncio.sleep(0.5) - - return False,0.0 + + return False, 0.0 diff --git a/src/chat/chat_loop/hfc_context.py b/src/chat/chat_loop/hfc_context.py index 8fc7d0dd0..36fb492ca 100644 --- a/src/chat/chat_loop/hfc_context.py +++ b/src/chat/chat_loop/hfc_context.py @@ -44,13 +44,13 @@ class HfcContext: self.energy_value = self.chat_stream.energy_value self.sleep_pressure = self.chat_stream.sleep_pressure - self.was_sleeping = False # 用于检测睡眠状态的切换 - + self.was_sleeping = False # 用于检测睡眠状态的切换 + self.last_message_time = time.time() self.last_read_time = time.time() - 10 - + # 从聊天流恢复breaking累积兴趣值 - self.breaking_accumulated_interest = getattr(self.chat_stream, 'breaking_accumulated_interest', 0.0) + self.breaking_accumulated_interest = getattr(self.chat_stream, "breaking_accumulated_interest", 0.0) self.action_manager = ActionManager() @@ -79,4 +79,4 @@ class HfcContext: self.chat_stream.sleep_pressure = self.sleep_pressure self.chat_stream.focus_energy = self.focus_energy self.chat_stream.no_reply_consecutive = self.no_reply_consecutive - self.chat_stream.breaking_accumulated_interest = self.breaking_accumulated_interest \ No newline at end of file + self.chat_stream.breaking_accumulated_interest = self.breaking_accumulated_interest diff --git a/src/chat/chat_loop/hfc_utils.py b/src/chat/chat_loop/hfc_utils.py index eeaac70f5..4cc78a224 100644 --- a/src/chat/chat_loop/hfc_utils.py +++ b/src/chat/chat_loop/hfc_utils.py @@ -122,6 +122,7 @@ class CycleDetail: self.loop_plan_info = loop_info["loop_plan_info"] self.loop_action_info = loop_info["loop_action_info"] + async def send_typing(): """ 发送打字状态指示 diff --git a/src/chat/chat_loop/proactive/events.py b/src/chat/chat_loop/proactive/events.py index a81ece6c3..c273afef1 100644 --- a/src/chat/chat_loop/proactive/events.py +++ b/src/chat/chat_loop/proactive/events.py @@ -1,11 +1,13 @@ from dataclasses import dataclass, field from typing import Optional, Dict, Any + @dataclass class ProactiveTriggerEvent: """ 主动思考触发事件的数据类 """ + source: str # 触发源的标识,例如 "silence_monitor", "insomnia_manager" reason: str # 触发的具体原因,例如 "聊天已沉默10分钟", "深夜emo" - metadata: Optional[Dict[str, Any]] = field(default_factory=dict) # 可选的元数据,用于传递额外信息 \ No newline at end of file + metadata: Optional[Dict[str, Any]] = field(default_factory=dict) # 可选的元数据,用于传递额外信息 diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index 1df61cb4a..9cb2f45c6 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -37,8 +37,10 @@ class ProactiveThinker: Args: trigger_event: 描述触发上下文的事件对象 """ - logger.info(f"{self.context.log_prefix} 接收到主动思考事件: " - f"来源='{trigger_event.source}', 原因='{trigger_event.reason}'") + logger.info( + f"{self.context.log_prefix} 接收到主动思考事件: " + f"来源='{trigger_event.source}', 原因='{trigger_event.reason}'" + ) try: # 1. 根据事件类型执行前置操作 @@ -63,6 +65,7 @@ class ProactiveThinker: try: from src.mood.mood_manager import mood_manager + mood_obj = mood_manager.get_mood_by_chat_id(self.context.stream_id) new_mood = None @@ -76,8 +79,10 @@ class ProactiveThinker: if new_mood: mood_obj.mood_state = new_mood mood_obj.last_change_time = time.time() - logger.info(f"{self.context.log_prefix} 因 '{trigger_event.reason}'," - f"情绪状态被强制更新为: {mood_obj.mood_state}") + logger.info( + f"{self.context.log_prefix} 因 '{trigger_event.reason}'," + f"情绪状态被强制更新为: {mood_obj.mood_state}" + ) except Exception as e: logger.error(f"{self.context.log_prefix} 设置失眠情绪时出错: {e}") @@ -91,19 +96,17 @@ class ProactiveThinker: """ try: # 直接调用 planner 的 PROACTIVE 模式 - actions, target_message = await self.cycle_processor.action_planner.plan( - mode=ChatMode.PROACTIVE - ) - + actions, target_message = await self.cycle_processor.action_planner.plan(mode=ChatMode.PROACTIVE) + # 获取第一个规划出的动作作为主要决策 action_result = actions[0] if actions else {} # 如果决策不是 do_nothing,则执行 if action_result and action_result.get("action_type") != "do_nothing": - # 在主动思考时,如果 target_message 为 None,则默认选取最新 message 作为 target_message if target_message is None and self.context.chat_stream and self.context.chat_stream.context: from src.chat.message_receive.message import MessageRecv + latest_message = self.context.chat_stream.context.get_last_message() if isinstance(latest_message, MessageRecv): user_info = latest_message.message_info.user_info diff --git a/src/chat/chat_loop/response_handler.py b/src/chat/chat_loop/response_handler.py index 63d23ef62..cc9414e9b 100644 --- a/src/chat/chat_loop/response_handler.py +++ b/src/chat/chat_loop/response_handler.py @@ -157,7 +157,7 @@ class ResponseHandler: await send_api.text_to_stream( text=data, stream_id=self.context.stream_id, - reply_to_message = message_data, + reply_to_message=message_data, set_reply=need_reply, typing=False, ) diff --git a/src/chat/emoji_system/emoji_manager.py b/src/chat/emoji_system/emoji_manager.py index 071f57a4a..b25fd1ab8 100644 --- a/src/chat/emoji_system/emoji_manager.py +++ b/src/chat/emoji_system/emoji_manager.py @@ -724,7 +724,7 @@ class EmojiManager: if not emoji.is_deleted and emoji.hash == emoji_hash: return emoji return None # 如果循环结束还没找到,则返回 None - + async def get_emoji_tag_by_hash(self, emoji_hash: str) -> Optional[str]: """根据哈希值获取已注册表情包的描述 @@ -755,7 +755,7 @@ class EmojiManager: except Exception as e: logger.error(f"获取表情包描述失败 (Hash: {emoji_hash}): {str(e)}") return None - + async def get_emoji_description_by_hash(self, emoji_hash: str) -> Optional[str]: """根据哈希值获取已注册表情包的描述 diff --git a/src/chat/message_receive/chat_stream.py b/src/chat/message_receive/chat_stream.py index fe0fb90b2..c43901eab 100644 --- a/src/chat/message_receive/chat_stream.py +++ b/src/chat/message_receive/chat_stream.py @@ -259,7 +259,7 @@ class ChatManager: "user_cardname": model_instance.user_cardname or "", } group_info_data = None - if model_instance and getattr(model_instance, 'group_id', None): + if model_instance and getattr(model_instance, "group_id", None): group_info_data = { "platform": model_instance.group_platform, "group_id": model_instance.group_id, @@ -405,7 +405,7 @@ class ChatManager: "user_cardname": model_instance.user_cardname or "", } group_info_data = None - if model_instance and getattr(model_instance, 'group_id', None): + if model_instance and getattr(model_instance, "group_id", None): group_info_data = { "platform": model_instance.group_platform, "group_id": model_instance.group_id, diff --git a/src/chat/message_receive/message.py b/src/chat/message_receive/message.py index 930d1f5ea..1df006a1c 100644 --- a/src/chat/message_receive/message.py +++ b/src/chat/message_receive/message.py @@ -120,7 +120,7 @@ class MessageRecv(Message): self.priority_mode = "interest" self.priority_info = None self.interest_value: float = 0.0 - + self.key_words = [] self.key_words_lite = [] diff --git a/src/chat/message_receive/storage.py b/src/chat/message_receive/storage.py index 8f9502e2d..8219ee761 100644 --- a/src/chat/message_receive/storage.py +++ b/src/chat/message_receive/storage.py @@ -20,7 +20,7 @@ class MessageStorage: if isinstance(keywords, list): return orjson.dumps(keywords).decode("utf-8") return "[]" - + @staticmethod def _deserialize_keywords(keywords_str: str) -> list: """将JSON字符串反序列化为关键词列表""" diff --git a/src/chat/planner_actions/action_modifier.py b/src/chat/planner_actions/action_modifier.py index b68e639f5..705f723c8 100644 --- a/src/chat/planner_actions/action_modifier.py +++ b/src/chat/planner_actions/action_modifier.py @@ -161,10 +161,8 @@ class ActionModifier: available_actions = list(self.action_manager.get_using_actions().keys()) available_actions_text = "、".join(available_actions) if available_actions else "无" - - logger.info( - f"{self.log_prefix} 当前可用动作: {available_actions_text}||移除: {removals_summary}" - ) + + logger.info(f"{self.log_prefix} 当前可用动作: {available_actions_text}||移除: {removals_summary}") def _check_action_associated_types(self, all_actions: Dict[str, ActionInfo], chat_context: ChatMessageContext): type_mismatched_actions: List[Tuple[str, str]] = [] diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 291c19a66..e4597cba3 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -188,15 +188,12 @@ class ActionPlanner: param_text = "" if action_info.action_parameters: param_text = "\n" + "\n".join( - f' "{p_name}":"{p_desc}"' - for p_name, p_desc in action_info.action_parameters.items() + f' "{p_name}":"{p_desc}"' for p_name, p_desc in action_info.action_parameters.items() ) require_text = "\n".join(f"- {req}" for req in action_info.action_require) - using_action_prompt = await global_prompt_manager.get_prompt_async( - "action_prompt" - ) + using_action_prompt = await global_prompt_manager.get_prompt_async("action_prompt") action_options_block += using_action_prompt.format( action_name=action_name, action_description=action_info.description, @@ -205,9 +202,7 @@ class ActionPlanner: ) return action_options_block - def find_message_by_id( - self, message_id: str, message_id_list: list - ) -> Optional[Dict[str, Any]]: + def find_message_by_id(self, message_id: str, message_id_list: list) -> Optional[Dict[str, Any]]: # sourcery skip: use-next """ 根据message_id从message_id_list中查找对应的原始消息 @@ -245,7 +240,7 @@ class ActionPlanner: async def plan( self, mode: ChatMode = ChatMode.FOCUS, - loop_start_time:float = 0.0, + loop_start_time: float = 0.0, available_actions: Optional[Dict[str, ActionInfo]] = None, ) -> Tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]: """ @@ -323,11 +318,15 @@ class ActionPlanner: # 如果获取的target_message为None,输出warning并重新plan if target_message is None: self.plan_retry_count += 1 - logger.warning(f"{self.log_prefix}无法找到target_message_id '{target_message_id}' 对应的消息,重试次数: {self.plan_retry_count}/{self.max_plan_retries}") - + logger.warning( + f"{self.log_prefix}无法找到target_message_id '{target_message_id}' 对应的消息,重试次数: {self.plan_retry_count}/{self.max_plan_retries}" + ) + # 如果连续三次plan均为None,输出error并选取最新消息 if self.plan_retry_count >= self.max_plan_retries: - logger.error(f"{self.log_prefix}连续{self.max_plan_retries}次plan获取target_message失败,选择最新消息作为target_message") + logger.error( + f"{self.log_prefix}连续{self.max_plan_retries}次plan获取target_message失败,选择最新消息作为target_message" + ) target_message = self.get_latest_message(message_id_list) self.plan_retry_count = 0 # 重置计数器 else: @@ -338,8 +337,7 @@ class ActionPlanner: self.plan_retry_count = 0 else: logger.warning(f"{self.log_prefix}动作'{action}'缺少target_message_id") - - + if action != "no_reply" and action != "reply" and action not in current_available_actions: logger.warning( f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{action}' (可用: {list(current_available_actions.keys())}),将强制使用 'no_reply'" @@ -362,36 +360,35 @@ class ActionPlanner: is_parallel = False if mode == ChatMode.NORMAL and action in current_available_actions: is_parallel = current_available_actions[action].parallel_action - - + action_data["loop_start_time"] = loop_start_time - + actions = [] - + # 1. 添加Planner取得的动作 - actions.append({ - "action_type": action, - "reasoning": reasoning, - "action_data": action_data, - "action_message": target_message, - "available_actions": available_actions # 添加这个字段 - }) - - if action != "reply" and is_parallel: - actions.append({ - "action_type": "reply", + actions.append( + { + "action_type": action, + "reasoning": reasoning, + "action_data": action_data, "action_message": target_message, - "available_actions": available_actions - }) - - return actions,target_message + "available_actions": available_actions, # 添加这个字段 + } + ) + + if action != "reply" and is_parallel: + actions.append( + {"action_type": "reply", "action_message": target_message, "available_actions": available_actions} + ) + + return actions, target_message async def build_planner_prompt( self, is_group_chat: bool, # Now passed as argument chat_target_info: Optional[dict], # Now passed as argument current_available_actions: Dict[str, ActionInfo], - refresh_time :bool = False, + refresh_time: bool = False, mode: ChatMode = ChatMode.FOCUS, ) -> tuple[str, list]: # sourcery skip: use-join """构建 Planner LLM 的提示词 (获取模板并填充数据)""" @@ -400,21 +397,15 @@ class ActionPlanner: 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 "" + f",也有人叫你{','.join(global_config.bot.alias_names)}" if global_config.bot.alias_names else "" ) bot_core_personality = global_config.personality.personality_core - identity_block = ( - f"你的名字是{bot_name}{bot_nickname},你{bot_core_personality}:" - ) + identity_block = f"你的名字是{bot_name}{bot_nickname},你{bot_core_personality}:" schedule_block = "" if global_config.schedule.enable: if current_activity := schedule_manager.get_current_activity(): - schedule_block = ( - f"你当前正在:{current_activity},但注意它与群聊的聊天无关。" - ) + schedule_block = f"你当前正在:{current_activity},但注意它与群聊的聊天无关。" mood_block = "" if global_config.mood.enable_mood: @@ -424,13 +415,9 @@ class ActionPlanner: # --- 根据模式构建不同的Prompt --- if mode == ChatMode.PROACTIVE: long_term_memory_block = await self._get_long_term_memory_context() - action_options_text = await self._build_action_options( - current_available_actions, mode - ) + action_options_text = await self._build_action_options(current_available_actions, mode) - prompt_template = await global_prompt_manager.get_prompt_async( - "proactive_planner_prompt" - ) + prompt_template = await global_prompt_manager.get_prompt_async("proactive_planner_prompt") prompt = prompt_template.format( time_block=time_block, identity_block=identity_block, @@ -463,12 +450,8 @@ class ActionPlanner: limit=5, ) - actions_before_now_block = build_readable_actions( - actions=actions_before_now - ) - actions_before_now_block = ( - f"你刚刚选择并执行过的action是:\n{actions_before_now_block}" - ) + actions_before_now_block = build_readable_actions(actions=actions_before_now) + actions_before_now_block = f"你刚刚选择并执行过的action是:\n{actions_before_now_block}" if refresh_time: self.last_obs_time_mark = time.time() @@ -504,30 +487,22 @@ class ActionPlanner: }}""" chat_context_description = "你现在正在一个群聊中" - chat_target_name = None + chat_target_name = None 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_target_info.get("person_name") or chat_target_info.get("user_nickname") or "对方" ) chat_context_description = f"你正在和 {chat_target_name} 私聊" - action_options_block = await self._build_action_options( - current_available_actions, mode - ) + action_options_block = await self._build_action_options(current_available_actions, mode) moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" custom_prompt_block = "" if global_config.custom_prompt.planner_custom_prompt_content: - custom_prompt_block = ( - global_config.custom_prompt.planner_custom_prompt_content - ) + custom_prompt_block = global_config.custom_prompt.planner_custom_prompt_content - planner_prompt_template = await global_prompt_manager.get_prompt_async( - "planner_prompt" - ) + planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt") prompt = planner_prompt_template.format( schedule_block=schedule_block, mood_block=mood_block, @@ -555,9 +530,7 @@ class ActionPlanner: """ is_group_chat = True is_group_chat, chat_target_info = get_chat_type_and_target_info(self.chat_id) - logger.debug( - f"{self.log_prefix}获取到聊天信息 - 群聊: {is_group_chat}, 目标信息: {chat_target_info}" - ) + logger.debug(f"{self.log_prefix}获取到聊天信息 - 群聊: {is_group_chat}, 目标信息: {chat_target_info}") current_available_actions_dict = self.action_manager.get_using_actions() @@ -568,13 +541,9 @@ class ActionPlanner: current_available_actions = {} for action_name in current_available_actions_dict: if action_name in all_registered_actions: - current_available_actions[action_name] = all_registered_actions[ - action_name - ] + current_available_actions[action_name] = all_registered_actions[action_name] else: - logger.warning( - f"{self.log_prefix}使用中的动作 {action_name} 未在已注册动作中找到" - ) + logger.warning(f"{self.log_prefix}使用中的动作 {action_name} 未在已注册动作中找到") # 将no_reply作为系统级特殊动作添加到可用动作中 # no_reply虽然是系统级决策,但需要让规划器认为它是可用的 diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index be58c5426..35d667f12 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -706,16 +706,16 @@ class DefaultReplyer: # 检查最新五条消息中是否包含bot自己说的消息 latest_5_messages = core_dialogue_list[-5:] if len(core_dialogue_list) >= 5 else core_dialogue_list has_bot_message = any(str(msg.get("user_id")) == bot_id for msg in latest_5_messages) - + # logger.info(f"最新五条消息:{latest_5_messages}") # logger.info(f"最新五条消息中是否包含bot自己说的消息:{has_bot_message}") - + # 如果最新五条消息中不包含bot的消息,则返回空字符串 if not has_bot_message: core_dialogue_prompt = "" else: core_dialogue_list = core_dialogue_list[-int(global_config.chat.max_context_size * 2) :] # 限制消息数量 - + core_dialogue_prompt_str = build_readable_messages( core_dialogue_list, replace_bot_name=True, @@ -819,7 +819,7 @@ class DefaultReplyer: mood_prompt = "" if reply_to: - #兼容旧的reply_to + # 兼容旧的reply_to sender, target = self._parse_reply_target(reply_to) else: # 获取 platform,如果不存在则从 chat_stream 获取,如果还是 None 则使用默认值 @@ -830,7 +830,7 @@ class DefaultReplyer: ) person_name = await person_info_manager.get_value(person_id, "person_name") sender = person_name - target = reply_message.get('processed_plain_text') + target = reply_message.get("processed_plain_text") person_info_manager = get_person_info_manager() person_id = person_info_manager.get_person_id_by_person_name(sender) @@ -1024,7 +1024,7 @@ class DefaultReplyer: chat_stream = self.chat_stream chat_id = chat_stream.stream_id is_group_chat = bool(chat_stream.group_info) - + if reply_message: sender = reply_message.get("sender") target = reply_message.get("target") @@ -1181,7 +1181,9 @@ class DefaultReplyer: else: logger.debug(f"\n{prompt}\n") - content, (reasoning_content, model_name, tool_calls) = await self.express_model.generate_response_async(prompt) + content, (reasoning_content, model_name, tool_calls) = await self.express_model.generate_response_async( + prompt + ) logger.debug(f"replyer生成内容: {content}") return content, reasoning_content, model_name, tool_calls diff --git a/src/chat/utils/chat_message_builder.py b/src/chat/utils/chat_message_builder.py index fac26806c..e6843874f 100644 --- a/src/chat/utils/chat_message_builder.py +++ b/src/chat/utils/chat_message_builder.py @@ -1250,7 +1250,7 @@ async def get_person_id_list(messages: List[Dict[str, Any]]) -> List[str]: # 检查必要信息是否存在 且 不是机器人自己 if not all([platform, user_id]) or user_id == global_config.bot.qq_account: continue - + # 添加空值检查,防止 platform 为 None 时出错 if platform is None: platform = "unknown" diff --git a/src/chat/utils/prompt_utils.py b/src/chat/utils/prompt_utils.py index 4f9e36777..843990146 100644 --- a/src/chat/utils/prompt_utils.py +++ b/src/chat/utils/prompt_utils.py @@ -12,6 +12,7 @@ from src.config.config import global_config from src.chat.message_receive.chat_stream import get_chat_manager from src.person_info.person_info import get_person_info_manager from src.plugin_system.apis import cross_context_api + logger = get_logger("prompt_utils") @@ -80,29 +81,29 @@ class PromptUtils: @staticmethod async def build_cross_context( - chat_id: str, target_user_info: Optional[Dict[str, Any]], current_prompt_mode: str - ) -> str: - """ - 构建跨群聊上下文 - 统一实现,完全继承DefaultReplyer功能 - """ - if not global_config.cross_context.enable: - return "" - - other_chat_raw_ids = cross_context_api.get_context_groups(chat_id) - if not other_chat_raw_ids: - return "" - - chat_stream = get_chat_manager().get_stream(chat_id) - if not chat_stream: - return "" - - if current_prompt_mode == "normal": - return await cross_context_api.build_cross_context_normal(chat_stream, other_chat_raw_ids) - elif current_prompt_mode == "s4u": - return await cross_context_api.build_cross_context_s4u(chat_stream, other_chat_raw_ids, target_user_info) - + chat_id: str, target_user_info: Optional[Dict[str, Any]], current_prompt_mode: str + ) -> str: + """ + 构建跨群聊上下文 - 统一实现,完全继承DefaultReplyer功能 + """ + if not global_config.cross_context.enable: return "" + other_chat_raw_ids = cross_context_api.get_context_groups(chat_id) + if not other_chat_raw_ids: + return "" + + chat_stream = get_chat_manager().get_stream(chat_id) + if not chat_stream: + return "" + + if current_prompt_mode == "normal": + return await cross_context_api.build_cross_context_normal(chat_stream, other_chat_raw_ids) + elif current_prompt_mode == "s4u": + return await cross_context_api.build_cross_context_s4u(chat_stream, other_chat_raw_ids, target_user_info) + + return "" + @staticmethod def parse_reply_target_id(reply_to: str) -> str: """ diff --git a/src/chat/utils/smart_prompt.py b/src/chat/utils/smart_prompt.py index aba79f7ec..d46fdaac2 100644 --- a/src/chat/utils/smart_prompt.py +++ b/src/chat/utils/smart_prompt.py @@ -194,7 +194,7 @@ class SmartPromptBuilder: core_dialogue, background_dialogue = await self._build_s4u_chat_history_prompts( params.message_list_before_now_long, params.target_user_info.get("user_id") if params.target_user_info else "", - params.sender + params.sender, ) context_data["core_dialogue_prompt"] = core_dialogue @@ -245,16 +245,16 @@ class SmartPromptBuilder: # 检查最新五条消息中是否包含bot自己说的消息 latest_5_messages = core_dialogue_list[-5:] if len(core_dialogue_list) >= 5 else core_dialogue_list has_bot_message = any(str(msg.get("user_id")) == bot_id for msg in latest_5_messages) - + # logger.info(f"最新五条消息:{latest_5_messages}") # logger.info(f"最新五条消息中是否包含bot自己说的消息:{has_bot_message}") - + # 如果最新五条消息中不包含bot的消息,则返回空字符串 if not has_bot_message: core_dialogue_prompt = "" else: core_dialogue_list = core_dialogue_list[-int(global_config.chat.max_context_size * 2) :] # 限制消息数量 - + core_dialogue_prompt_str = build_readable_messages( core_dialogue_list, replace_bot_name=True, diff --git a/src/chat/utils/utils_image.py b/src/chat/utils/utils_image.py index aee19eeaf..ed1641d5f 100644 --- a/src/chat/utils/utils_image.py +++ b/src/chat/utils/utils_image.py @@ -27,16 +27,15 @@ logger = get_logger("chat_image") def is_image_message(message: Dict[str, Any]) -> bool: """ 判断消息是否为图片消息 - + Args: message: 消息字典 - + Returns: bool: 是否为图片消息 """ return message.get("type") == "image" or ( - isinstance(message.get("content"), dict) and - message["content"].get("type") == "image" + isinstance(message.get("content"), dict) and message["content"].get("type") == "image" ) @@ -596,7 +595,6 @@ class ImageManager: return "", "[图片]" - # 创建全局单例 image_manager = None diff --git a/src/common/database/monthly_plan_db.py b/src/common/database/monthly_plan_db.py index cfe6501fc..7d254fa9f 100644 --- a/src/common/database/monthly_plan_db.py +++ b/src/common/database/monthly_plan_db.py @@ -62,10 +62,12 @@ def get_active_plans_for_month(month: str) -> List[MonthlyPlan]: """ with get_db_session() as session: try: - plans = session.query(MonthlyPlan).filter( - MonthlyPlan.target_month == month, - MonthlyPlan.status == 'active' - ).order_by(MonthlyPlan.created_at.desc()).all() + plans = ( + session.query(MonthlyPlan) + .filter(MonthlyPlan.target_month == month, MonthlyPlan.status == "active") + .order_by(MonthlyPlan.created_at.desc()) + .all() + ) return plans except Exception as e: logger.error(f"查询 {month} 的有效月度计划时发生错误: {e}") diff --git a/src/config/config.py b/src/config/config.py index 232c48fb6..06cb6370e 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -81,8 +81,8 @@ def get_key_comment(toml_table, key): return item.trivia.comment if hasattr(toml_table, "keys"): for k in toml_table.keys(): - if isinstance(k, KeyType) and k.key == key: # type: ignore - return k.trivia.comment # type: ignore + if isinstance(k, KeyType) and k.key == key: # type: ignore + return k.trivia.comment # type: ignore return None diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 54a138d0b..7dbc0ce0e 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -259,7 +259,6 @@ class NormalChatConfig(ValidatedConfigBase): """普通聊天配置类""" - class ExpressionRule(ValidatedConfigBase): """表达学习规则""" @@ -653,7 +652,8 @@ class ContextGroup(ValidatedConfigBase): name: str = Field(..., description="共享组的名称") chat_ids: List[List[str]] = Field( - ..., description='属于该组的聊天ID列表,格式为 [["type", "chat_id"], ...],例如 [["group", "123456"], ["private", "789012"]]' + ..., + description='属于该组的聊天ID列表,格式为 [["type", "chat_id"], ...],例如 [["group", "123456"], ["private", "789012"]]', ) diff --git a/src/main.py b/src/main.py index 96d5b8322..e5869b3b7 100644 --- a/src/main.py +++ b/src/main.py @@ -28,36 +28,57 @@ from src.plugin_system.core.plugin_hot_reload import hot_reload_manager # 导入消息API和traceback模块 from src.common.message import get_global_api - + from src.chat.memory_system.Hippocampus import hippocampus_manager + if not global_config.memory.enable_memory: import src.chat.memory_system.Hippocampus as hippocampus_module - + class MockHippocampusManager: def initialize(self): pass + def get_hippocampus(self): return None + async def build_memory(self): pass + async def forget_memory(self, percentage: float = 0.005): pass + async def consolidate_memory(self): pass - async def get_memory_from_text(self, text: str, max_memory_num: int = 3, max_memory_length: int = 2, max_depth: int = 3, fast_retrieval: bool = False) -> list: + + async def get_memory_from_text( + self, + text: str, + max_memory_num: int = 3, + max_memory_length: int = 2, + max_depth: int = 3, + fast_retrieval: bool = False, + ) -> list: return [] - async def get_memory_from_topic(self, valid_keywords: list[str], max_memory_num: int = 3, max_memory_length: int = 2, max_depth: int = 3) -> list: + + async def get_memory_from_topic( + self, valid_keywords: list[str], max_memory_num: int = 3, max_memory_length: int = 2, max_depth: int = 3 + ) -> list: return [] - async def get_activate_from_text(self, text: str, max_depth: int = 3, fast_retrieval: bool = False) -> tuple[float, list[str]]: + + async def get_activate_from_text( + self, text: str, max_depth: int = 3, fast_retrieval: bool = False + ) -> tuple[float, list[str]]: return 0.0, [] + def get_memory_from_keyword(self, keyword: str, max_depth: int = 2) -> list: return [] + def get_all_node_names(self) -> list: return [] - + hippocampus_module.hippocampus_manager = MockHippocampusManager() - - # 插件系统现在使用统一的插件加载器 + +# 插件系统现在使用统一的插件加载器 install(extra_lines=3) @@ -67,7 +88,7 @@ logger = get_logger("main") class MainSystem: def __init__(self): self.hippocampus_manager = hippocampus_manager - + self.individuality: Individuality = get_individuality() # 使用消息API替代直接的FastAPI实例 @@ -207,7 +228,6 @@ MoFox_Bot(第三方修改版) get_emoji_manager().initialize() logger.info("表情包管理器初始化成功") - # 启动情绪管理器 await mood_manager.start() logger.info("情绪管理器初始化成功") @@ -222,11 +242,11 @@ MoFox_Bot(第三方修改版) # 初始化记忆系统 self.hippocampus_manager.initialize() logger.info("记忆系统初始化成功") - + # 初始化异步记忆管理器 try: from src.chat.memory_system.async_memory_optimizer import async_memory_manager - + await async_memory_manager.initialize() logger.info("记忆管理器初始化成功") except Exception as e: diff --git a/src/plugin_system/apis/cross_context_api.py b/src/plugin_system/apis/cross_context_api.py index 31a9f0e53..8dd4aaf97 100644 --- a/src/plugin_system/apis/cross_context_api.py +++ b/src/plugin_system/apis/cross_context_api.py @@ -36,27 +36,19 @@ def get_context_groups(chat_id: str) -> Optional[List[List[str]]]: # 检查当前聊天的ID和类型是否在组的chat_ids中 if [current_type, str(current_chat_raw_id)] in group.chat_ids: # 返回组内其他聊天的 [type, id] 列表 - return [ - chat_info - for chat_info in group.chat_ids - if chat_info != [current_type, str(current_chat_raw_id)] - ] + return [chat_info for chat_info in group.chat_ids if chat_info != [current_type, str(current_chat_raw_id)]] return None -async def build_cross_context_normal( - chat_stream: ChatStream, other_chat_infos: List[List[str]] -) -> str: +async def build_cross_context_normal(chat_stream: ChatStream, other_chat_infos: List[List[str]]) -> str: """ 构建跨群聊/私聊上下文 (Normal模式) """ cross_context_messages = [] for chat_type, chat_raw_id in other_chat_infos: is_group = chat_type == "group" - stream_id = get_chat_manager().get_stream_id( - chat_stream.platform, chat_raw_id, is_group=is_group - ) + stream_id = get_chat_manager().get_stream_id(chat_stream.platform, chat_raw_id, is_group=is_group) if not stream_id: continue @@ -68,9 +60,7 @@ async def build_cross_context_normal( ) if messages: chat_name = get_chat_manager().get_stream_name(stream_id) or chat_raw_id - formatted_messages, _ = build_readable_messages_with_id( - messages, timestamp_mode="relative" - ) + formatted_messages, _ = build_readable_messages_with_id(messages, timestamp_mode="relative") cross_context_messages.append(f'[以下是来自"{chat_name}"的近期消息]\n{formatted_messages}') except Exception as e: logger.error(f"获取聊天 {chat_raw_id} 的消息失败: {e}") @@ -97,9 +87,7 @@ async def build_cross_context_s4u( if user_id: for chat_type, chat_raw_id in other_chat_infos: is_group = chat_type == "group" - stream_id = get_chat_manager().get_stream_id( - chat_stream.platform, chat_raw_id, is_group=is_group - ) + stream_id = get_chat_manager().get_stream_id(chat_stream.platform, chat_raw_id, is_group=is_group) if not stream_id: continue @@ -114,9 +102,7 @@ async def build_cross_context_s4u( if user_messages: chat_name = get_chat_manager().get_stream_name(stream_id) or chat_raw_id user_name = ( - target_user_info.get("person_name") - or target_user_info.get("user_nickname") - or user_id + target_user_info.get("person_name") or target_user_info.get("user_nickname") or user_id ) formatted_messages, _ = build_readable_messages_with_id( user_messages, timestamp_mode="relative" @@ -182,9 +168,7 @@ async def get_chat_history_by_group_name(group_name: str) -> str: ) if messages: chat_name = get_chat_manager().get_stream_name(stream_id) or chat_raw_id - formatted_messages, _ = build_readable_messages_with_id( - messages, timestamp_mode="relative" - ) + formatted_messages, _ = build_readable_messages_with_id(messages, timestamp_mode="relative") cross_context_messages.append(f'[以下是来自"{chat_name}"的近期消息]\n{formatted_messages}') except Exception as e: logger.error(f"获取聊天 {chat_raw_id} 的消息失败: {e}") @@ -193,4 +177,4 @@ async def get_chat_history_by_group_name(group_name: str) -> str: if not cross_context_messages: return f"无法从互通组 {group_name} 中获取任何聊天记录。" - return "# 跨上下文参考\n" + "\n\n".join(cross_context_messages) + "\n" \ No newline at end of file + return "# 跨上下文参考\n" + "\n\n".join(cross_context_messages) + "\n" diff --git a/src/plugin_system/apis/generator_api.py b/src/plugin_system/apis/generator_api.py index ee60b72b6..2eaf8e4e4 100644 --- a/src/plugin_system/apis/generator_api.py +++ b/src/plugin_system/apis/generator_api.py @@ -107,9 +107,7 @@ async def generate_reply( """ try: # 获取回复器 - replyer = get_replyer( - chat_stream, chat_id, request_type=request_type - ) + replyer = get_replyer(chat_stream, chat_id, request_type=request_type) if not replyer: logger.error("[GeneratorAPI] 无法获取回复器") return False, [], None diff --git a/src/plugin_system/apis/send_api.py b/src/plugin_system/apis/send_api.py index 5e24cb1b0..0cfd3c301 100644 --- a/src/plugin_system/apis/send_api.py +++ b/src/plugin_system/apis/send_api.py @@ -51,6 +51,7 @@ logger = get_logger("send_api") # 适配器命令响应等待池 _adapter_response_pool: Dict[str, asyncio.Future] = {} + def message_dict_to_message_recv(message_dict: Dict[str, Any]) -> Optional[MessageRecv]: """查找要回复的消息 @@ -97,10 +98,11 @@ def message_dict_to_message_recv(message_dict: Dict[str, Any]) -> Optional[Messa } message_recv = MessageRecv(message_dict) - + logger.info(f"[SendAPI] 找到匹配的回复消息,发送者: {message_dict.get('user_nickname', '')}") return message_recv + def put_adapter_response(request_id: str, response_data: dict) -> None: """将适配器响应放入响应池""" if request_id in _adapter_response_pool: @@ -192,7 +194,7 @@ async def _send_to_target( anchor_message.update_chat_stream(target_stream) reply_to_platform_id = ( f"{anchor_message.message_info.platform}:{anchor_message.message_info.user_info.user_id}" - ) + ) else: anchor_message = None reply_to_platform_id = None @@ -234,7 +236,6 @@ async def _send_to_target( return False - # ============================================================================= # 公共API函数 - 预定义类型的发送函数 # ============================================================================= @@ -274,7 +275,9 @@ async def text_to_stream( ) -async def emoji_to_stream(emoji_base64: str, stream_id: str, storage_message: bool = True, set_reply: bool = False) -> bool: +async def emoji_to_stream( + emoji_base64: str, stream_id: str, storage_message: bool = True, set_reply: bool = False +) -> bool: """向指定流发送表情包 Args: @@ -285,10 +288,14 @@ async def emoji_to_stream(emoji_base64: str, stream_id: str, storage_message: bo Returns: bool: 是否发送成功 """ - return await _send_to_target("emoji", emoji_base64, stream_id, "", typing=False, storage_message=storage_message, set_reply=set_reply) + return await _send_to_target( + "emoji", emoji_base64, stream_id, "", typing=False, storage_message=storage_message, set_reply=set_reply + ) -async def image_to_stream(image_base64: str, stream_id: str, storage_message: bool = True, set_reply: bool = False) -> bool: +async def image_to_stream( + image_base64: str, stream_id: str, storage_message: bool = True, set_reply: bool = False +) -> bool: """向指定流发送图片 Args: @@ -299,11 +306,17 @@ async def image_to_stream(image_base64: str, stream_id: str, storage_message: bo Returns: bool: 是否发送成功 """ - return await _send_to_target("image", image_base64, stream_id, "", typing=False, storage_message=storage_message, set_reply=set_reply) + return await _send_to_target( + "image", image_base64, stream_id, "", typing=False, storage_message=storage_message, set_reply=set_reply + ) async def command_to_stream( - command: Union[str, dict], stream_id: str, storage_message: bool = True, display_message: str = "", set_reply: bool = False + command: Union[str, dict], + stream_id: str, + storage_message: bool = True, + display_message: str = "", + set_reply: bool = False, ) -> bool: """向指定流发送命令 diff --git a/src/plugin_system/core/event_manager.py b/src/plugin_system/core/event_manager.py index 8f70b259b..7f92b1632 100644 --- a/src/plugin_system/core/event_manager.py +++ b/src/plugin_system/core/event_manager.py @@ -68,7 +68,7 @@ class EventManager: event = BaseEvent(event_name, allowed_subscribers, allowed_triggers) self._events[event_name] = event logger.debug(f"事件 {event_name} 注册成功") - + # 检查是否有缓存的订阅需要处理 self._process_pending_subscriptions(event_name) diff --git a/src/plugins/built_in/maizone_refactored/plugin.py b/src/plugins/built_in/maizone_refactored/plugin.py index a82db7511..c54872872 100644 --- a/src/plugins/built_in/maizone_refactored/plugin.py +++ b/src/plugins/built_in/maizone_refactored/plugin.py @@ -53,7 +53,9 @@ class MaiZoneRefactoredPlugin(BasePlugin): "enable_reply": ConfigField(type=bool, default=True, description="完成后是否回复"), "ai_image_number": ConfigField(type=int, default=1, description="AI生成图片数量"), "image_number": ConfigField(type=int, default=1, description="本地配图数量(1-9张)"), - "image_directory": ConfigField(type=str, default=(Path(__file__).parent / "images").as_posix(), description="图片存储目录") + "image_directory": ConfigField( + type=str, default=(Path(__file__).parent / "images").as_posix(), description="图片存储目录" + ), }, "read": { "permission": ConfigField(type=list, default=[], description="阅读权限QQ号列表"), @@ -75,7 +77,9 @@ class MaiZoneRefactoredPlugin(BasePlugin): "forbidden_hours_end": ConfigField(type=int, default=6, description="禁止发送的结束小时(24小时制)"), }, "cookie": { - "http_fallback_host": ConfigField(type=str, default="127.0.0.1", description="备用Cookie获取服务的主机地址"), + "http_fallback_host": ConfigField( + type=str, default="127.0.0.1", description="备用Cookie获取服务的主机地址" + ), "http_fallback_port": ConfigField(type=int, default=9999, description="备用Cookie获取服务的端口"), "napcat_token": ConfigField(type=str, default="", description="Napcat服务的认证Token(可选)"), }, @@ -95,14 +99,14 @@ class MaiZoneRefactoredPlugin(BasePlugin): image_service = ImageService(self.get_config) cookie_service = CookieService(self.get_config) reply_tracker_service = ReplyTrackerService() - + # 使用已创建的 reply_tracker_service 实例 qzone_service = QZoneService( - self.get_config, - content_service, - image_service, + self.get_config, + content_service, + image_service, cookie_service, - reply_tracker_service # 传入已创建的实例 + reply_tracker_service, # 传入已创建的实例 ) scheduler_service = SchedulerService(self.get_config, qzone_service) monitor_service = MonitorService(self.get_config, qzone_service) diff --git a/src/plugins/built_in/maizone_refactored/services/qzone_service.py b/src/plugins/built_in/maizone_refactored/services/qzone_service.py index 6960dd5ab..2c7dddaec 100644 --- a/src/plugins/built_in/maizone_refactored/services/qzone_service.py +++ b/src/plugins/built_in/maizone_refactored/services/qzone_service.py @@ -272,8 +272,10 @@ class QZoneService: # 检查是否已经在持久化记录中标记为已回复 if not self.reply_tracker.has_replied(fid, comment_tid): # 记录日志以便追踪 - logger.debug(f"发现新评论需要回复 - 说说ID: {fid}, 评论ID: {comment_tid}, " - f"评论人: {comment.get('nickname', '')}, 内容: {comment.get('content', '')}") + logger.debug( + f"发现新评论需要回复 - 说说ID: {fid}, 评论ID: {comment_tid}, " + f"评论人: {comment.get('nickname', '')}, 内容: {comment.get('content', '')}" + ) comments_to_reply.append(comment) if not comments_to_reply: diff --git a/src/plugins/built_in/maizone_refactored/services/reply_tracker_service.py b/src/plugins/built_in/maizone_refactored/services/reply_tracker_service.py index 9df67d26a..0fa7edb99 100644 --- a/src/plugins/built_in/maizone_refactored/services/reply_tracker_service.py +++ b/src/plugins/built_in/maizone_refactored/services/reply_tracker_service.py @@ -41,7 +41,7 @@ class ReplyTrackerService: if not isinstance(data, dict): logger.error("加载的数据不是字典格式") return False - + for feed_id, comments in data.items(): if not isinstance(feed_id, str): logger.error(f"无效的说说ID格式: {feed_id}") @@ -70,12 +70,14 @@ class ReplyTrackerService: logger.warning("回复记录文件为空,将创建新的记录") self.replied_comments = {} return - + data = json.loads(file_content) if self._validate_data(data): self.replied_comments = data - logger.info(f"已加载 {len(self.replied_comments)} 条说说的回复记录," - f"总计 {sum(len(comments) for comments in self.replied_comments.values())} 条评论") + logger.info( + f"已加载 {len(self.replied_comments)} 条说说的回复记录," + f"总计 {sum(len(comments) for comments in self.replied_comments.values())} 条评论" + ) else: logger.error("加载的数据格式无效,将创建新的记录") self.replied_comments = {} @@ -112,12 +114,12 @@ class ReplyTrackerService: self._cleanup_old_records() # 创建临时文件 - temp_file = self.reply_record_file.with_suffix('.tmp') - + temp_file = self.reply_record_file.with_suffix(".tmp") + # 先写入临时文件 with open(temp_file, "w", encoding="utf-8") as f: json.dump(self.replied_comments, f, ensure_ascii=False, indent=2) - + # 如果写入成功,重命名为正式文件 if temp_file.stat().st_size > 0: # 确保写入成功 # 在Windows上,如果目标文件已存在,需要先删除它 @@ -128,7 +130,7 @@ class ReplyTrackerService: else: logger.error("临时文件写入失败,文件大小为0") temp_file.unlink() # 删除空的临时文件 - + except Exception as e: logger.error(f"保存回复记录失败: {e}", exc_info=True) # 尝试删除可能存在的临时文件 @@ -204,7 +206,7 @@ class ReplyTrackerService: # 确保将comment_id转换为字符串格式 comment_id_str = str(comment_id) - + if feed_id not in self.replied_comments: self.replied_comments[feed_id] = {} diff --git a/src/schedule/monthly_plan_manager.py b/src/schedule/monthly_plan_manager.py index 570cc00b1..bb966a48f 100644 --- a/src/schedule/monthly_plan_manager.py +++ b/src/schedule/monthly_plan_manager.py @@ -76,7 +76,7 @@ class MonthlyPlanManager: if len(plans) > max_plans: logger.warning(f"当前月度计划数量 ({len(plans)}) 超出上限 ({max_plans}),将自动删除多余的计划。") # 数据库查询结果已按创建时间降序排序(新的在前),直接截取超出上限的部分进行删除 - plans_to_delete = plans[:len(plans)-max_plans] + plans_to_delete = plans[: len(plans) - max_plans] delete_ids = [p.id for p in plans_to_delete] delete_plans_by_ids(delete_ids) # 重新获取计划列表 @@ -101,7 +101,7 @@ class MonthlyPlanManager: async def _generate_monthly_plans_logic(self, target_month: Optional[str] = None) -> bool: """ 生成指定月份的月度计划的核心逻辑 - + :param target_month: 目标月份,格式为 "YYYY-MM"。如果为 None,则为当前月份。 :return: 是否生成成功 """ @@ -291,6 +291,8 @@ class MonthlyPlanManager: except Exception as e: logger.error(f" 归档 {target_month} 月度计划时发生错误: {e}") + + class MonthlyPlanGenerationTask(AsyncTask): """每月初自动生成新月度计划的任务""" @@ -327,7 +329,7 @@ class MonthlyPlanGenerationTask(AsyncTask): current_month = next_month.strftime("%Y-%m") logger.info(f" 到达月初,开始生成 {current_month} 的月度计划...") await self.monthly_plan_manager._generate_monthly_plans_logic(current_month) - + except asyncio.CancelledError: logger.info(" 每月月度计划生成任务被取消。") break diff --git a/src/schedule/schedule_manager.py b/src/schedule/schedule_manager.py index e4457a3fc..5e661479b 100644 --- a/src/schedule/schedule_manager.py +++ b/src/schedule/schedule_manager.py @@ -165,14 +165,16 @@ class ScheduleManager: schedule_str = f"已成功加载今天的日程 ({today_str}):\n" if self.today_schedule: for item in self.today_schedule: - schedule_str += f" - {item.get('time_range', '未知时间')}: {item.get('activity', '未知活动')}\n" + schedule_str += ( + f" - {item.get('time_range', '未知时间')}: {item.get('activity', '未知活动')}\n" + ) logger.info(schedule_str) return # 成功加载,直接返回 else: logger.warning("数据库中的日程数据格式无效,将重新生成日程") else: logger.info(f"数据库中未找到今天的日程 ({today_str}),将调用 LLM 生成。") - + # 仅在需要时生成 await self.generate_and_save_schedule() From e1fbdaad8c8ef0ccb62efc0fb751c645f6da87ec Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 5 Sep 2025 21:01:32 +0800 Subject: [PATCH 09/69] rrrrrrrrrrrrrruuuuuuuuuuuuuuuuuuuuffffffffffffffffffffffffff --- plugins/napcat_adapter_plugin/plugin.py | 1 - .../napcat_adapter_plugin/src/recv_handler/message_sending.py | 3 +-- src/chat/chat_loop/cycle_processor.py | 1 - src/chat/chat_loop/energy_manager.py | 1 - src/chat/chat_loop/heartFC_chat.py | 3 +-- src/chat/chat_loop/hfc_context.py | 2 -- src/chat/chat_loop/hfc_utils.py | 2 -- src/chat/replyer/default_generator.py | 1 - src/chat/replyer/replyer_manager.py | 3 +-- src/chat/utils/prompt_utils.py | 1 - src/chat/utils/utils_image.py | 1 - src/plugin_system/apis/generator_api.py | 1 - src/plugin_system/apis/send_api.py | 3 --- src/plugin_system/core/plugin_manager.py | 2 +- 14 files changed, 4 insertions(+), 21 deletions(-) diff --git a/plugins/napcat_adapter_plugin/plugin.py b/plugins/napcat_adapter_plugin/plugin.py index a87eeb437..48ae8603d 100644 --- a/plugins/napcat_adapter_plugin/plugin.py +++ b/plugins/napcat_adapter_plugin/plugin.py @@ -7,7 +7,6 @@ from . import event_types, CONSTS, event_handlers from typing import List from src.plugin_system import BasePlugin, BaseEventHandler, register_plugin, EventType, ConfigField -from src.plugin_system.base.base_event import HandlerResult from src.plugin_system.core.event_manager import event_manager from src.common.logger import get_logger diff --git a/plugins/napcat_adapter_plugin/src/recv_handler/message_sending.py b/plugins/napcat_adapter_plugin/src/recv_handler/message_sending.py index ff9b8def1..653fe5444 100644 --- a/plugins/napcat_adapter_plugin/src/recv_handler/message_sending.py +++ b/plugins/napcat_adapter_plugin/src/recv_handler/message_sending.py @@ -1,4 +1,3 @@ -import json import asyncio from src.common.logger import get_logger @@ -30,7 +29,7 @@ class MessageSending: message_dict = message_base.to_dict() if chunker.should_chunk_message(message_dict): - logger.info(f"消息过大,进行切片发送到 MaiBot") + logger.info("消息过大,进行切片发送到 MaiBot") # 切片消息 chunks = chunker.chunk_message(message_dict) diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index 975b6134b..acbdf474e 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -5,7 +5,6 @@ import math import random from typing import Optional, Dict, Any, Tuple -from src.chat.message_receive.chat_stream import get_chat_manager from src.chat.utils.timer_calculator import Timer from src.common.logger import get_logger from src.config.config import global_config diff --git a/src/chat/chat_loop/energy_manager.py b/src/chat/chat_loop/energy_manager.py index 9358664a0..cc3cf8d0e 100644 --- a/src/chat/chat_loop/energy_manager.py +++ b/src/chat/chat_loop/energy_manager.py @@ -3,7 +3,6 @@ import time from typing import Optional from src.common.logger import get_logger from src.config.config import global_config -from src.plugin_system.base.component_types import ChatMode from .hfc_context import HfcContext from src.schedule.schedule_manager import schedule_manager diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index 130dec4e7..caa13affe 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -2,14 +2,13 @@ import asyncio import time import traceback import random -from typing import Optional, List, Dict, Any, Tuple +from typing import Optional, List, Dict, Any from collections import deque from src.common.logger import get_logger from src.config.config import global_config from src.person_info.relationship_builder_manager import relationship_builder_manager from src.chat.express.expression_learner import expression_learner_manager -from src.plugin_system.base.component_types import ChatMode from src.schedule.schedule_manager import schedule_manager, SleepState from src.plugin_system.apis import message_api diff --git a/src/chat/chat_loop/hfc_context.py b/src/chat/chat_loop/hfc_context.py index 36fb492ca..9e7c73722 100644 --- a/src/chat/chat_loop/hfc_context.py +++ b/src/chat/chat_loop/hfc_context.py @@ -1,10 +1,8 @@ from typing import List, Optional, TYPE_CHECKING import time from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager -from src.common.logger import get_logger from src.person_info.relationship_builder_manager import RelationshipBuilder from src.chat.express.expression_learner import ExpressionLearner -from src.plugin_system.base.component_types import ChatMode from src.chat.planner_actions.action_manager import ActionManager from src.chat.chat_loop.hfc_utils import CycleDetail diff --git a/src/chat/chat_loop/hfc_utils.py b/src/chat/chat_loop/hfc_utils.py index 4cc78a224..ae77b2378 100644 --- a/src/chat/chat_loop/hfc_utils.py +++ b/src/chat/chat_loop/hfc_utils.py @@ -1,13 +1,11 @@ import time from typing import Optional, Dict, Any, Union -from src.config.config import global_config from src.common.logger import get_logger from src.chat.message_receive.chat_stream import get_chat_manager from src.plugin_system.apis import send_api from maim_message.message_base import GroupInfo -from src.common.message_repository import count_messages logger = get_logger("hfc") diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 35d667f12..20978f165 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -15,7 +15,6 @@ from src.chat.utils.prompt_utils import PromptUtils from src.mais4u.mai_think import mai_thinking_manager from src.common.logger import get_logger from src.config.config import global_config, model_config -from src.config.api_ada_configs import TaskConfig from src.individuality.individuality import get_individuality from src.llm_models.utils_model import LLMRequest from src.chat.message_receive.message import UserInfo, Seg, MessageRecv, MessageSending diff --git a/src/chat/replyer/replyer_manager.py b/src/chat/replyer/replyer_manager.py index 2613e49a1..2f64ab07f 100644 --- a/src/chat/replyer/replyer_manager.py +++ b/src/chat/replyer/replyer_manager.py @@ -1,7 +1,6 @@ -from typing import Dict, Optional, List, Tuple +from typing import Dict, Optional from src.common.logger import get_logger -from src.config.api_ada_configs import TaskConfig from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager from src.chat.replyer.default_generator import DefaultReplyer diff --git a/src/chat/utils/prompt_utils.py b/src/chat/utils/prompt_utils.py index 843990146..e6507c86c 100644 --- a/src/chat/utils/prompt_utils.py +++ b/src/chat/utils/prompt_utils.py @@ -4,7 +4,6 @@ """ import re -import time from typing import Dict, Any, Optional, Tuple from src.common.logger import get_logger diff --git a/src/chat/utils/utils_image.py b/src/chat/utils/utils_image.py index ed1641d5f..847d48fac 100644 --- a/src/chat/utils/utils_image.py +++ b/src/chat/utils/utils_image.py @@ -4,7 +4,6 @@ import time import hashlib import uuid import io -import asyncio import numpy as np from typing import Optional, Tuple, Dict, Any diff --git a/src/plugin_system/apis/generator_api.py b/src/plugin_system/apis/generator_api.py index 2eaf8e4e4..b20909e77 100644 --- a/src/plugin_system/apis/generator_api.py +++ b/src/plugin_system/apis/generator_api.py @@ -12,7 +12,6 @@ import traceback from typing import Tuple, Any, Dict, List, Optional from rich.traceback import install from src.common.logger import get_logger -from src.config.api_ada_configs import TaskConfig from src.chat.replyer.default_generator import DefaultReplyer from src.chat.message_receive.chat_stream import ChatStream from src.chat.utils.utils import process_llm_response diff --git a/src/plugin_system/apis/send_api.py b/src/plugin_system/apis/send_api.py index 0cfd3c301..7a4d371a2 100644 --- a/src/plugin_system/apis/send_api.py +++ b/src/plugin_system/apis/send_api.py @@ -30,7 +30,6 @@ import traceback import time -import difflib import asyncio from typing import Optional, Union, Dict, Any from src.common.logger import get_logger @@ -41,8 +40,6 @@ from maim_message import UserInfo from src.chat.message_receive.chat_stream import ChatStream from src.chat.message_receive.uni_message_sender import HeartFCSender from src.chat.message_receive.message import MessageSending, MessageRecv -from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, replace_user_references_async -from src.person_info.person_info import get_person_info_manager from maim_message import Seg from src.config.config import global_config diff --git a/src/plugin_system/core/plugin_manager.py b/src/plugin_system/core/plugin_manager.py index 2794e2fc5..07d33b773 100644 --- a/src/plugin_system/core/plugin_manager.py +++ b/src/plugin_system/core/plugin_manager.py @@ -200,7 +200,7 @@ class PluginManager: # 检查并调用 on_plugin_loaded 钩子(如果存在) if hasattr(plugin_instance, "on_plugin_loaded") and callable( - getattr(plugin_instance, "on_plugin_loaded") + plugin_instance.on_plugin_loaded ): logger.debug(f"为插件 '{plugin_name}' 调用 on_plugin_loaded 钩子") try: From 4b256721d34c27315545d98bfa08146831851e19 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 5 Sep 2025 21:40:42 +0800 Subject: [PATCH 10/69] =?UTF-8?q?refactor(chat):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E5=BE=AA=E7=8E=AF=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E7=AE=80=E5=8C=96CycleProcessor=E8=81=8C=E8=B4=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将`no_reply`计数器管理、睡眠压力增加等逻辑从`CycleProcessor`上移至`HeartFChatting`主循环中,使其职责更清晰。`CycleProcessor.observe`现在直接返回执行的动作类型,简化了其内部状态管理。 主要变更: - `CycleProcessor`不再处理回复生成、并行任务和最终循环信息的构建,这些复杂的逻辑被移除,极大地简化了该类。 - `HeartFChatting`现在负责根据`observe`返回的动作类型来管理`no_reply`计数器和睡眠压力。 - 删除了`CycleProcessor.execute_plan`方法,主动思考的回复流程被重构,直接调用`generator_api`和`response_handler.send_response`。 - 移除了`response_handler`中已废弃的`generate_response`方法。 --- src/chat/chat_loop/cycle_processor.py | 267 +----------------- src/chat/chat_loop/heartFC_chat.py | 43 +-- .../chat_loop/proactive/proactive_thinker.py | 31 +- src/chat/chat_loop/response_handler.py | 98 ------- 4 files changed, 42 insertions(+), 397 deletions(-) diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index acbdf474e..b446c697a 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -94,7 +94,7 @@ class CycleProcessor: return loop_info, reply_text, cycle_timers - async def observe(self, interest_value: float = 0.0) -> bool: + async def observe(self, interest_value: float = 0.0) -> str: """ 观察和处理单次思考循环的核心方法 @@ -326,226 +326,7 @@ class CycleProcessor: self.context.chat_instance.cycle_tracker.print_cycle_info(cycle_timers) action_type = actions[0]["action_type"] if actions else "no_action" - # 管理no_reply计数器:当执行了非no_reply动作时,重置计数器 - if action_type != "no_reply": - # no_reply逻辑已集成到heartFC_chat.py中,直接重置计数器 - self.context.chat_instance.recent_interest_records.clear() - self.context.no_reply_consecutive = 0 - logger.debug(f"{self.log_prefix} 执行了{action_type}动作,重置no_reply计数器") - return True - - if action_type == "no_reply": - self.context.no_reply_consecutive += 1 - self.context.chat_instance._determine_form_type() - - # 在一轮动作执行完毕后,增加睡眠压力 - if self.context.energy_manager and global_config.sleep_system.enable_insomnia_system: - if action_type not in ["no_reply", "no_action"]: - self.context.energy_manager.increase_sleep_pressure() - - return True - - async def execute_plan(self, action_result: Dict[str, Any], target_message: Optional[Dict[str, Any]]): - """ - 执行一个已经制定好的计划 - """ - action_type = action_result.get("action_type", "error") - - # 这里我们需要为执行计划创建一个新的循环追踪 - cycle_timers, thinking_id = self.cycle_tracker.start_cycle(is_proactive=True) - loop_start_time = time.time() - - if action_type == "reply": - # 主动思考不应该直接触发简单回复,但为了逻辑完整性,我们假设它会调用response_handler - # 注意:这里的 available_actions 和 plan_result 是缺失的,需要根据实际情况处理 - await self._handle_reply_action( - target_message, {}, None, loop_start_time, cycle_timers, thinking_id, {"action_result": action_result} - ) - else: - await self._handle_other_actions( - action_type, - action_result.get("reasoning", ""), - action_result.get("action_data", {}), - action_result.get("is_parallel", False), - None, - target_message, - cycle_timers, - thinking_id, - {"action_result": action_result}, - loop_start_time, - ) - - async def _handle_reply_action( - self, message_data, available_actions, gen_task, loop_start_time, cycle_timers, thinking_id, plan_result - ): - """ - 处理回复类型的动作 - - Args: - message_data: 消息数据 - available_actions: 可用动作列表 - gen_task: 预先创建的生成任务(可能为None) - loop_start_time: 循环开始时间 - cycle_timers: 循环计时器 - thinking_id: 思考ID - plan_result: 规划结果 - - 功能说明: - - 根据聊天模式决定是否使用预生成的回复或实时生成 - - 在NORMAL模式下使用异步生成提高效率 - - 在FOCUS模式下同步生成确保及时响应 - - 发送生成的回复并结束循环 - """ - # 初始化reply_to_str以避免UnboundLocalError - reply_to_str = None - - if self.context.loop_mode == ChatMode.NORMAL: - if not gen_task: - reply_to_str = await self._build_reply_to_str(message_data) - gen_task = asyncio.create_task( - self.response_handler.generate_response( - message_data=message_data, - available_actions=available_actions, - reply_to=reply_to_str, - request_type="chat.replyer.normal", - ) - ) - else: - # 如果gen_task已存在但reply_to_str还未构建,需要构建它 - if reply_to_str is None: - reply_to_str = await self._build_reply_to_str(message_data) - - try: - response_set = await asyncio.wait_for(gen_task, timeout=global_config.chat.thinking_timeout) - except asyncio.TimeoutError: - response_set = None - else: - reply_to_str = await self._build_reply_to_str(message_data) - response_set = await self.response_handler.generate_response( - message_data=message_data, - available_actions=available_actions, - reply_to=reply_to_str, - request_type="chat.replyer.focus", - ) - - if response_set: - loop_info, _, _ = await self.response_handler.generate_and_send_reply( - response_set, reply_to_str, loop_start_time, message_data, cycle_timers, thinking_id, plan_result - ) - self.cycle_tracker.end_cycle(loop_info, cycle_timers) - - async def _handle_other_actions( - self, - action_type, - reasoning, - action_data, - is_parallel, - gen_task, - action_message, - cycle_timers, - thinking_id, - plan_result, - loop_start_time, - ): - """ - 处理非回复类型的动作(如no_reply、自定义动作等) - - Args: - action_type: 动作类型 - reasoning: 动作理由 - action_data: 动作数据 - is_parallel: 是否并行执行 - gen_task: 生成任务 - action_message: 动作消息 - cycle_timers: 循环计时器 - thinking_id: 思考ID - plan_result: 规划结果 - loop_start_time: 循环开始时间 - - 功能说明: - - 在NORMAL模式下可能并行执行回复生成和动作处理 - - 等待所有异步任务完成 - - 整合回复和动作的执行结果 - - 构建最终循环信息并结束循环 - """ - background_reply_task = None - if self.context.loop_mode == ChatMode.NORMAL and is_parallel and gen_task: - background_reply_task = asyncio.create_task( - self._handle_parallel_reply( - gen_task, loop_start_time, action_message, cycle_timers, thinking_id, plan_result - ) - ) - - background_action_task = asyncio.create_task( - self._handle_action(action_type, reasoning, action_data, cycle_timers, thinking_id, action_message) - ) - - reply_loop_info, action_success, action_reply_text, action_command = None, False, "", "" - - if background_reply_task: - results = await asyncio.gather(background_reply_task, background_action_task, return_exceptions=True) - reply_result, action_result_val = results - if not isinstance(reply_result, BaseException) and reply_result is not None: - reply_loop_info, _, _ = reply_result - else: - reply_loop_info = None - - if not isinstance(action_result_val, BaseException) and action_result_val is not None: - action_success, action_reply_text, action_command = action_result_val - else: - action_success, action_reply_text, action_command = False, "", "" - else: - results = await asyncio.gather(background_action_task, return_exceptions=True) - if results and len(results) > 0: - action_result_val = results[0] # Get the actual result from the tuple - else: - action_result_val = (False, "", "") - - if not isinstance(action_result_val, BaseException) and action_result_val is not None: - action_success, action_reply_text, action_command = action_result_val - else: - action_success, action_reply_text, action_command = False, "", "" - - loop_info = self._build_final_loop_info( - reply_loop_info, action_success, action_reply_text, action_command, plan_result - ) - self.cycle_tracker.end_cycle(loop_info, cycle_timers) - - async def _handle_parallel_reply( - self, gen_task, loop_start_time, action_message, cycle_timers, thinking_id, plan_result - ): - """ - 处理并行回复生成 - - Args: - gen_task: 回复生成任务 - loop_start_time: 循环开始时间 - action_message: 动作消息 - cycle_timers: 循环计时器 - thinking_id: 思考ID - plan_result: 规划结果 - - Returns: - tuple: (循环信息, 回复文本, 计时器信息) 或 None - - 功能说明: - - 等待并行回复生成任务完成(带超时) - - 构建回复目标字符串 - - 发送生成的回复 - - 返回循环信息供上级方法使用 - """ - try: - response_set = await asyncio.wait_for(gen_task, timeout=global_config.chat.thinking_timeout) - except asyncio.TimeoutError: - return None, "", {} - - if not response_set: - return None, "", {} - - reply_to_str = await self._build_reply_to_str(action_message) - return await self.response_handler.generate_and_send_reply( - response_set, reply_to_str, loop_start_time, action_message, cycle_timers, thinking_id, plan_result - ) + return action_type async def _handle_action( self, action, reasoning, action_data, cycle_timers, thinking_id, action_message @@ -594,12 +375,12 @@ class CycleProcessor: if "reply" in available_actions: fallback_action = "reply" elif available_actions: - fallback_action = list(available_actions.keys())[0] + fallback_action = list(available_actions.keys()) if fallback_action and fallback_action != action: logger.info(f"{self.context.log_prefix} 使用回退动作: {fallback_action}") action_handler = self.context.action_manager.create_action( - action_name=fallback_action, + action_name=fallback_action if isinstance(fallback_action, list) else fallback_action, action_data=action_data, reasoning=f"原动作'{action}'不可用,自动回退。{reasoning}", cycle_timers=cycle_timers, @@ -619,43 +400,3 @@ class CycleProcessor: logger.error(f"{self.context.log_prefix} 处理{action}时出错: {e}") traceback.print_exc() return False, "", "" - - def _build_final_loop_info(self, reply_loop_info, action_success, action_reply_text, action_command, plan_result): - """ - 构建最终的循环信息 - - Args: - reply_loop_info: 回复循环信息(可能为None) - action_success: 动作执行是否成功 - action_reply_text: 动作回复文本 - action_command: 动作命令 - plan_result: 规划结果 - - Returns: - dict: 完整的循环信息,包含规划信息和动作信息 - - 功能说明: - - 如果有回复循环信息,则在其基础上添加动作信息 - - 如果没有回复信息,则创建新的循环信息结构 - - 整合所有执行结果供循环跟踪器记录 - """ - if reply_loop_info: - loop_info = reply_loop_info - loop_info["loop_action_info"].update( - { - "action_taken": action_success, - "command": action_command, - "taken_time": time.time(), - } - ) - else: - loop_info = { - "loop_plan_info": {"action_result": plan_result.get("action_result", {})}, - "loop_action_info": { - "action_taken": action_success, - "reply_text": action_reply_text, - "command": action_command, - "taken_time": time.time(), - }, - } - return loop_info diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index caa13affe..80a7aff76 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -342,31 +342,36 @@ class HeartFChatting: logger.info(f"{self.context.log_prefix} 从睡眠中被唤醒,将处理积压的消息。") # 根据聊天模式处理新消息 - # 统一使用 _should_process_messages 判断是否应该处理 should_process, interest_value = await self._should_process_messages(recent_messages) - if should_process: - self.context.last_read_time = time.time() - await self.cycle_processor.observe(interest_value=interest_value) - else: - # Normal模式:消息数量不足,等待 + if not should_process: + # 消息数量不足或兴趣不够,等待 await asyncio.sleep(0.5) - return True + return True # Skip rest of the logic for this iteration - if not await self._should_process_messages(recent_messages): - return has_new_messages + # Messages should be processed + action_type = await self.cycle_processor.observe(interest_value=interest_value) - # 处理新消息 - for message in recent_messages: - await self.cycle_processor.observe(interest_value=interest_value) + # 管理no_reply计数器 + if action_type != "no_reply": + self.recent_interest_records.clear() + self.context.no_reply_consecutive = 0 + logger.debug(f"{self.context.log_prefix} 执行了{action_type}动作,重置no_reply计数器") + else: # action_type == "no_reply" + self.context.no_reply_consecutive += 1 + self._determine_form_type() + + # 在一轮动作执行完毕后,增加睡眠压力 + if self.context.energy_manager and global_config.sleep_system.enable_insomnia_system: + if action_type not in ["no_reply", "no_action"]: + self.context.energy_manager.increase_sleep_pressure() # 如果成功观察,增加能量值并重置累积兴趣值 - if has_new_messages: - self.context.energy_value += 1 / global_config.chat.focus_value - # 重置累积兴趣值,因为消息已经被成功处理 - self.context.breaking_accumulated_interest = 0.0 - logger.info( - f"{self.context.log_prefix} 能量值增加,当前能量值:{self.context.energy_value:.1f},重置累积兴趣值" - ) + self.context.energy_value += 1 / global_config.chat.focus_value + # 重置累积兴趣值,因为消息已经被成功处理 + self.context.breaking_accumulated_interest = 0.0 + logger.info( + f"{self.context.log_prefix} 能量值增加,当前能量值:{self.context.energy_value:.1f},重置累积兴趣值" + ) # 更新上一帧的睡眠状态 self.context.was_sleeping = is_sleeping diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index 9cb2f45c6..e2be9fdc2 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -6,6 +6,7 @@ from src.common.logger import get_logger from src.plugin_system.base.component_types import ChatMode from ..hfc_context import HfcContext from .events import ProactiveTriggerEvent +from src.plugin_system.apis import generator_api if TYPE_CHECKING: from ..cycle_processor import CycleProcessor @@ -103,23 +104,19 @@ class ProactiveThinker: # 如果决策不是 do_nothing,则执行 if action_result and action_result.get("action_type") != "do_nothing": - # 在主动思考时,如果 target_message 为 None,则默认选取最新 message 作为 target_message - if target_message is None and self.context.chat_stream and self.context.chat_stream.context: - from src.chat.message_receive.message import MessageRecv - - latest_message = self.context.chat_stream.context.get_last_message() - if isinstance(latest_message, MessageRecv): - user_info = latest_message.message_info.user_info - target_message = { - "chat_info_platform": latest_message.message_info.platform, - "user_platform": user_info.platform if user_info else None, - "user_id": user_info.user_id if user_info else None, - "processed_plain_text": latest_message.processed_plain_text, - "is_mentioned": latest_message.is_mentioned, - } - - # 将决策结果交给 cycle_processor 的后续流程处理 - await self.cycle_processor.execute_plan(action_result, target_message) + if action_result.get("action_type") == "reply": + success, response_set, _ = await generator_api.generate_reply( + chat_stream=self.context.chat_stream, + reply_message=action_result["action_message"], + available_actions={}, + enable_tool=False, + request_type="chat.replyer.proactive", + from_plugin=False, + ) + if success and response_set: + await self.cycle_processor.response_handler.send_response( + response_set, time.time(), action_result["action_message"] + ) else: logger.info(f"{self.context.log_prefix} 主动思考决策: 保持沉默") diff --git a/src/chat/chat_loop/response_handler.py b/src/chat/chat_loop/response_handler.py index cc9414e9b..7a5acfc53 100644 --- a/src/chat/chat_loop/response_handler.py +++ b/src/chat/chat_loop/response_handler.py @@ -172,101 +172,3 @@ class ResponseHandler: ) return reply_text - - # TODO: 已废弃 - async def generate_response( - self, - message_data: dict, - available_actions: Optional[Dict[str, Any]], - reply_to: str, - request_type: str = "chat.replyer.normal", - ) -> Optional[list]: - """ - 生成回复内容 - - Args: - message_data: 消息数据 - available_actions: 可用动作列表 - reply_to: 回复目标 - request_type: 请求类型,默认为普通回复 - - Returns: - list: 生成的回复内容列表,失败时返回None - - 功能说明: - - 在生成回复前进行反注入检测(提高效率) - - 调用生成器API生成回复 - - 根据配置启用或禁用工具功能 - - 处理生成失败的情况 - - 记录生成过程中的错误和异常 - """ - try: - # === 反注入检测(仅在需要生成回复时) === - # 执行反注入检测(直接使用字典格式) - anti_injector = get_anti_injector() - result, modified_content, reason = await anti_injector.process_message( - message_data, self.context.chat_stream - ) - - # 根据反注入结果处理消息数据 - await anti_injector.handle_message_storage(result, modified_content, reason or "", message_data) - - if result == ProcessResult.BLOCKED_BAN: - # 用户被封禁 - 直接阻止回复生成 - anti_injector_logger.warning(f"用户被反注入系统封禁,阻止回复生成: {reason}") - return None - elif result == ProcessResult.BLOCKED_INJECTION: - # 消息被阻止(危险内容等) - 直接阻止回复生成 - anti_injector_logger.warning(f"消息被反注入系统阻止,阻止回复生成: {reason}") - return None - elif result == ProcessResult.COUNTER_ATTACK: - # 反击模式:生成反击消息作为回复 - anti_injector_logger.info(f"反击模式启动,生成反击回复: {reason}") - if modified_content: - # 返回反击消息作为回复内容 - return [("text", modified_content)] - else: - # 没有反击内容时阻止回复生成 - return None - - # 检查是否需要加盾处理 - safety_prompt = None - if result == ProcessResult.SHIELDED: - # 获取安全系统提示词并注入 - shield = anti_injector.shield - safety_prompt = shield.get_safety_system_prompt() - await Prompt.create_async(safety_prompt, "anti_injection_safety_prompt") - anti_injector_logger.info(f"消息已被反注入系统加盾处理,已注入安全提示词: {reason}") - - # 处理被修改的消息内容(用于生成回复) - modified_reply_to = reply_to - if modified_content: - # 更新消息内容用于生成回复 - anti_injector_logger.info(f"消息内容已被反注入系统修改,使用修改后内容生成回复: {reason}") - # 解析原始reply_to格式:"发送者:消息内容" - if ":" in reply_to: - sender_part, _ = reply_to.split(":", 1) - modified_reply_to = f"{sender_part}:{modified_content}" - else: - # 如果格式不标准,直接使用修改后的内容 - modified_reply_to = modified_content - - # === 正常的回复生成流程 === - success, reply_set, _ = await generator_api.generate_reply( - chat_stream=self.context.chat_stream, - reply_to=modified_reply_to, # 使用可能被修改的内容 - available_actions=available_actions, - enable_tool=global_config.tool.enable_tool, - request_type=request_type, - from_plugin=False, - ) - - if not success or not reply_set: - logger.info(f"对 {message_data.get('processed_plain_text')} 的回复生成失败") - return None - - return reply_set - - except Exception as e: - logger.error(f"{self.context.log_prefix}回复生成出现错误:{str(e)} {traceback.format_exc()}") - return None From 8074a1b0825e20aa758a7c7b91ee9b8b54fbbf2c Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 5 Sep 2025 21:51:55 +0800 Subject: [PATCH 11/69] =?UTF-8?q?refactor(sleep):=20=E5=B0=86=E7=9D=A1?= =?UTF-8?q?=E7=9C=A0=E5=92=8C=E5=94=A4=E9=86=92=E7=AE=A1=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E9=87=8D=E6=9E=84=E5=88=B0=E7=8B=AC=E7=AB=8B=E7=9A=84?= =?UTF-8?q?sleep=5Fmanager=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原先分散在 `chat/chat_loop` 和 `schedule` 目录下的 `WakeUpManager` 和 `SleepManager` 相关逻辑,统一移动并整合到新的 `src/chat/chat_loop/sleep_manager` 模块中。 这次重构旨在提高代码的模块化和内聚性,使睡眠相关的功能更加集中和易于管理。同时更新了所有相关的导入路径,以反映新的文件结构。 --- src/chat/chat_loop/heartFC_chat.py | 2 +- src/chat/chat_loop/hfc_context.py | 2 +- .../chat_loop/sleep_manager}/sleep_manager.py | 2 +- src/chat/chat_loop/{ => sleep_manager}/wakeup_manager.py | 4 ++-- src/schedule/schedule_manager.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) rename src/{schedule => chat/chat_loop/sleep_manager}/sleep_manager.py (99%) rename src/chat/chat_loop/{ => sleep_manager}/wakeup_manager.py (98%) diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index 80a7aff76..5f502a68f 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -18,7 +18,7 @@ from .proactive.proactive_thinker import ProactiveThinker from .cycle_processor import CycleProcessor from .response_handler import ResponseHandler from .cycle_tracker import CycleTracker -from .wakeup_manager import WakeUpManager +from .sleep_manager.wakeup_manager import WakeUpManager from .proactive.events import ProactiveTriggerEvent logger = get_logger("hfc") diff --git a/src/chat/chat_loop/hfc_context.py b/src/chat/chat_loop/hfc_context.py index 9e7c73722..fca6a65e4 100644 --- a/src/chat/chat_loop/hfc_context.py +++ b/src/chat/chat_loop/hfc_context.py @@ -7,7 +7,7 @@ from src.chat.planner_actions.action_manager import ActionManager from src.chat.chat_loop.hfc_utils import CycleDetail if TYPE_CHECKING: - from .wakeup_manager import WakeUpManager + from .sleep_manager.wakeup_manager import WakeUpManager from .energy_manager import EnergyManager from .heartFC_chat import HeartFChatting diff --git a/src/schedule/sleep_manager.py b/src/chat/chat_loop/sleep_manager/sleep_manager.py similarity index 99% rename from src/schedule/sleep_manager.py rename to src/chat/chat_loop/sleep_manager/sleep_manager.py index 8da39ba0e..3acda568a 100644 --- a/src/schedule/sleep_manager.py +++ b/src/chat/chat_loop/sleep_manager/sleep_manager.py @@ -10,7 +10,7 @@ from src.manager.local_store_manager import local_storage from src.plugin_system.apis import send_api, generator_api if TYPE_CHECKING: - from src.chat.chat_loop.wakeup_manager import WakeUpManager + from mmc.src.chat.chat_loop.sleep_manager.wakeup_manager import WakeUpManager logger = get_logger("sleep_manager") diff --git a/src/chat/chat_loop/wakeup_manager.py b/src/chat/chat_loop/sleep_manager/wakeup_manager.py similarity index 98% rename from src/chat/chat_loop/wakeup_manager.py rename to src/chat/chat_loop/sleep_manager/wakeup_manager.py index df5957b14..b72fe80de 100644 --- a/src/chat/chat_loop/wakeup_manager.py +++ b/src/chat/chat_loop/sleep_manager/wakeup_manager.py @@ -4,7 +4,7 @@ from typing import Optional from src.common.logger import get_logger from src.config.config import global_config from src.manager.local_store_manager import local_storage -from .hfc_context import HfcContext +from ..hfc_context import HfcContext logger = get_logger("wakeup") @@ -139,7 +139,7 @@ class WakeUpManager: # 只有在休眠且非失眠状态下才累积唤醒度 from src.schedule.schedule_manager import schedule_manager - from src.schedule.sleep_manager import SleepState + from mmc.src.chat.chat_loop.sleep_manager.sleep_manager import SleepState current_sleep_state = schedule_manager.get_current_sleep_state() if current_sleep_state != SleepState.SLEEPING: diff --git a/src/schedule/schedule_manager.py b/src/schedule/schedule_manager.py index 5e661479b..e543eb706 100644 --- a/src/schedule/schedule_manager.py +++ b/src/schedule/schedule_manager.py @@ -15,10 +15,10 @@ from src.llm_models.utils_model import LLMRequest from src.common.logger import get_logger from json_repair import repair_json from src.manager.async_task_manager import AsyncTask, async_task_manager -from .sleep_manager import SleepManager, SleepState +from ..chat.chat_loop.sleep_manager.sleep_manager import SleepManager, SleepState if TYPE_CHECKING: - from src.chat.chat_loop.wakeup_manager import WakeUpManager + from src.chat.chat_loop.sleep_manager.wakeup_manager import WakeUpManager logger = get_logger("schedule_manager") From cb8a9e33fcf9947a7cfa06aa2016e10719ca8052 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 5 Sep 2025 21:57:13 +0800 Subject: [PATCH 12/69] =?UTF-8?q?docs(chat):=20=E4=B8=BAHeartFChatting?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=B7=BB=E5=8A=A0=E8=AF=A6=E7=BB=86=E7=9A=84?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 `_handle_proactive_monitor_completion`, `_proactive_monitor_loop`, `_should_enable_proactive_thinking`, `_get_dynamic_thinking_interval`, 和 `_format_duration` 等关键函数补充了详细的文档字符串。 这些文档字符串解释了每个函数的功能、参数和返回值,以提高代码的可读性和可维护性。 --- src/chat/chat_loop/heartFC_chat.py | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index 5f502a68f..037ad620e 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -157,6 +157,16 @@ class HeartFChatting: logger.info(f"{self.context.log_prefix} HeartFChatting: 结束了聊天") def _handle_proactive_monitor_completion(self, task: asyncio.Task): + """ + 处理主动思考监视器任务完成 + + Args: + task: 完成的异步任务对象 + + 功能说明: + - 处理任务异常完成的情况 + - 记录任务正常结束或被取消的日志 + """ try: if exception := task.exception(): logger.error(f"{self.context.log_prefix} 主动思考监视器异常: {exception}") @@ -166,6 +176,15 @@ class HeartFChatting: logger.info(f"{self.context.log_prefix} 主动思考监视器被取消") async def _proactive_monitor_loop(self): + """ + 主动思考监视器循环 + + 功能说明: + - 定期检查是否需要进行主动思考 + - 计算聊天沉默时间,并与动态思考间隔比较 + - 当沉默时间超过阈值时,触发主动思考 + - 处理思考过程中的异常 + """ while self.context.running: await asyncio.sleep(15) @@ -191,6 +210,17 @@ class HeartFChatting: logger.error(traceback.format_exc()) def _should_enable_proactive_thinking(self) -> bool: + """ + 判断是否应启用主动思考 + + Returns: + bool: 如果应启用主动思考则返回True,否则返回False + + 功能说明: + - 检查全局配置和特定聊天设置 + - 支持按群聊和私聊分别配置 + - 支持白名单模式,只在特定聊天中启用 + """ if not self.context.chat_stream: return False @@ -212,6 +242,17 @@ class HeartFChatting: return not enable_list or current_chat_identifier in enable_list def _get_dynamic_thinking_interval(self) -> float: + """ + 获取动态思考间隔时间 + + Returns: + float: 思考间隔秒数 + + 功能说明: + - 尝试从timing_utils导入正态分布间隔函数 + - 根据配置计算动态间隔,增加随机性 + - 在无法导入或计算出错时,回退到固定的间隔 + """ try: from src.utils.timing_utils import get_normal_distributed_interval @@ -239,6 +280,15 @@ class HeartFChatting: return max(300, abs(global_config.chat.proactive_thinking_interval)) def _format_duration(self, seconds: float) -> str: + """ + 格式化时长为可读字符串 + + Args: + seconds: 时长秒数 + + Returns: + str: 格式化后的字符串 (例如 "1小时2分3秒") + """ hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) secs = int(seconds % 60) From c9b712d8fa4010c8637424d04690541484cc855b Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sat, 6 Sep 2025 00:10:54 +0800 Subject: [PATCH 13/69] =?UTF-8?q?refactor(prompt):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=B9=B6=E7=BB=9F=E4=B8=80=E6=8F=90=E7=A4=BA=E8=AF=8D=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除旧的智能提示词系统(smart_prompt.py)和相关参数模块(prompt_parameters.py) - 将 prompt_builder.py 重命名为 prompt.py 并精简功能 - 更新所有模块的导入路径从 `src.chat.utils.prompt_builder` 到 `src.chat.utils.prompt` - 统一提示词构建接口,使用新的 PromptContext 替代 SmartPromptParameters - 移除重复和冗余代码,简化系统架构 BREAKING CHANGE: 旧的 SmartPrompt 系统已被完全移除,所有相关模块需要改用新的统一 Prompt 系统 --- scripts/update_prompt_imports.py | 61 ++ src/chat/chat_loop/response_handler.py | 2 +- src/chat/express/expression_learner.py | 2 +- src/chat/express/expression_selector.py | 2 +- src/chat/memory_system/memory_activator.py | 2 +- src/chat/message_receive/bot.py | 2 +- src/chat/planner_actions/planner.py | 2 +- src/chat/replyer/default_generator.py | 31 +- src/chat/utils/prompt.py | 693 +++++++++++++ src/chat/utils/prompt_builder.py | 299 ------ src/chat/utils/prompt_parameters.py | 156 --- src/chat/utils/prompt_utils.py | 2 +- src/chat/utils/smart_prompt.py | 938 ------------------ src/mais4u/mai_think.py | 2 +- .../body_emotion_action_manager.py | 2 +- src/mais4u/mais4u_chat/s4u_mood_manager.py | 2 +- src/mais4u/mais4u_chat/s4u_prompt.py | 2 +- src/mood/mood_manager.py | 2 +- src/person_info/relationship_fetcher.py | 2 +- src/plugin_system/core/tool_use.py | 2 +- 20 files changed, 782 insertions(+), 1424 deletions(-) create mode 100644 scripts/update_prompt_imports.py create mode 100644 src/chat/utils/prompt.py delete mode 100644 src/chat/utils/prompt_builder.py delete mode 100644 src/chat/utils/prompt_parameters.py delete mode 100644 src/chat/utils/smart_prompt.py diff --git a/scripts/update_prompt_imports.py b/scripts/update_prompt_imports.py new file mode 100644 index 000000000..672659086 --- /dev/null +++ b/scripts/update_prompt_imports.py @@ -0,0 +1,61 @@ +""" +更新Prompt类导入脚本 +将旧的prompt_builder.Prompt导入更新为unified_prompt.Prompt +""" + +import os +import re +from pathlib import Path + +# 需要更新的文件列表 +files_to_update = [ + "src/person_info/relationship_fetcher.py", + "src/mood/mood_manager.py", + "src/mais4u/mais4u_chat/body_emotion_action_manager.py", + "src/chat/express/expression_learner.py", + "src/chat/planner_actions/planner.py", + "src/mais4u/mais4u_chat/s4u_prompt.py", + "src/chat/message_receive/bot.py", + "src/chat/replyer/default_generator.py", + "src/chat/express/expression_selector.py", + "src/mais4u/mai_think.py", + "src/mais4u/mais4u_chat/s4u_mood_manager.py", + "src/plugin_system/core/tool_use.py", + "src/chat/memory_system/memory_activator.py", + "src/chat/utils/smart_prompt.py" +] + +def update_prompt_imports(file_path): + """更新文件中的Prompt导入""" + if not os.path.exists(file_path): + print(f"文件不存在: {file_path}") + return False + + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 替换导入语句 + old_import = "from src.chat.utils.prompt_builder import Prompt, global_prompt_manager" + new_import = "from src.chat.utils.prompt import Prompt, global_prompt_manager" + + if old_import in content: + new_content = content.replace(old_import, new_import) + with open(file_path, 'w', encoding='utf-8') as f: + f.write(new_content) + print(f"已更新: {file_path}") + return True + else: + print(f"无需更新: {file_path}") + return False + +def main(): + """主函数""" + updated_count = 0 + for file_path in files_to_update: + if update_prompt_imports(file_path): + updated_count += 1 + + print(f"\n更新完成!共更新了 {updated_count} 个文件") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/chat/chat_loop/response_handler.py b/src/chat/chat_loop/response_handler.py index ecfc6addb..55dca45a3 100644 --- a/src/chat/chat_loop/response_handler.py +++ b/src/chat/chat_loop/response_handler.py @@ -12,7 +12,7 @@ from .hfc_context import HfcContext # 导入反注入系统 from src.chat.antipromptinjector import get_anti_injector from src.chat.antipromptinjector.types import ProcessResult -from src.chat.utils.prompt_builder import Prompt +from src.chat.utils.prompt import Prompt logger = get_logger("hfc") anti_injector_logger = get_logger("anti_injector") diff --git a/src/chat/express/expression_learner.py b/src/chat/express/expression_learner.py index 7f127f0a5..1b9fcf267 100644 --- a/src/chat/express/expression_learner.py +++ b/src/chat/express/expression_learner.py @@ -13,7 +13,7 @@ from src.common.database.sqlalchemy_models import Expression from src.llm_models.utils_model import LLMRequest from src.config.config import model_config, global_config from src.chat.utils.chat_message_builder import get_raw_msg_by_timestamp_with_chat_inclusive, build_anonymous_messages -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.chat.utils.prompt import Prompt, global_prompt_manager from src.chat.message_receive.chat_stream import get_chat_manager diff --git a/src/chat/express/expression_selector.py b/src/chat/express/expression_selector.py index f0991c7c7..2883ec82d 100644 --- a/src/chat/express/expression_selector.py +++ b/src/chat/express/expression_selector.py @@ -11,7 +11,7 @@ from src.config.config import global_config, model_config from src.common.logger import get_logger from sqlalchemy import select from src.common.database.sqlalchemy_models import Expression -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.chat.utils.prompt import Prompt, global_prompt_manager from src.common.database.sqlalchemy_database_api import get_db_session logger = get_logger("expression_selector") diff --git a/src/chat/memory_system/memory_activator.py b/src/chat/memory_system/memory_activator.py index 4067363f0..33d22a5dd 100644 --- a/src/chat/memory_system/memory_activator.py +++ b/src/chat/memory_system/memory_activator.py @@ -8,7 +8,7 @@ from datetime import datetime from src.llm_models.utils_model import LLMRequest from src.config.config import global_config, model_config from src.common.logger import get_logger -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.chat.utils.prompt import Prompt, global_prompt_manager from src.chat.memory_system.Hippocampus import hippocampus_manager diff --git a/src/chat/message_receive/bot.py b/src/chat/message_receive/bot.py index e71616892..260a42170 100644 --- a/src/chat/message_receive/bot.py +++ b/src/chat/message_receive/bot.py @@ -12,7 +12,7 @@ from src.chat.message_receive.chat_stream import get_chat_manager, ChatStream from src.chat.message_receive.message import MessageRecv, MessageRecvS4U from src.chat.message_receive.storage import MessageStorage from src.chat.heart_flow.heartflow_message_processor import HeartFCMessageReceiver -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.chat.utils.prompt import Prompt, global_prompt_manager from src.plugin_system.core import component_registry, event_manager, global_announcement_manager from src.plugin_system.base import BaseCommand, EventType from src.mais4u.mais4u_chat.s4u_msg_processor import S4UMessageProcessor diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 291c19a66..c08d52029 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -9,7 +9,7 @@ from json_repair import repair_json from src.llm_models.utils_model import LLMRequest from src.config.config import global_config, model_config from src.common.logger import get_logger -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.chat.utils.prompt import Prompt, global_prompt_manager from src.chat.utils.chat_message_builder import ( build_readable_actions, get_actions_by_timestamp_with_chat, diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index be58c5426..973565d37 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -1,6 +1,6 @@ """ -默认回复生成器 - 集成SmartPrompt系统 -使用重构后的SmartPrompt系统替换原有的复杂提示词构建逻辑 +默认回复生成器 - 集成统一Prompt系统 +使用重构后的统一Prompt系统替换原有的复杂提示词构建逻辑 """ import traceback @@ -23,7 +23,7 @@ from src.chat.message_receive.chat_stream import ChatStream from src.chat.message_receive.uni_message_sender import HeartFCSender from src.chat.utils.timer_calculator import Timer from src.chat.utils.utils import get_chat_type_and_target_info -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.chat.utils.prompt import Prompt, global_prompt_manager from src.chat.utils.chat_message_builder import ( build_readable_messages, get_raw_msg_before_timestamp_with_chat, @@ -39,8 +39,8 @@ from src.plugin_system.base.component_types import ActionInfo, EventType from src.plugin_system.apis import llm_api from src.schedule.schedule_manager import schedule_manager -# 导入新的智能Prompt系统 -from src.chat.utils.smart_prompt import SmartPrompt, SmartPromptParameters +# 导入新的统一Prompt系统 +from src.chat.utils.prompt import Prompt, PromptContext logger = get_logger("replyer") @@ -971,8 +971,8 @@ class DefaultReplyer: # 根据配置选择模板 current_prompt_mode = global_config.personality.prompt_mode - # 使用重构后的SmartPrompt系统 - prompt_params = SmartPromptParameters( + # 使用新的统一Prompt系统 + prompt_context = PromptContext( chat_id=chat_id, is_group_chat=is_group_chat, sender=sender, @@ -1005,12 +1005,9 @@ class DefaultReplyer: action_descriptions=action_descriptions, ) - # 使用重构后的SmartPrompt系统 - smart_prompt = SmartPrompt( - template_name=None, # 由current_prompt_mode自动选择 - parameters=prompt_params, - ) - prompt_text = await smart_prompt.build_prompt() + # 使用新的统一Prompt系统 + prompt = Prompt(template_name=None, context=prompt_context) # 由current_prompt_mode自动选择 + prompt_text = await prompt.build_prompt() return prompt_text @@ -1111,8 +1108,8 @@ class DefaultReplyer: template_name = "default_expressor_prompt" - # 使用重构后的SmartPrompt系统 - Expressor模式 - prompt_params = SmartPromptParameters( + # 使用新的统一Prompt系统 - Expressor模式 + prompt_context = PromptContext( chat_id=chat_id, is_group_chat=is_group_chat, sender=sender, @@ -1132,8 +1129,8 @@ class DefaultReplyer: relation_info_block=relation_info, ) - smart_prompt = SmartPrompt(parameters=prompt_params) - prompt_text = await smart_prompt.build_prompt() + prompt = Prompt(template_name=template_name, context=prompt_context) + prompt_text = await prompt.build_prompt() return prompt_text diff --git a/src/chat/utils/prompt.py b/src/chat/utils/prompt.py new file mode 100644 index 000000000..1e44b72d8 --- /dev/null +++ b/src/chat/utils/prompt.py @@ -0,0 +1,693 @@ +""" +统一提示词系统 - 合并模板管理和智能构建功能 +将原有的Prompt类和SmartPrompt功能整合为一个真正的Prompt类 +""" + +import re +import asyncio +import time +import contextvars +from dataclasses import dataclass, field +from typing import Dict, Any, Optional, List, Union, Literal, Tuple +from contextlib import asynccontextmanager + +from rich.traceback import install +from src.common.logger import get_logger +from src.config.config import global_config +from src.chat.utils.chat_message_builder import build_readable_messages +from src.chat.utils.prompt_utils import PromptUtils +from src.person_info.person_info import get_person_info_manager + +install(extra_lines=3) +logger = get_logger("unified_prompt") + + +@dataclass +class PromptParameters: + """统一提示词参数系统""" + + # 基础参数 + chat_id: str = "" + is_group_chat: bool = False + sender: str = "" + target: str = "" + reply_to: str = "" + extra_info: str = "" + prompt_mode: Literal["s4u", "normal", "minimal"] = "s4u" + + # 功能开关 + enable_tool: bool = True + enable_memory: bool = True + enable_expression: bool = True + enable_relation: bool = True + enable_cross_context: bool = True + enable_knowledge: bool = True + + # 性能控制 + max_context_messages: int = 50 + + # 调试选项 + debug_mode: bool = False + + # 聊天历史和上下文 + chat_target_info: Optional[Dict[str, Any]] = None + message_list_before_now_long: List[Dict[str, Any]] = field(default_factory=list) + message_list_before_short: List[Dict[str, Any]] = field(default_factory=list) + chat_talking_prompt_short: str = "" + target_user_info: Optional[Dict[str, Any]] = None + + # 已构建的内容块 + expression_habits_block: str = "" + relation_info_block: str = "" + memory_block: str = "" + tool_info_block: str = "" + knowledge_prompt: str = "" + cross_context_block: str = "" + + # 其他内容块 + keywords_reaction_prompt: str = "" + extra_info_block: str = "" + time_block: str = "" + identity_block: str = "" + schedule_block: str = "" + moderation_prompt_block: str = "" + reply_target_block: str = "" + mood_prompt: str = "" + action_descriptions: str = "" + + # 可用动作信息 + available_actions: Optional[Dict[str, Any]] = None + + def validate(self) -> List[str]: + """参数验证""" + errors = [] + if not self.chat_id: + errors.append("chat_id不能为空") + if self.prompt_mode not in ["s4u", "normal", "minimal"]: + errors.append("prompt_mode必须是's4u'、'normal'或'minimal'") + if self.max_context_messages <= 0: + errors.append("max_context_messages必须大于0") + return errors + + +class PromptContext: + """提示词上下文管理器""" + + def __init__(self): + self._context_prompts: Dict[str, Dict[str, "Prompt"]] = {} + self._current_context_var = contextvars.ContextVar("current_context", default=None) + self._context_lock = asyncio.Lock() + + @property + def _current_context(self) -> Optional[str]: + """获取当前协程的上下文ID""" + return self._current_context_var.get() + + @_current_context.setter + def _current_context(self, value: Optional[str]): + """设置当前协程的上下文ID""" + self._current_context_var.set(value) # type: ignore + + @asynccontextmanager + async def async_scope(self, context_id: Optional[str] = None): + """创建一个异步的临时提示模板作用域""" + if context_id is not None: + try: + await asyncio.wait_for(self._context_lock.acquire(), timeout=5.0) + try: + if context_id not in self._context_prompts: + self._context_prompts[context_id] = {} + finally: + self._context_lock.release() + except asyncio.TimeoutError: + logger.warning(f"获取上下文锁超时,context_id: {context_id}") + context_id = None + + previous_context = self._current_context + token = self._current_context_var.set(context_id) if context_id else None + else: + previous_context = self._current_context + token = None + + try: + yield self + finally: + if context_id is not None and token is not None: + try: + self._current_context_var.reset(token) + except Exception as e: + logger.warning(f"恢复上下文时出错: {e}") + try: + self._current_context = previous_context + except Exception: + ... + + async def get_prompt_async(self, name: str) -> Optional["Prompt"]: + """异步获取当前作用域中的提示模板""" + async with self._context_lock: + current_context = self._current_context + logger.debug(f"获取提示词: {name} 当前上下文: {current_context}") + if ( + current_context + and current_context in self._context_prompts + and name in self._context_prompts[current_context] + ): + return self._context_prompts[current_context][name] + return None + + async def register_async(self, prompt: "Prompt", context_id: Optional[str] = None) -> None: + """异步注册提示模板到指定作用域""" + async with self._context_lock: + if target_context := context_id or self._current_context: + if prompt.name: + self._context_prompts.setdefault(target_context, {})[prompt.name] = prompt + + +class PromptManager: + """统一提示词管理器""" + + def __init__(self): + self._prompts = {} + self._counter = 0 + self._context = PromptContext() + self._lock = asyncio.Lock() + + @asynccontextmanager + async def async_message_scope(self, message_id: Optional[str] = None): + """为消息处理创建异步临时作用域""" + async with self._context.async_scope(message_id): + yield self + + async def get_prompt_async(self, name: str) -> "Prompt": + """异步获取提示模板""" + context_prompt = await self._context.get_prompt_async(name) + if context_prompt is not None: + logger.debug(f"从上下文中获取提示词: {name} {context_prompt}") + return context_prompt + + async with self._lock: + if name not in self._prompts: + raise KeyError(f"Prompt '{name}' not found") + return self._prompts[name] + + def generate_name(self, template: str) -> str: + """为未命名的prompt生成名称""" + self._counter += 1 + return f"prompt_{self._counter}" + + def register(self, prompt: "Prompt") -> None: + """注册一个prompt""" + if not prompt.name: + prompt.name = self.generate_name(prompt.template) + self._prompts[prompt.name] = prompt + + def add_prompt(self, name: str, fstr: str) -> "Prompt": + """添加新提示模板""" + prompt = Prompt(fstr, name=name) + if prompt.name: + self._prompts[prompt.name] = prompt + return prompt + + async def format_prompt(self, name: str, **kwargs) -> str: + """格式化提示模板""" + prompt = await self.get_prompt_async(name) + result = prompt.format(**kwargs) + return result + + +# 全局单例 +global_prompt_manager = PromptManager() + + +class Prompt: + """ + 统一提示词类 - 合并模板管理和智能构建功能 + 真正的Prompt类,支持模板管理和智能上下文构建 + """ + + # 临时标记,作为类常量 + _TEMP_LEFT_BRACE = "__ESCAPED_LEFT_BRACE__" + _TEMP_RIGHT_BRACE = "__ESCAPED_RIGHT_BRACE__" + + def __init__( + self, + template: str, + name: Optional[str] = None, + parameters: Optional[PromptParameters] = None, + should_register: bool = True + ): + """ + 初始化统一提示词 + + Args: + template: 提示词模板字符串 + name: 提示词名称 + parameters: 构建参数 + should_register: 是否自动注册到全局管理器 + """ + self.template = template + self.name = name + self.parameters = parameters or PromptParameters() + self.args = self._parse_template_args(template) + self._formatted_result = "" + + # 预处理模板中的转义花括号 + self._processed_template = self._process_escaped_braces(template) + + # 自动注册 + if should_register and not global_prompt_manager._context._current_context: + global_prompt_manager.register(self) + + @staticmethod + def _process_escaped_braces(template) -> str: + """处理模板中的转义花括号""" + if isinstance(template, list): + template = "\n".join(str(item) for item in template) + elif not isinstance(template, str): + template = str(template) + + return template.replace("\\{", Prompt._TEMP_LEFT_BRACE).replace("\\}", Prompt._TEMP_RIGHT_BRACE) + + @staticmethod + def _restore_escaped_braces(template: str) -> str: + """将临时标记还原为实际的花括号字符""" + return template.replace(Prompt._TEMP_LEFT_BRACE, "{").replace(Prompt._TEMP_RIGHT_BRACE, "}") + + def _parse_template_args(self, template: str) -> List[str]: + """解析模板参数""" + template_args = [] + processed_template = self._process_escaped_braces(template) + result = re.findall(r"\{(.*?)}", processed_template) + for expr in result: + if expr and expr not in template_args: + template_args.append(expr) + return template_args + + async def build(self) -> str: + """ + 构建完整的提示词,包含智能上下文 + + Returns: + str: 构建完成的提示词文本 + """ + # 参数验证 + errors = self.parameters.validate() + if errors: + logger.error(f"参数验证失败: {', '.join(errors)}") + raise ValueError(f"参数验证失败: {', '.join(errors)}") + + start_time = time.time() + try: + # 构建上下文数据 + context_data = await self._build_context_data() + + # 格式化模板 + result = await self._format_with_context(context_data) + + total_time = time.time() - start_time + logger.debug(f"Prompt构建完成,模式: {self.parameters.prompt_mode}, 耗时: {total_time:.2f}s") + + self._formatted_result = result + return result + + except asyncio.TimeoutError as e: + logger.error(f"构建Prompt超时: {e}") + raise TimeoutError(f"构建Prompt超时: {e}") + except Exception as e: + logger.error(f"构建Prompt失败: {e}") + raise RuntimeError(f"构建Prompt失败: {e}") + + async def _build_context_data(self) -> Dict[str, Any]: + """构建智能上下文数据""" + # 并行执行所有构建任务 + start_time = time.time() + timing_logs = {} + + try: + # 准备构建任务 + tasks = [] + task_names = [] + + # 初始化预构建参数 + pre_built_params = {} + if self.parameters.expression_habits_block: + pre_built_params["expression_habits_block"] = self.parameters.expression_habits_block + if self.parameters.relation_info_block: + pre_built_params["relation_info_block"] = self.parameters.relation_info_block + if self.parameters.memory_block: + pre_built_params["memory_block"] = self.parameters.memory_block + if self.parameters.tool_info_block: + pre_built_params["tool_info_block"] = self.parameters.tool_info_block + if self.parameters.knowledge_prompt: + pre_built_params["knowledge_prompt"] = self.parameters.knowledge_prompt + if self.parameters.cross_context_block: + pre_built_params["cross_context_block"] = self.parameters.cross_context_block + + # 根据参数确定要构建的项 + if self.parameters.enable_expression and not pre_built_params.get("expression_habits_block"): + tasks.append(self._build_expression_habits()) + task_names.append("expression_habits") + + if self.parameters.enable_memory and not pre_built_params.get("memory_block"): + tasks.append(self._build_memory_block()) + task_names.append("memory_block") + + if self.parameters.enable_relation and not pre_built_params.get("relation_info_block"): + tasks.append(self._build_relation_info()) + task_names.append("relation_info") + + if self.parameters.enable_tool and not pre_built_params.get("tool_info_block"): + tasks.append(self._build_tool_info()) + task_names.append("tool_info") + + if self.parameters.enable_knowledge and not pre_built_params.get("knowledge_prompt"): + tasks.append(self._build_knowledge_info()) + task_names.append("knowledge_info") + + if self.parameters.enable_cross_context and not pre_built_params.get("cross_context_block"): + tasks.append(self._build_cross_context()) + task_names.append("cross_context") + + # 性能优化 + base_timeout = 10.0 + task_timeout = 2.0 + timeout_seconds = min( + max(base_timeout, len(tasks) * task_timeout), + 30.0, + ) + + max_concurrent_tasks = 5 + if len(tasks) > max_concurrent_tasks: + results = [] + for i in range(0, len(tasks), max_concurrent_tasks): + batch_tasks = tasks[i : i + max_concurrent_tasks] + batch_names = task_names[i : i + max_concurrent_tasks] + + batch_results = await asyncio.wait_for( + asyncio.gather(*batch_tasks, return_exceptions=True), timeout=timeout_seconds + ) + results.extend(batch_results) + else: + results = await asyncio.wait_for( + asyncio.gather(*tasks, return_exceptions=True), timeout=timeout_seconds + ) + + # 处理结果 + context_data = {} + for i, result in enumerate(results): + task_name = task_names[i] if i < len(task_names) else f"task_{i}" + + if isinstance(result, Exception): + logger.error(f"构建任务{task_name}失败: {str(result)}") + elif isinstance(result, dict): + context_data.update(result) + + # 添加预构建的参数 + for key, value in pre_built_params.items(): + if value: + context_data[key] = value + + except asyncio.TimeoutError: + logger.error(f"构建超时 ({timeout_seconds}s)") + context_data = {} + for key, value in pre_built_params.items(): + if value: + context_data[key] = value + + # 构建聊天历史 + if self.parameters.prompt_mode == "s4u": + await self._build_s4u_chat_context(context_data) + else: + await self._build_normal_chat_context(context_data) + + # 补充基础信息 + context_data.update({ + "keywords_reaction_prompt": self.parameters.keywords_reaction_prompt, + "extra_info_block": self.parameters.extra_info_block, + "time_block": self.parameters.time_block or f"当前时间:{time.strftime('%Y-%m-%d %H:%M:%S')}", + "identity": self.parameters.identity_block, + "schedule_block": self.parameters.schedule_block, + "moderation_prompt": self.parameters.moderation_prompt_block, + "reply_target_block": self.parameters.reply_target_block, + "mood_state": self.parameters.mood_prompt, + "action_descriptions": self.parameters.action_descriptions, + }) + + total_time = time.time() - start_time + logger.debug(f"上下文构建完成,总耗时: {total_time:.2f}s") + + return context_data + + async def _build_s4u_chat_context(self, context_data: Dict[str, Any]) -> None: + """构建S4U模式的聊天上下文""" + if not self.parameters.message_list_before_now_long: + return + + core_dialogue, background_dialogue = await self._build_s4u_chat_history_prompts( + self.parameters.message_list_before_now_long, + self.parameters.target_user_info.get("user_id") if self.parameters.target_user_info else "", + self.parameters.sender + ) + + context_data["core_dialogue_prompt"] = core_dialogue + context_data["background_dialogue_prompt"] = background_dialogue + + async def _build_normal_chat_context(self, context_data: Dict[str, Any]) -> None: + """构建normal模式的聊天上下文""" + if not self.parameters.chat_talking_prompt_short: + return + + context_data["chat_info"] = f"""群里的聊天内容: +{self.parameters.chat_talking_prompt_short}""" + + async def _build_s4u_chat_history_prompts( + self, message_list_before_now: List[Dict[str, Any]], target_user_id: str, sender: str + ) -> Tuple[str, str]: + """构建S4U风格的分离对话prompt""" + # 实现逻辑与原有SmartPromptBuilder相同 + core_dialogue_list = [] + bot_id = str(global_config.bot.qq_account) + + for msg_dict in message_list_before_now: + try: + msg_user_id = str(msg_dict.get("user_id")) + reply_to = msg_dict.get("reply_to", "") + platform, reply_to_user_id = PromptUtils.parse_reply_target(reply_to) + if (msg_user_id == bot_id and reply_to_user_id == target_user_id) or msg_user_id == target_user_id: + core_dialogue_list.append(msg_dict) + except Exception as e: + logger.error(f"处理消息记录时出错: {msg_dict}, 错误: {e}") + + # 构建背景对话 prompt + all_dialogue_prompt = "" + if message_list_before_now: + latest_25_msgs = message_list_before_now[-int(global_config.chat.max_context_size) :] + all_dialogue_prompt_str = build_readable_messages( + latest_25_msgs, + replace_bot_name=True, + timestamp_mode="normal", + truncate=True, + ) + all_dialogue_prompt = f"所有用户的发言:\n{all_dialogue_prompt_str}" + + # 构建核心对话 prompt + core_dialogue_prompt = "" + if core_dialogue_list: + latest_5_messages = core_dialogue_list[-5:] if len(core_dialogue_list) >= 5 else core_dialogue_list + has_bot_message = any(str(msg.get("user_id")) == bot_id for msg in latest_5_messages) + + if not has_bot_message: + core_dialogue_prompt = "" + else: + core_dialogue_list = core_dialogue_list[-int(global_config.chat.max_context_size * 2) :] + + core_dialogue_prompt_str = build_readable_messages( + core_dialogue_list, + replace_bot_name=True, + merge_messages=False, + timestamp_mode="normal_no_YMD", + read_mark=0.0, + truncate=True, + show_actions=True, + ) + core_dialogue_prompt = f"""-------------------------------- +这是你和{sender}的对话,你们正在交流中: +{core_dialogue_prompt_str} +-------------------------------- +""" + + return core_dialogue_prompt, all_dialogue_prompt + + async def _build_expression_habits(self) -> Dict[str, Any]: + """构建表达习惯""" + # 简化的实现,完整实现需要导入相关模块 + return {"expression_habits_block": ""} + + async def _build_memory_block(self) -> Dict[str, Any]: + """构建记忆块""" + # 简化的实现 + return {"memory_block": ""} + + async def _build_relation_info(self) -> Dict[str, Any]: + """构建关系信息""" + try: + relation_info = await PromptUtils.build_relation_info(self.parameters.chat_id, self.parameters.reply_to) + return {"relation_info_block": relation_info} + except Exception as e: + logger.error(f"构建关系信息失败: {e}") + return {"relation_info_block": ""} + + async def _build_tool_info(self) -> Dict[str, Any]: + """构建工具信息""" + # 简化的实现 + return {"tool_info_block": ""} + + async def _build_knowledge_info(self) -> Dict[str, Any]: + """构建知识信息""" + # 简化的实现 + return {"knowledge_prompt": ""} + + async def _build_cross_context(self) -> Dict[str, Any]: + """构建跨群上下文""" + try: + cross_context = await PromptUtils.build_cross_context( + self.parameters.chat_id, self.parameters.prompt_mode, self.parameters.target_user_info + ) + return {"cross_context_block": cross_context} + except Exception as e: + logger.error(f"构建跨群上下文失败: {e}") + return {"cross_context_block": ""} + + async def _format_with_context(self, context_data: Dict[str, Any]) -> str: + """使用上下文数据格式化模板""" + if self.parameters.prompt_mode == "s4u": + params = self._prepare_s4u_params(context_data) + elif self.parameters.prompt_mode == "normal": + params = self._prepare_normal_params(context_data) + else: + params = self._prepare_default_params(context_data) + + return await global_prompt_manager.format_prompt(self.name, **params) if self.name else self.format(**params) + + def _prepare_s4u_params(self, context_data: Dict[str, Any]) -> Dict[str, Any]: + """准备S4U模式的参数""" + return { + **context_data, + "expression_habits_block": context_data.get("expression_habits_block", ""), + "tool_info_block": context_data.get("tool_info_block", ""), + "knowledge_prompt": context_data.get("knowledge_prompt", ""), + "memory_block": context_data.get("memory_block", ""), + "relation_info_block": context_data.get("relation_info_block", ""), + "extra_info_block": self.parameters.extra_info_block or context_data.get("extra_info_block", ""), + "cross_context_block": context_data.get("cross_context_block", ""), + "identity": self.parameters.identity_block or context_data.get("identity", ""), + "action_descriptions": self.parameters.action_descriptions or context_data.get("action_descriptions", ""), + "sender_name": self.parameters.sender, + "mood_state": self.parameters.mood_prompt or context_data.get("mood_state", ""), + "background_dialogue_prompt": context_data.get("background_dialogue_prompt", ""), + "time_block": context_data.get("time_block", ""), + "core_dialogue_prompt": context_data.get("core_dialogue_prompt", ""), + "reply_target_block": context_data.get("reply_target_block", ""), + "reply_style": global_config.personality.reply_style, + "keywords_reaction_prompt": self.parameters.keywords_reaction_prompt or context_data.get("keywords_reaction_prompt", ""), + "moderation_prompt": self.parameters.moderation_prompt_block or context_data.get("moderation_prompt", ""), + } + + def _prepare_normal_params(self, context_data: Dict[str, Any]) -> Dict[str, Any]: + """准备Normal模式的参数""" + return { + **context_data, + "expression_habits_block": context_data.get("expression_habits_block", ""), + "tool_info_block": context_data.get("tool_info_block", ""), + "knowledge_prompt": context_data.get("knowledge_prompt", ""), + "memory_block": context_data.get("memory_block", ""), + "relation_info_block": context_data.get("relation_info_block", ""), + "extra_info_block": self.parameters.extra_info_block or context_data.get("extra_info_block", ""), + "cross_context_block": context_data.get("cross_context_block", ""), + "identity": self.parameters.identity_block or context_data.get("identity", ""), + "action_descriptions": self.parameters.action_descriptions or context_data.get("action_descriptions", ""), + "schedule_block": self.parameters.schedule_block or context_data.get("schedule_block", ""), + "time_block": context_data.get("time_block", ""), + "chat_info": context_data.get("chat_info", ""), + "reply_target_block": context_data.get("reply_target_block", ""), + "config_expression_style": global_config.personality.reply_style, + "mood_state": self.parameters.mood_prompt or context_data.get("mood_state", ""), + "keywords_reaction_prompt": self.parameters.keywords_reaction_prompt or context_data.get("keywords_reaction_prompt", ""), + "moderation_prompt": self.parameters.moderation_prompt_block or context_data.get("moderation_prompt", ""), + } + + def _prepare_default_params(self, context_data: Dict[str, Any]) -> Dict[str, Any]: + """准备默认模式的参数""" + return { + "expression_habits_block": context_data.get("expression_habits_block", ""), + "relation_info_block": context_data.get("relation_info_block", ""), + "chat_target": "", + "time_block": context_data.get("time_block", ""), + "chat_info": context_data.get("chat_info", ""), + "identity": self.parameters.identity_block or context_data.get("identity", ""), + "chat_target_2": "", + "reply_target_block": context_data.get("reply_target_block", ""), + "raw_reply": self.parameters.target, + "reason": "", + "mood_state": self.parameters.mood_prompt or context_data.get("mood_state", ""), + "reply_style": global_config.personality.reply_style, + "keywords_reaction_prompt": self.parameters.keywords_reaction_prompt or context_data.get("keywords_reaction_prompt", ""), + "moderation_prompt": self.parameters.moderation_prompt_block or context_data.get("moderation_prompt", ""), + } + + def format(self, *args, **kwargs) -> str: + """格式化模板,支持位置参数和关键字参数""" + try: + # 先用位置参数格式化 + if args: + formatted_args = {} + for i in range(len(args)): + if i < len(self.args): + formatted_args[self.args[i]] = args[i] + processed_template = self._processed_template.format(**formatted_args) + else: + processed_template = self._processed_template + + # 再用关键字参数格式化 + if kwargs: + processed_template = processed_template.format(**kwargs) + + # 将临时标记还原为实际的花括号 + result = self._restore_escaped_braces(processed_template) + return result + except (IndexError, KeyError) as e: + raise ValueError(f"格式化模板失败: {self.template}, args={args}, kwargs={kwargs} {str(e)}") from e + + def __str__(self) -> str: + """返回格式化后的结果或原始模板""" + return self._formatted_result if self._formatted_result else self.template + + def __repr__(self) -> str: + """返回提示词的表示形式""" + return f"Prompt(template='{self.template}', name='{self.name}')" + + +# 工厂函数 +def create_prompt( + template: str, + name: Optional[str] = None, + parameters: Optional[PromptParameters] = None, + **kwargs +) -> Prompt: + """快速创建Prompt实例的工厂函数""" + if parameters is None: + parameters = PromptParameters(**kwargs) + return Prompt(template, name, parameters) + + +async def create_prompt_async( + template: str, + name: Optional[str] = None, + parameters: Optional[PromptParameters] = None, + **kwargs +) -> Prompt: + """异步创建Prompt实例""" + prompt = create_prompt(template, name, parameters, **kwargs) + if global_prompt_manager._context._current_context: + await global_prompt_manager._context.register_async(prompt) + return prompt \ No newline at end of file diff --git a/src/chat/utils/prompt_builder.py b/src/chat/utils/prompt_builder.py deleted file mode 100644 index 3585b5959..000000000 --- a/src/chat/utils/prompt_builder.py +++ /dev/null @@ -1,299 +0,0 @@ -import re -import asyncio -import contextvars - -from rich.traceback import install -from contextlib import asynccontextmanager -from typing import Dict, Any, Optional, List, Union - -from src.common.logger import get_logger - -install(extra_lines=3) - -logger = get_logger("prompt_build") - - -class PromptContext: - def __init__(self): - self._context_prompts: Dict[str, Dict[str, "Prompt"]] = {} - # 使用contextvars创建协程上下文变量 - self._current_context_var = contextvars.ContextVar("current_context", default=None) - self._context_lock = asyncio.Lock() # 保留锁用于其他操作 - - @property - def _current_context(self) -> Optional[str]: - """获取当前协程的上下文ID""" - return self._current_context_var.get() - - @_current_context.setter - def _current_context(self, value: Optional[str]): - """设置当前协程的上下文ID""" - self._current_context_var.set(value) # type: ignore - - @asynccontextmanager - async def async_scope(self, context_id: Optional[str] = None): - # sourcery skip: hoist-statement-from-if, use-contextlib-suppress - """创建一个异步的临时提示模板作用域""" - # 保存当前上下文并设置新上下文 - if context_id is not None: - try: - # 添加超时保护,避免长时间等待锁 - await asyncio.wait_for(self._context_lock.acquire(), timeout=5.0) - try: - if context_id not in self._context_prompts: - self._context_prompts[context_id] = {} - finally: - self._context_lock.release() - except asyncio.TimeoutError: - logger.warning(f"获取上下文锁超时,context_id: {context_id}") - # 超时时直接进入,不设置上下文 - context_id = None - - # 保存当前协程的上下文值,不影响其他协程 - previous_context = self._current_context - # 设置当前协程的新上下文 - token = self._current_context_var.set(context_id) if context_id else None # type: ignore - else: - # 如果没有提供新上下文,保持当前上下文不变 - previous_context = self._current_context - token = None - - try: - yield self - finally: - # 恢复之前的上下文,添加异常保护 - if context_id is not None and token is not None: - try: - self._current_context_var.reset(token) - except Exception as e: - logger.warning(f"恢复上下文时出错: {e}") - # 如果reset失败,尝试直接设置 - try: - self._current_context = previous_context - except Exception: - ... - # 静默忽略恢复失败 - - async def get_prompt_async(self, name: str) -> Optional["Prompt"]: - """异步获取当前作用域中的提示模板""" - async with self._context_lock: - current_context = self._current_context - logger.debug(f"获取提示词: {name} 当前上下文: {current_context}") - if ( - current_context - and current_context in self._context_prompts - and name in self._context_prompts[current_context] - ): - return self._context_prompts[current_context][name] - return None - - async def register_async(self, prompt: "Prompt", context_id: Optional[str] = None) -> None: - """异步注册提示模板到指定作用域""" - async with self._context_lock: - if target_context := context_id or self._current_context: - if prompt.name: - self._context_prompts.setdefault(target_context, {})[prompt.name] = prompt - - -class PromptManager: - def __init__(self): - self._prompts = {} - self._counter = 0 - self._context = PromptContext() - self._lock = asyncio.Lock() - - @asynccontextmanager - async def async_message_scope(self, message_id: Optional[str] = None): - """为消息处理创建异步临时作用域,支持 message_id 为 None 的情况""" - async with self._context.async_scope(message_id): - yield self - - async def get_prompt_async(self, name: str) -> "Prompt": - # 首先尝试从当前上下文获取 - context_prompt = await self._context.get_prompt_async(name) - if context_prompt is not None: - logger.debug(f"从上下文中获取提示词: {name} {context_prompt}") - return context_prompt - # 如果上下文中不存在,则使用全局提示模板 - async with self._lock: - # logger.debug(f"从全局获取提示词: {name}") - if name not in self._prompts: - raise KeyError(f"Prompt '{name}' not found") - return self._prompts[name] - - def generate_name(self, template: str) -> str: - """为未命名的prompt生成名称""" - self._counter += 1 - return f"prompt_{self._counter}" - - def register(self, prompt: "Prompt") -> None: - """注册一个prompt""" - if not prompt.name: - prompt.name = self.generate_name(prompt.template) - self._prompts[prompt.name] = prompt - - def add_prompt(self, name: str, fstr: str) -> "Prompt": - prompt = Prompt(fstr, name=name) - if prompt.name: - self._prompts[prompt.name] = prompt - return prompt - - async def format_prompt(self, name: str, **kwargs) -> str: - # 获取当前提示词 - prompt = await self.get_prompt_async(name) - # 获取基本格式化结果 - result = prompt.format(**kwargs) - return result - - -# 全局单例 -global_prompt_manager = PromptManager() - - -class Prompt(str): - template: str - name: Optional[str] - args: List[str] - _args: List[Any] - _kwargs: Dict[str, Any] - # 临时标记,作为类常量 - _TEMP_LEFT_BRACE = "__ESCAPED_LEFT_BRACE__" - _TEMP_RIGHT_BRACE = "__ESCAPED_RIGHT_BRACE__" - - @staticmethod - def _process_escaped_braces(template) -> str: - """处理模板中的转义花括号,将 \\{ 和 \\} 替换为临时标记""" # type: ignore - # 如果传入的是列表,将其转换为字符串 - if isinstance(template, list): - template = "\n".join(str(item) for item in template) - elif not isinstance(template, str): - template = str(template) - - return template.replace("\\{", Prompt._TEMP_LEFT_BRACE).replace("\\}", Prompt._TEMP_RIGHT_BRACE) - - @staticmethod - def _restore_escaped_braces(template: str) -> str: - """将临时标记还原为实际的花括号字符""" - return template.replace(Prompt._TEMP_LEFT_BRACE, "{").replace(Prompt._TEMP_RIGHT_BRACE, "}") - - def __new__( - cls, fstr, name: Optional[str] = None, args: Optional[Union[List[Any], tuple[Any, ...]]] = None, **kwargs - ): - # 如果传入的是元组,转换为列表 - if isinstance(args, tuple): - args = list(args) - should_register = kwargs.pop("_should_register", True) - - # 预处理模板中的转义花括号 - processed_fstr = cls._process_escaped_braces(fstr) - - # 解析模板 - template_args = [] - result = re.findall(r"\{(.*?)}", processed_fstr) - for expr in result: - if expr and expr not in template_args: - template_args.append(expr) - - # 如果提供了初始参数,立即格式化 - if kwargs or args: - formatted = cls._format_template(fstr, args=args, kwargs=kwargs) - obj = super().__new__(cls, formatted) - else: - obj = super().__new__(cls, "") - - obj.template = fstr - obj.name = name - obj.args = template_args - obj._args = args or [] - obj._kwargs = kwargs - - # 修改自动注册逻辑 - if should_register and not global_prompt_manager._context._current_context: - global_prompt_manager.register(obj) - return obj - - @classmethod - async def create_async( - cls, fstr, name: Optional[str] = None, args: Optional[Union[List[Any], tuple[Any, ...]]] = None, **kwargs - ): - """异步创建Prompt实例""" - prompt = cls(fstr, name, args, **kwargs) - if global_prompt_manager._context._current_context: - await global_prompt_manager._context.register_async(prompt) - return prompt - - @classmethod - def _format_template( - cls, template, args: Optional[List[Any]] = None, kwargs: Optional[Dict[str, Any]] = None - ) -> str: - if kwargs is None: - kwargs = {} - # 预处理模板中的转义花括号 - processed_template = cls._process_escaped_braces(template) - - template_args = [] - result = re.findall(r"\{(.*?)}", processed_template) - for expr in result: - if expr and expr not in template_args: - template_args.append(expr) - formatted_args = {} - formatted_kwargs = {} - - # 处理位置参数 - if args: - # print(len(template_args), len(args), template_args, args) - for i in range(len(args)): - if i < len(template_args): - arg = args[i] - if isinstance(arg, Prompt): - formatted_args[template_args[i]] = arg.format(**kwargs) - else: - formatted_args[template_args[i]] = arg - else: - logger.error( - f"构建提示词模板失败,解析到的参数列表{template_args},长度为{len(template_args)},输入的参数列表为{args},提示词模板为{template}" - ) - raise ValueError("格式化模板失败") - - # 处理关键字参数 - if kwargs: - for key, value in kwargs.items(): - if isinstance(value, Prompt): - remaining_kwargs = {k: v for k, v in kwargs.items() if k != key} - formatted_kwargs[key] = value.format(**remaining_kwargs) - else: - formatted_kwargs[key] = value - - try: - # 先用位置参数格式化 - if args: - processed_template = processed_template.format(**formatted_args) - # 再用关键字参数格式化 - if kwargs: - processed_template = processed_template.format(**formatted_kwargs) - - # 将临时标记还原为实际的花括号 - result = cls._restore_escaped_braces(processed_template) - return result - except (IndexError, KeyError) as e: - raise ValueError( - f"格式化模板失败: {template}, args={formatted_args}, kwargs={formatted_kwargs} {str(e)}" - ) from e - - def format(self, *args, **kwargs) -> "str": - """支持位置参数和关键字参数的格式化,使用""" - ret = type(self)( - self.template, - self.name, - args=list(args) if args else self._args, - _should_register=False, - **kwargs or self._kwargs, - ) - # print(f"prompt build result: {ret} name: {ret.name} ") - return str(ret) - - def __str__(self) -> str: - return super().__str__() if self._kwargs or self._args else self.template - - def __repr__(self) -> str: - return f"Prompt(template='{self.template}', name='{self.name}')" diff --git a/src/chat/utils/prompt_parameters.py b/src/chat/utils/prompt_parameters.py deleted file mode 100644 index 2558917d4..000000000 --- a/src/chat/utils/prompt_parameters.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -智能提示词参数模块 - 优化参数结构 -简化SmartPromptParameters,减少冗余和重复 -""" - -from dataclasses import dataclass, field -from typing import Dict, Any, Optional, List, Literal - - -@dataclass -class SmartPromptParameters: - """简化的智能提示词参数系统""" - - # 基础参数 - chat_id: str = "" - is_group_chat: bool = False - sender: str = "" - target: str = "" - reply_to: str = "" - extra_info: str = "" - prompt_mode: Literal["s4u", "normal", "minimal"] = "s4u" - - # 功能开关 - enable_tool: bool = True - enable_memory: bool = True - enable_expression: bool = True - enable_relation: bool = True - enable_cross_context: bool = True - enable_knowledge: bool = True - - # 性能控制 - max_context_messages: int = 50 - - # 调试选项 - debug_mode: bool = False - - # 聊天历史和上下文 - chat_target_info: Optional[Dict[str, Any]] = None - message_list_before_now_long: List[Dict[str, Any]] = field(default_factory=list) - message_list_before_short: List[Dict[str, Any]] = field(default_factory=list) - chat_talking_prompt_short: str = "" - target_user_info: Optional[Dict[str, Any]] = None - - # 已构建的内容块 - expression_habits_block: str = "" - relation_info_block: str = "" - memory_block: str = "" - tool_info_block: str = "" - knowledge_prompt: str = "" - cross_context_block: str = "" - - # 其他内容块 - keywords_reaction_prompt: str = "" - extra_info_block: str = "" - time_block: str = "" - identity_block: str = "" - schedule_block: str = "" - moderation_prompt_block: str = "" - reply_target_block: str = "" - mood_prompt: str = "" - action_descriptions: str = "" - - # 可用动作信息 - available_actions: Optional[Dict[str, Any]] = None - - def validate(self) -> List[str]: - """统一的参数验证""" - errors = [] - if not self.chat_id: - errors.append("chat_id不能为空") - if self.prompt_mode not in ["s4u", "normal", "minimal"]: - errors.append("prompt_mode必须是's4u'、'normal'或'minimal'") - if self.max_context_messages <= 0: - errors.append("max_context_messages必须大于0") - return errors - - def get_needed_build_tasks(self) -> List[str]: - """获取需要执行的任务列表""" - tasks = [] - - if self.enable_expression and not self.expression_habits_block: - tasks.append("expression_habits") - - if self.enable_memory and not self.memory_block: - tasks.append("memory_block") - - if self.enable_relation and not self.relation_info_block: - tasks.append("relation_info") - - if self.enable_tool and not self.tool_info_block: - tasks.append("tool_info") - - if self.enable_knowledge and not self.knowledge_prompt: - tasks.append("knowledge_info") - - if self.enable_cross_context and not self.cross_context_block: - tasks.append("cross_context") - - return tasks - - @classmethod - def from_legacy_params(cls, **kwargs) -> "SmartPromptParameters": - """ - 从旧版参数创建新参数对象 - - Args: - **kwargs: 旧版参数 - - Returns: - SmartPromptParameters: 新参数对象 - """ - return cls( - # 基础参数 - chat_id=kwargs.get("chat_id", ""), - is_group_chat=kwargs.get("is_group_chat", False), - sender=kwargs.get("sender", ""), - target=kwargs.get("target", ""), - reply_to=kwargs.get("reply_to", ""), - extra_info=kwargs.get("extra_info", ""), - prompt_mode=kwargs.get("current_prompt_mode", "s4u"), - # 功能开关 - enable_tool=kwargs.get("enable_tool", True), - enable_memory=kwargs.get("enable_memory", True), - enable_expression=kwargs.get("enable_expression", True), - enable_relation=kwargs.get("enable_relation", True), - enable_cross_context=kwargs.get("enable_cross_context", True), - enable_knowledge=kwargs.get("enable_knowledge", True), - # 性能控制 - max_context_messages=kwargs.get("max_context_messages", 50), - debug_mode=kwargs.get("debug_mode", False), - # 聊天历史和上下文 - chat_target_info=kwargs.get("chat_target_info"), - message_list_before_now_long=kwargs.get("message_list_before_now_long", []), - message_list_before_short=kwargs.get("message_list_before_short", []), - chat_talking_prompt_short=kwargs.get("chat_talking_prompt_short", ""), - target_user_info=kwargs.get("target_user_info"), - # 已构建的内容块 - expression_habits_block=kwargs.get("expression_habits_block", ""), - relation_info_block=kwargs.get("relation_info", ""), - memory_block=kwargs.get("memory_block", ""), - tool_info_block=kwargs.get("tool_info", ""), - knowledge_prompt=kwargs.get("knowledge_prompt", ""), - cross_context_block=kwargs.get("cross_context_block", ""), - # 其他内容块 - keywords_reaction_prompt=kwargs.get("keywords_reaction_prompt", ""), - extra_info_block=kwargs.get("extra_info_block", ""), - time_block=kwargs.get("time_block", ""), - identity_block=kwargs.get("identity_block", ""), - schedule_block=kwargs.get("schedule_block", ""), - moderation_prompt_block=kwargs.get("moderation_prompt_block", ""), - reply_target_block=kwargs.get("reply_target_block", ""), - mood_prompt=kwargs.get("mood_prompt", ""), - action_descriptions=kwargs.get("action_descriptions", ""), - # 可用动作信息 - available_actions=kwargs.get("available_actions", None), - ) diff --git a/src/chat/utils/prompt_utils.py b/src/chat/utils/prompt_utils.py index f9985be53..a5bb931dd 100644 --- a/src/chat/utils/prompt_utils.py +++ b/src/chat/utils/prompt_utils.py @@ -1,6 +1,6 @@ """ 共享提示词工具模块 - 消除重复代码 -提供统一的工具函数供DefaultReplyer和SmartPrompt使用 +提供统一的工具函数供DefaultReplyer和统一Prompt系统使用 """ import re diff --git a/src/chat/utils/smart_prompt.py b/src/chat/utils/smart_prompt.py deleted file mode 100644 index aba79f7ec..000000000 --- a/src/chat/utils/smart_prompt.py +++ /dev/null @@ -1,938 +0,0 @@ -""" -智能Prompt系统 - 完全重构版本 -基于原有DefaultReplyer的完整功能集成,使用新的参数结构 -解决实现质量不高、功能集成不完整和错误处理不足的问题 -""" - -import asyncio -import time -from datetime import datetime -from dataclasses import dataclass, field -from typing import Dict, Any, Optional, List, Tuple - -from src.chat.utils.prompt_builder import global_prompt_manager, Prompt -from src.common.logger import get_logger -from src.config.config import global_config -from src.chat.utils.chat_message_builder import ( - build_readable_messages, -) -from src.person_info.person_info import get_person_info_manager -from src.chat.utils.prompt_utils import PromptUtils -from src.chat.utils.prompt_parameters import SmartPromptParameters - -logger = get_logger("smart_prompt") - - -@dataclass -class ChatContext: - """聊天上下文信息""" - - chat_id: str = "" - platform: str = "" - is_group: bool = False - user_id: str = "" - user_nickname: str = "" - group_id: Optional[str] = None - timestamp: datetime = field(default_factory=datetime.now) - - -class SmartPromptBuilder: - """重构的智能提示词构建器 - 统一错误处理和功能集成,移除缓存机制和依赖检查""" - - def __init__(self): - # 移除缓存相关初始化 - pass - - async def build_context_data(self, params: SmartPromptParameters) -> Dict[str, Any]: - """并行构建完整的上下文数据 - 移除缓存机制和依赖检查""" - - # 并行执行所有构建任务 - start_time = time.time() - timing_logs = {} - - try: - # 准备构建任务 - tasks = [] - task_names = [] - - # 初始化预构建参数,使用新的结构 - pre_built_params = {} - if params.expression_habits_block: - pre_built_params["expression_habits_block"] = params.expression_habits_block - if params.relation_info_block: - pre_built_params["relation_info_block"] = params.relation_info_block - if params.memory_block: - pre_built_params["memory_block"] = params.memory_block - if params.tool_info_block: - pre_built_params["tool_info_block"] = params.tool_info_block - if params.knowledge_prompt: - pre_built_params["knowledge_prompt"] = params.knowledge_prompt - if params.cross_context_block: - pre_built_params["cross_context_block"] = params.cross_context_block - - # 根据新的参数结构确定要构建的项 - if params.enable_expression and not pre_built_params.get("expression_habits_block"): - tasks.append(self._build_expression_habits(params)) - task_names.append("expression_habits") - - if params.enable_memory and not pre_built_params.get("memory_block"): - tasks.append(self._build_memory_block(params)) - task_names.append("memory_block") - - if params.enable_relation and not pre_built_params.get("relation_info_block"): - tasks.append(self._build_relation_info(params)) - task_names.append("relation_info") - - # 添加mai_think上下文构建任务 - if not pre_built_params.get("mai_think"): - tasks.append(self._build_mai_think_context(params)) - task_names.append("mai_think_context") - - if params.enable_tool and not pre_built_params.get("tool_info_block"): - tasks.append(self._build_tool_info(params)) - task_names.append("tool_info") - - if params.enable_knowledge and not pre_built_params.get("knowledge_prompt"): - tasks.append(self._build_knowledge_info(params)) - task_names.append("knowledge_info") - - if params.enable_cross_context and not pre_built_params.get("cross_context_block"): - tasks.append(self._build_cross_context(params)) - task_names.append("cross_context") - - # 性能优化:根据任务数量动态调整超时时间 - base_timeout = 10.0 # 基础超时时间 - task_timeout = 2.0 # 每个任务的超时时间 - timeout_seconds = min( - max(base_timeout, len(tasks) * task_timeout), # 根据任务数量计算超时 - 30.0, # 最大超时时间 - ) - - # 性能优化:限制并发任务数量,避免资源耗尽 - max_concurrent_tasks = 5 # 最大并发任务数 - if len(tasks) > max_concurrent_tasks: - # 分批执行任务 - results = [] - for i in range(0, len(tasks), max_concurrent_tasks): - batch_tasks = tasks[i : i + max_concurrent_tasks] - batch_names = task_names[i : i + max_concurrent_tasks] - - batch_results = await asyncio.wait_for( - asyncio.gather(*batch_tasks, return_exceptions=True), timeout=timeout_seconds - ) - results.extend(batch_results) - else: - # 一次性执行所有任务 - results = await asyncio.wait_for( - asyncio.gather(*tasks, return_exceptions=True), timeout=timeout_seconds - ) - - # 处理结果并收集性能数据 - context_data = {} - for i, result in enumerate(results): - task_name = task_names[i] if i < len(task_names) else f"task_{i}" - - if isinstance(result, Exception): - logger.error(f"构建任务{task_name}失败: {str(result)}") - elif isinstance(result, dict): - # 结果格式: {component_name: value} - context_data.update(result) - - # 记录耗时过长的任务 - if task_name in timing_logs and timing_logs[task_name] > 8.0: - logger.warning(f"构建任务{task_name}耗时过长: {timing_logs[task_name]:.2f}s") - - # 添加预构建的参数 - for key, value in pre_built_params.items(): - if value: - context_data[key] = value - - except asyncio.TimeoutError: - logger.error(f"构建超时 ({timeout_seconds}s)") - context_data = {} - - # 添加预构建的参数,即使在超时情况下 - for key, value in pre_built_params.items(): - if value: - context_data[key] = value - - # 构建聊天历史 - 根据模式不同 - if params.prompt_mode == "s4u": - await self._build_s4u_chat_context(context_data, params) - else: - await self._build_normal_chat_context(context_data, params) - - # 补充基础信息 - context_data.update( - { - "keywords_reaction_prompt": params.keywords_reaction_prompt, - "extra_info_block": params.extra_info_block, - "time_block": params.time_block or f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", - "identity": params.identity_block, - "schedule_block": params.schedule_block, - "moderation_prompt": params.moderation_prompt_block, - "reply_target_block": params.reply_target_block, - "mood_state": params.mood_prompt, - "action_descriptions": params.action_descriptions, - } - ) - - total_time = time.time() - start_time - if timing_logs: - timing_str = "; ".join([f"{name}: {time:.2f}s" for name, time in timing_logs.items()]) - logger.info(f"构建任务耗时: {timing_str}") - logger.debug(f"构建完成,总耗时: {total_time:.2f}s") - - return context_data - - async def _build_s4u_chat_context(self, context_data: Dict[str, Any], params: SmartPromptParameters) -> None: - """构建S4U模式的聊天上下文 - 使用新参数结构""" - if not params.message_list_before_now_long: - return - - # 使用共享工具构建分离历史 - core_dialogue, background_dialogue = await self._build_s4u_chat_history_prompts( - params.message_list_before_now_long, - params.target_user_info.get("user_id") if params.target_user_info else "", - params.sender - ) - - context_data["core_dialogue_prompt"] = core_dialogue - context_data["background_dialogue_prompt"] = background_dialogue - - async def _build_normal_chat_context(self, context_data: Dict[str, Any], params: SmartPromptParameters) -> None: - """构建normal模式的聊天上下文 - 使用新参数结构""" - if not params.chat_talking_prompt_short: - return - - context_data["chat_info"] = f"""群里的聊天内容: -{params.chat_talking_prompt_short}""" - - async def _build_s4u_chat_history_prompts( - self, message_list_before_now: List[Dict[str, Any]], target_user_id: str, sender: str - ) -> Tuple[str, str]: - """构建S4U风格的分离对话prompt - 完整实现""" - core_dialogue_list = [] - bot_id = str(global_config.bot.qq_account) - - # 过滤消息:分离bot和目标用户的对话 vs 其他用户的对话 - for msg_dict in message_list_before_now: - try: - msg_user_id = str(msg_dict.get("user_id")) - reply_to = msg_dict.get("reply_to", "") - _platform, reply_to_user_id = self._parse_reply_target(reply_to) - if (msg_user_id == bot_id and reply_to_user_id == target_user_id) or msg_user_id == target_user_id: - # bot 和目标用户的对话 - core_dialogue_list.append(msg_dict) - except Exception as e: - logger.error(f"处理消息记录时出错: {msg_dict}, 错误: {e}") - - # 构建背景对话 prompt - all_dialogue_prompt = "" - if message_list_before_now: - latest_25_msgs = message_list_before_now[-int(global_config.chat.max_context_size) :] - all_dialogue_prompt_str = build_readable_messages( - latest_25_msgs, - replace_bot_name=True, - timestamp_mode="normal", - truncate=True, - ) - all_dialogue_prompt = f"所有用户的发言:\n{all_dialogue_prompt_str}" - - # 构建核心对话 prompt - core_dialogue_prompt = "" - if core_dialogue_list: - # 检查最新五条消息中是否包含bot自己说的消息 - latest_5_messages = core_dialogue_list[-5:] if len(core_dialogue_list) >= 5 else core_dialogue_list - has_bot_message = any(str(msg.get("user_id")) == bot_id for msg in latest_5_messages) - - # logger.info(f"最新五条消息:{latest_5_messages}") - # logger.info(f"最新五条消息中是否包含bot自己说的消息:{has_bot_message}") - - # 如果最新五条消息中不包含bot的消息,则返回空字符串 - if not has_bot_message: - core_dialogue_prompt = "" - else: - core_dialogue_list = core_dialogue_list[-int(global_config.chat.max_context_size * 2) :] # 限制消息数量 - - core_dialogue_prompt_str = build_readable_messages( - core_dialogue_list, - replace_bot_name=True, - merge_messages=False, - timestamp_mode="normal_no_YMD", - read_mark=0.0, - truncate=True, - show_actions=True, - ) - core_dialogue_prompt = f"""-------------------------------- -这是你和{sender}的对话,你们正在交流中: -{core_dialogue_prompt_str} --------------------------------- -""" - - return core_dialogue_prompt, all_dialogue_prompt - - async def _build_mai_think_context(self, params: SmartPromptParameters) -> Any: - """构建mai_think上下文 - 完全继承DefaultReplyer功能""" - from src.mais4u.mai_think import mai_thinking_manager - - # 获取mai_think实例 - mai_think = mai_thinking_manager.get_mai_think(params.chat_id) - - # 设置mai_think的上下文信息 - mai_think.memory_block = params.memory_block or "" - mai_think.relation_info_block = params.relation_info_block or "" - mai_think.time_block = params.time_block or f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - - # 设置聊天目标信息 - if params.is_group_chat: - chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1") - chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2") - else: - chat_target_name = "对方" - if params.chat_target_info: - chat_target_name = ( - params.chat_target_info.get("person_name") or params.chat_target_info.get("user_nickname") or "对方" - ) - chat_target_1 = await global_prompt_manager.format_prompt( - "chat_target_private1", sender_name=chat_target_name - ) - chat_target_2 = await global_prompt_manager.format_prompt( - "chat_target_private2", sender_name=chat_target_name - ) - - mai_think.chat_target = chat_target_1 - mai_think.chat_target_2 = chat_target_2 - mai_think.chat_info = params.chat_talking_prompt_short or "" - mai_think.mood_state = params.mood_prompt or "" - mai_think.identity = params.identity_block or "" - mai_think.sender = params.sender - mai_think.target = params.target - - # 返回mai_think实例,以便后续使用 - return mai_think - - def _parse_reply_target_id(self, reply_to: str) -> str: - """解析回复目标中的用户ID""" - if not reply_to: - return "" - - # 复用_parse_reply_target方法的逻辑 - sender, _ = self._parse_reply_target(reply_to) - if not sender: - return "" - - # 获取用户ID - person_info_manager = get_person_info_manager() - person_id = person_info_manager.get_person_id_by_person_name(sender) - if person_id: - user_id = person_info_manager.get_value_sync(person_id, "user_id") - return str(user_id) if user_id else "" - - async def _build_expression_habits(self, params: SmartPromptParameters) -> Dict[str, Any]: - """构建表达习惯 - 使用共享工具类,完全继承DefaultReplyer功能""" - # 检查是否允许在此聊天流中使用表达 - use_expression, _, _ = global_config.expression.get_expression_config_for_chat(params.chat_id) - if not use_expression: - return {"expression_habits_block": ""} - - from src.chat.express.expression_selector import expression_selector - - style_habits = [] - grammar_habits = [] - - # 使用从处理器传来的选中表达方式 - # LLM模式:调用LLM选择5-10个,然后随机选5个 - try: - selected_expressions = await expression_selector.select_suitable_expressions_llm( - params.chat_id, params.chat_talking_prompt_short, max_num=8, min_num=2, target_message=params.target - ) - except Exception as e: - logger.error(f"选择表达方式失败: {e}") - selected_expressions = [] - - if selected_expressions: - logger.debug(f"使用处理器选中的{len(selected_expressions)}个表达方式") - for expr in selected_expressions: - if isinstance(expr, dict) and "situation" in expr and "style" in expr: - expr_type = expr.get("type", "style") - if expr_type == "grammar": - grammar_habits.append(f"当{expr['situation']}时,使用 {expr['style']}") - else: - style_habits.append(f"当{expr['situation']}时,使用 {expr['style']}") - else: - logger.debug("没有从处理器获得表达方式,将使用空的表达方式") - # 不再在replyer中进行随机选择,全部交给处理器处理 - - style_habits_str = "\n".join(style_habits) - grammar_habits_str = "\n".join(grammar_habits) - - # 动态构建expression habits块 - expression_habits_block = "" - expression_habits_title = "" - if style_habits_str.strip(): - expression_habits_title = ( - "你可以参考以下的语言习惯,当情景合适就使用,但不要生硬使用,以合理的方式结合到你的回复中:" - ) - expression_habits_block += f"{style_habits_str}\n" - if grammar_habits_str.strip(): - expression_habits_title = ( - "你可以选择下面的句法进行回复,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式使用:" - ) - expression_habits_block += f"{grammar_habits_str}\n" - - if style_habits_str.strip() and grammar_habits_str.strip(): - expression_habits_title = "你可以参考以下的语言习惯和句法,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式结合到你的回复中。" - - return {"expression_habits_block": f"{expression_habits_title}\n{expression_habits_block}"} - - async def _build_memory_block(self, params: SmartPromptParameters) -> Dict[str, Any]: - """构建记忆块 - 使用共享工具类,完全继承DefaultReplyer功能""" - if not global_config.memory.enable_memory: - return {"memory_block": ""} - - from src.chat.memory_system.memory_activator import MemoryActivator - from src.chat.memory_system.vector_instant_memory import VectorInstantMemoryV2 - - instant_memory = None - - # 初始化记忆激活器 - try: - memory_activator = MemoryActivator() - - # 获取长期记忆 - running_memories = await memory_activator.activate_memory_with_chat_history( - target_message=params.target, chat_history_prompt=params.chat_talking_prompt_short - ) - except Exception as e: - logger.error(f"激活记忆失败: {e}") - running_memories = [] - - # 处理瞬时记忆 - if global_config.memory.enable_instant_memory: - # 使用异步记忆包装器(最优化的非阻塞模式) - try: - from src.chat.memory_system.async_instant_memory_wrapper import get_async_instant_memory - - # 获取异步记忆包装器 - async_memory = get_async_instant_memory(params.chat_id) - - # 后台存储聊天历史(完全非阻塞) - async_memory.store_memory_background(params.chat_talking_prompt_short) - - # 快速检索记忆,最大超时2秒 - instant_memory = await async_memory.get_memory_with_fallback(params.target, max_timeout=2.0) - - logger.info(f"异步瞬时记忆:{instant_memory}") - - except ImportError: - # 如果异步包装器不可用,尝试使用异步记忆管理器 - try: - from src.chat.memory_system.async_memory_optimizer import ( - retrieve_memory_nonblocking, - store_memory_nonblocking, - ) - - # 异步存储聊天历史(非阻塞) - asyncio.create_task( - store_memory_nonblocking(chat_id=params.chat_id, content=params.chat_talking_prompt_short) - ) - - # 尝试从缓存获取瞬时记忆 - instant_memory = await retrieve_memory_nonblocking(chat_id=params.chat_id, query=params.target) - - # 如果没有缓存结果,快速检索一次 - if instant_memory is None: - try: - # 使用VectorInstantMemoryV2实例 - instant_memory_system = VectorInstantMemoryV2(chat_id=params.chat_id, retention_hours=1) - instant_memory = await asyncio.wait_for( - instant_memory_system.get_memory_for_context(params.target), timeout=1.5 - ) - except asyncio.TimeoutError: - logger.warning("瞬时记忆检索超时,使用空结果") - instant_memory = "" - - logger.info(f"向量瞬时记忆:{instant_memory}") - - except ImportError: - # 最后的fallback:使用原有逻辑但加上超时控制 - logger.warning("异步记忆系统不可用,使用带超时的同步方式") - - # 使用VectorInstantMemoryV2实例 - instant_memory_system = VectorInstantMemoryV2(chat_id=params.chat_id, retention_hours=1) - - # 异步存储聊天历史 - asyncio.create_task(instant_memory_system.store_message(params.chat_talking_prompt_short)) - - # 带超时的记忆检索 - try: - instant_memory = await asyncio.wait_for( - instant_memory_system.get_memory_for_context(params.target), - timeout=1.0, # 最保守的1秒超时 - ) - except asyncio.TimeoutError: - logger.warning("瞬时记忆检索超时,跳过记忆获取") - instant_memory = "" - except Exception as e: - logger.error(f"瞬时记忆检索失败: {e}") - instant_memory = "" - - logger.info(f"同步瞬时记忆:{instant_memory}") - - except Exception as e: - logger.error(f"瞬时记忆系统异常: {e}") - instant_memory = "" - - # 构建记忆字符串,即使某种记忆为空也要继续 - memory_str = "" - has_any_memory = False - - # 添加长期记忆 - if running_memories: - if not memory_str: - memory_str = "以下是当前在聊天中,你回忆起的记忆:\n" - for running_memory in running_memories: - memory_str += f"- {running_memory['content']}\n" - has_any_memory = True - - # 添加瞬时记忆 - if instant_memory: - if not memory_str: - memory_str = "以下是当前在聊天中,你回忆起的记忆:\n" - memory_str += f"- {instant_memory}\n" - has_any_memory = True - - # 注入视频分析结果引导语 - memory_str = self._inject_video_prompt_if_needed(params.target, memory_str) - - # 只有当完全没有任何记忆时才返回空字符串 - return {"memory_block": memory_str if has_any_memory else ""} - - def _inject_video_prompt_if_needed(self, target: str, memory_str: str) -> str: - """统一视频分析结果注入逻辑""" - if target and ("[视频内容]" in target or "好的,我将根据您提供的" in target): - video_prompt_injection = ( - "\n请注意,以上内容是你刚刚观看的视频,请以第一人称分享你的观后感,而不是在分析一份报告。" - ) - return memory_str + video_prompt_injection - return memory_str - - async def _build_relation_info(self, params: SmartPromptParameters) -> Dict[str, Any]: - """构建关系信息 - 使用共享工具类""" - try: - relation_info = await PromptUtils.build_relation_info(params.chat_id, params.reply_to) - return {"relation_info_block": relation_info} - except Exception as e: - logger.error(f"构建关系信息失败: {e}") - return {"relation_info_block": ""} - - async def _build_tool_info(self, params: SmartPromptParameters) -> Dict[str, Any]: - """构建工具信息 - 使用共享工具类,完全继承DefaultReplyer功能""" - if not params.enable_tool: - return {"tool_info_block": ""} - - if not params.reply_to: - return {"tool_info_block": ""} - - sender, text = PromptUtils.parse_reply_target(params.reply_to) - - if not text: - return {"tool_info_block": ""} - - from src.plugin_system.core.tool_use import ToolExecutor - - # 使用工具执行器获取信息 - try: - tool_executor = ToolExecutor(chat_id=params.chat_id) - tool_results, _, _ = await tool_executor.execute_from_chat_message( - sender=sender, target_message=text, chat_history=params.chat_talking_prompt_short, return_details=False - ) - - if tool_results: - tool_info_str = "以下是你通过工具获取到的实时信息:\n" - for tool_result in tool_results: - tool_name = tool_result.get("tool_name", "unknown") - content = tool_result.get("content", "") - result_type = tool_result.get("type", "tool_result") - - tool_info_str += f"- 【{tool_name}】{result_type}: {content}\n" - - tool_info_str += "以上是你获取到的实时信息,请在回复时参考这些信息。" - logger.info(f"获取到 {len(tool_results)} 个工具结果") - - return {"tool_info_block": tool_info_str} - else: - logger.debug("未获取到任何工具结果") - return {"tool_info_block": ""} - - except Exception as e: - logger.error(f"工具信息获取失败: {e}") - return {"tool_info_block": ""} - - async def _build_knowledge_info(self, params: SmartPromptParameters) -> Dict[str, Any]: - """构建知识信息 - 使用共享工具类,完全继承DefaultReplyer功能""" - if not params.reply_to: - logger.debug("没有回复对象,跳过获取知识库内容") - return {"knowledge_prompt": ""} - - sender, content = PromptUtils.parse_reply_target(params.reply_to) - if not content: - logger.debug("回复对象内容为空,跳过获取知识库内容") - return {"knowledge_prompt": ""} - - logger.debug( - f"获取知识库内容,元消息:{params.chat_talking_prompt_short[:30]}...,消息长度: {len(params.chat_talking_prompt_short)}" - ) - - # 从LPMM知识库获取知识 - try: - # 检查LPMM知识库是否启用 - if not global_config.lpmm_knowledge.enable: - logger.debug("LPMM知识库未启用,跳过获取知识库内容") - return {"knowledge_prompt": ""} - - from src.plugins.built_in.knowledge.lpmm_get_knowledge import SearchKnowledgeFromLPMMTool - from src.plugin_system.apis import llm_api - from src.config.config import model_config - - time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) - bot_name = global_config.bot.nickname - - prompt = await global_prompt_manager.format_prompt( - "lpmm_get_knowledge_prompt", - bot_name=bot_name, - time_now=time_now, - chat_history=params.chat_talking_prompt_short, - sender=sender, - target_message=content, - ) - - _, _, _, _, tool_calls = await llm_api.generate_with_model_with_tools( - prompt, - model_config=model_config.model_task_config.tool_use, - tool_options=[SearchKnowledgeFromLPMMTool.get_tool_definition()], - ) - - if tool_calls: - from src.plugin_system.core.tool_use import ToolExecutor - - tool_executor = ToolExecutor(chat_id=params.chat_id) - result = await tool_executor.execute_tool_call(tool_calls[0], SearchKnowledgeFromLPMMTool()) - - if not result or not result.get("content"): - logger.debug("从LPMM知识库获取知识失败,返回空知识...") - return {"knowledge_prompt": ""} - - found_knowledge_from_lpmm = result.get("content", "") - logger.debug( - f"从LPMM知识库获取知识,相关信息:{found_knowledge_from_lpmm[:100]}...,信息长度: {len(found_knowledge_from_lpmm)}" - ) - - return { - "knowledge_prompt": f"你有以下这些**知识**:\n{found_knowledge_from_lpmm}\n请你**记住上面的知识**,之后可能会用到。\n" - } - else: - logger.debug("从LPMM知识库获取知识失败,可能是从未导入过知识,返回空知识...") - return {"knowledge_prompt": ""} - - except Exception as e: - logger.error(f"获取知识库内容时发生异常: {str(e)}") - return {"knowledge_prompt": ""} - - async def _build_cross_context(self, params: SmartPromptParameters) -> Dict[str, Any]: - """构建跨群上下文 - 使用共享工具类""" - try: - cross_context = await PromptUtils.build_cross_context( - params.chat_id, params.prompt_mode, params.target_user_info - ) - return {"cross_context_block": cross_context} - except Exception as e: - logger.error(f"构建跨群上下文失败: {e}") - return {"cross_context_block": ""} - - def _parse_reply_target(self, target_message: str) -> Tuple[str, str]: - """解析回复目标消息 - 使用共享工具类""" - return PromptUtils.parse_reply_target(target_message) - - -class SmartPrompt: - """重构的智能提示词核心类 - 移除缓存机制和依赖检查,简化架构""" - - def __init__( - self, - template_name: Optional[str] = None, - parameters: Optional[SmartPromptParameters] = None, - ): - self.parameters = parameters or SmartPromptParameters() - self.template_name = template_name or self._get_default_template() - self.builder = SmartPromptBuilder() - - def _get_default_template(self) -> str: - """根据模式选择默认模板""" - if self.parameters.prompt_mode == "s4u": - return "s4u_style_prompt" - elif self.parameters.prompt_mode == "normal": - return "normal_style_prompt" - else: - return "default_expressor_prompt" - - async def build_prompt(self) -> str: - """构建最终的Prompt文本 - 移除缓存机制和依赖检查""" - # 参数验证 - errors = self.parameters.validate() - if errors: - logger.error(f"参数验证失败: {', '.join(errors)}") - raise ValueError(f"参数验证失败: {', '.join(errors)}") - - start_time = time.time() - try: - # 构建基础上下文的完整映射 - context_data = await self.builder.build_context_data(self.parameters) - - # 检查关键上下文数据 - if not context_data or not isinstance(context_data, dict): - logger.error("构建的上下文数据无效") - raise ValueError("构建的上下文数据无效") - - # 获取模板 - template = await self._get_template() - if template is None: - logger.error("无法获取模板") - raise ValueError("无法获取模板") - - # 根据模式传递不同的参数 - if self.parameters.prompt_mode == "s4u": - result = await self._build_s4u_prompt(template, context_data) - elif self.parameters.prompt_mode == "normal": - result = await self._build_normal_prompt(template, context_data) - else: - result = await self._build_default_prompt(template, context_data) - - # 记录性能数据 - total_time = time.time() - start_time - logger.debug(f"SmartPrompt构建完成,模式: {self.parameters.prompt_mode}, 耗时: {total_time:.2f}s") - - return result - - except asyncio.TimeoutError as e: - logger.error(f"构建Prompt超时: {e}") - raise TimeoutError(f"构建Prompt超时: {e}") - except Exception as e: - logger.error(f"构建Prompt失败: {e}") - raise RuntimeError(f"构建Prompt失败: {e}") - - async def _get_template(self) -> Optional[Prompt]: - """获取模板""" - try: - return await global_prompt_manager.get_prompt_async(self.template_name) - except Exception as e: - logger.error(f"获取模板 {self.template_name} 失败: {e}") - raise RuntimeError(f"获取模板 {self.template_name} 失败: {e}") - - async def _build_s4u_prompt(self, template: Prompt, context_data: Dict[str, Any]) -> str: - """构建S4U模式的完整Prompt - 使用新参数结构""" - params = { - **context_data, - "expression_habits_block": context_data.get("expression_habits_block", ""), - "tool_info_block": context_data.get("tool_info_block", ""), - "knowledge_prompt": context_data.get("knowledge_prompt", ""), - "memory_block": context_data.get("memory_block", ""), - "relation_info_block": context_data.get("relation_info_block", ""), - "extra_info_block": self.parameters.extra_info_block or context_data.get("extra_info_block", ""), - "cross_context_block": context_data.get("cross_context_block", ""), - "identity": self.parameters.identity_block or context_data.get("identity", ""), - "action_descriptions": self.parameters.action_descriptions or context_data.get("action_descriptions", ""), - "sender_name": self.parameters.sender, - "mood_state": self.parameters.mood_prompt or context_data.get("mood_state", ""), - "background_dialogue_prompt": context_data.get("background_dialogue_prompt", ""), - "time_block": context_data.get("time_block", ""), - "core_dialogue_prompt": context_data.get("core_dialogue_prompt", ""), - "reply_target_block": context_data.get("reply_target_block", ""), - "reply_style": global_config.personality.reply_style, - "keywords_reaction_prompt": self.parameters.keywords_reaction_prompt - or context_data.get("keywords_reaction_prompt", ""), - "moderation_prompt": self.parameters.moderation_prompt_block or context_data.get("moderation_prompt", ""), - } - return await global_prompt_manager.format_prompt(self.template_name, **params) - - async def _build_normal_prompt(self, template: Prompt, context_data: Dict[str, Any]) -> str: - """构建Normal模式的完整Prompt - 使用新参数结构""" - params = { - **context_data, - "expression_habits_block": context_data.get("expression_habits_block", ""), - "tool_info_block": context_data.get("tool_info_block", ""), - "knowledge_prompt": context_data.get("knowledge_prompt", ""), - "memory_block": context_data.get("memory_block", ""), - "relation_info_block": context_data.get("relation_info_block", ""), - "extra_info_block": self.parameters.extra_info_block or context_data.get("extra_info_block", ""), - "cross_context_block": context_data.get("cross_context_block", ""), - "identity": self.parameters.identity_block or context_data.get("identity", ""), - "action_descriptions": self.parameters.action_descriptions or context_data.get("action_descriptions", ""), - "schedule_block": self.parameters.schedule_block or context_data.get("schedule_block", ""), - "time_block": context_data.get("time_block", ""), - "chat_info": context_data.get("chat_info", ""), - "reply_target_block": context_data.get("reply_target_block", ""), - "config_expression_style": global_config.personality.reply_style, - "mood_state": self.parameters.mood_prompt or context_data.get("mood_state", ""), - "keywords_reaction_prompt": self.parameters.keywords_reaction_prompt - or context_data.get("keywords_reaction_prompt", ""), - "moderation_prompt": self.parameters.moderation_prompt_block or context_data.get("moderation_prompt", ""), - } - return await global_prompt_manager.format_prompt(self.template_name, **params) - - async def _build_default_prompt(self, template: Prompt, context_data: Dict[str, Any]) -> str: - """构建默认模式的Prompt - 使用新参数结构""" - params = { - "expression_habits_block": context_data.get("expression_habits_block", ""), - "relation_info_block": context_data.get("relation_info_block", ""), - "chat_target": "", - "time_block": context_data.get("time_block", ""), - "chat_info": context_data.get("chat_info", ""), - "identity": self.parameters.identity_block or context_data.get("identity", ""), - "chat_target_2": "", - "reply_target_block": context_data.get("reply_target_block", ""), - "raw_reply": self.parameters.target, - "reason": "", - "mood_state": self.parameters.mood_prompt or context_data.get("mood_state", ""), - "reply_style": global_config.personality.reply_style, - "keywords_reaction_prompt": self.parameters.keywords_reaction_prompt - or context_data.get("keywords_reaction_prompt", ""), - "moderation_prompt": self.parameters.moderation_prompt_block or context_data.get("moderation_prompt", ""), - } - return await global_prompt_manager.format_prompt(self.template_name, **params) - - -# 工厂函数 - 简化创建 - 更新参数结构 -def create_smart_prompt( - chat_id: str = "", sender_name: str = "", target_message: str = "", reply_to: str = "", **kwargs -) -> SmartPrompt: - """快速创建智能Prompt实例的工厂函数 - 使用新参数结构""" - - # 使用新的参数结构 - parameters = SmartPromptParameters( - chat_id=chat_id, sender=sender_name, target=target_message, reply_to=reply_to, **kwargs - ) - - return SmartPrompt(parameters=parameters) - - -class SmartPromptHealthChecker: - """SmartPrompt健康检查器 - 移除依赖检查""" - - @staticmethod - async def check_system_health() -> Dict[str, Any]: - """检查系统健康状态 - 移除依赖检查""" - health_status = {"status": "healthy", "components": {}, "issues": []} - - try: - # 检查配置 - try: - from src.config.config import global_config - - health_status["components"]["config"] = "ok" - - # 检查关键配置项 - if not hasattr(global_config, "personality") or not hasattr(global_config.personality, "prompt_mode"): - health_status["issues"].append("缺少personality.prompt_mode配置") - health_status["status"] = "degraded" - - if not hasattr(global_config, "memory") or not hasattr(global_config.memory, "enable_memory"): - health_status["issues"].append("缺少memory.enable_memory配置") - - except Exception as e: - health_status["components"]["config"] = f"failed: {str(e)}" - health_status["issues"].append("配置加载失败") - health_status["status"] = "unhealthy" - - # 检查Prompt模板 - try: - required_templates = ["s4u_style_prompt", "normal_style_prompt", "default_expressor_prompt"] - for template_name in required_templates: - try: - await global_prompt_manager.get_prompt_async(template_name) - health_status["components"][f"template_{template_name}"] = "ok" - except Exception as e: - health_status["components"][f"template_{template_name}"] = f"failed: {str(e)}" - health_status["issues"].append(f"模板{template_name}加载失败") - health_status["status"] = "degraded" - - except Exception as e: - health_status["components"]["prompt_templates"] = f"failed: {str(e)}" - health_status["issues"].append("Prompt模板检查失败") - health_status["status"] = "unhealthy" - - return health_status - - except Exception as e: - return {"status": "unhealthy", "components": {}, "issues": [f"健康检查异常: {str(e)}"]} - - @staticmethod - async def run_performance_test() -> Dict[str, Any]: - """运行性能测试""" - test_results = {"status": "completed", "tests": {}, "summary": {}} - - try: - # 创建测试参数 - test_params = SmartPromptParameters( - chat_id="test_chat", - sender="test_user", - target="test_message", - reply_to="test_user:test_message", - prompt_mode="s4u", - ) - - # 测试不同模式下的构建性能 - modes = ["s4u", "normal", "minimal"] - for mode in modes: - test_params.prompt_mode = mode - smart_prompt = SmartPrompt(parameters=test_params) - - # 运行多次测试取平均值 - times = [] - for _ in range(3): - start_time = time.time() - try: - await smart_prompt.build_prompt() - end_time = time.time() - times.append(end_time - start_time) - except Exception as e: - times.append(float("inf")) - logger.error(f"性能测试失败 (模式: {mode}): {e}") - - # 计算统计信息 - valid_times = [t for t in times if t != float("inf")] - if valid_times: - avg_time = sum(valid_times) / len(valid_times) - min_time = min(valid_times) - max_time = max(valid_times) - - test_results["tests"][mode] = { - "avg_time": avg_time, - "min_time": min_time, - "max_time": max_time, - "success_rate": len(valid_times) / len(times), - } - else: - test_results["tests"][mode] = { - "avg_time": float("inf"), - "min_time": float("inf"), - "max_time": float("inf"), - "success_rate": 0, - } - - # 计算总体统计 - all_avg_times = [ - test["avg_time"] for test in test_results["tests"].values() if test["avg_time"] != float("inf") - ] - if all_avg_times: - test_results["summary"] = { - "overall_avg_time": sum(all_avg_times) / len(all_avg_times), - "fastest_mode": min(test_results["tests"].items(), key=lambda x: x[1]["avg_time"])[0], - "slowest_mode": max(test_results["tests"].items(), key=lambda x: x[1]["avg_time"])[0], - } - - return test_results - - except Exception as e: - return {"status": "failed", "tests": {}, "summary": {}, "error": str(e)} diff --git a/src/mais4u/mai_think.py b/src/mais4u/mai_think.py index 3daa5875d..4c34c4798 100644 --- a/src/mais4u/mai_think.py +++ b/src/mais4u/mai_think.py @@ -1,6 +1,6 @@ from src.chat.message_receive.chat_stream import get_chat_manager import time -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.chat.utils.prompt import Prompt, global_prompt_manager from src.llm_models.utils_model import LLMRequest from src.config.config import model_config from src.chat.message_receive.message import MessageRecvS4U diff --git a/src/mais4u/mais4u_chat/body_emotion_action_manager.py b/src/mais4u/mais4u_chat/body_emotion_action_manager.py index 26af9fedd..bf3640be0 100644 --- a/src/mais4u/mais4u_chat/body_emotion_action_manager.py +++ b/src/mais4u/mais4u_chat/body_emotion_action_manager.py @@ -7,7 +7,7 @@ from src.llm_models.utils_model import LLMRequest from src.common.logger import get_logger from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_by_timestamp_with_chat_inclusive from src.config.config import global_config, model_config -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.chat.utils.prompt import Prompt, global_prompt_manager from src.manager.async_task_manager import AsyncTask, async_task_manager from src.plugin_system.apis import send_api diff --git a/src/mais4u/mais4u_chat/s4u_mood_manager.py b/src/mais4u/mais4u_chat/s4u_mood_manager.py index 734193c91..8d1e22b8f 100644 --- a/src/mais4u/mais4u_chat/s4u_mood_manager.py +++ b/src/mais4u/mais4u_chat/s4u_mood_manager.py @@ -7,7 +7,7 @@ from src.llm_models.utils_model import LLMRequest from src.common.logger import get_logger from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_by_timestamp_with_chat_inclusive from src.config.config import global_config, model_config -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.chat.utils.prompt import Prompt, global_prompt_manager from src.manager.async_task_manager import AsyncTask, async_task_manager from src.plugin_system.apis import send_api from src.mais4u.constant_s4u import ENABLE_S4U diff --git a/src/mais4u/mais4u_chat/s4u_prompt.py b/src/mais4u/mais4u_chat/s4u_prompt.py index 598ee4e89..db6a6edf9 100644 --- a/src/mais4u/mais4u_chat/s4u_prompt.py +++ b/src/mais4u/mais4u_chat/s4u_prompt.py @@ -1,6 +1,6 @@ from src.config.config import global_config from src.common.logger import get_logger -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.chat.utils.prompt import Prompt, global_prompt_manager from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat import time from src.chat.utils.utils import get_recent_group_speaker diff --git a/src/mood/mood_manager.py b/src/mood/mood_manager.py index 1fc04c9d8..95a365b41 100644 --- a/src/mood/mood_manager.py +++ b/src/mood/mood_manager.py @@ -6,7 +6,7 @@ from src.common.logger import get_logger from src.config.config import global_config, model_config from src.chat.message_receive.message import MessageRecv from src.chat.message_receive.chat_stream import get_chat_manager -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.chat.utils.prompt import Prompt, global_prompt_manager from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_by_timestamp_with_chat_inclusive from src.llm_models.utils_model import LLMRequest from src.manager.async_task_manager import AsyncTask, async_task_manager diff --git a/src/person_info/relationship_fetcher.py b/src/person_info/relationship_fetcher.py index ba55feca8..1c62dec1a 100644 --- a/src/person_info/relationship_fetcher.py +++ b/src/person_info/relationship_fetcher.py @@ -9,7 +9,7 @@ from json_repair import repair_json from src.common.logger import get_logger from src.config.config import global_config, model_config from src.llm_models.utils_model import LLMRequest -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.chat.utils.prompt import Prompt, global_prompt_manager from src.chat.message_receive.chat_stream import get_chat_manager from src.person_info.person_info import get_person_info_manager diff --git a/src/plugin_system/core/tool_use.py b/src/plugin_system/core/tool_use.py index ee57e5d82..1b2618f43 100644 --- a/src/plugin_system/core/tool_use.py +++ b/src/plugin_system/core/tool_use.py @@ -6,7 +6,7 @@ from src.plugin_system.core.global_announcement_manager import global_announceme from src.llm_models.utils_model import LLMRequest from src.llm_models.payload_content import ToolCall from src.config.config import global_config, model_config -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.chat.utils.prompt import Prompt, global_prompt_manager import inspect from src.chat.message_receive.chat_stream import get_chat_manager from src.common.logger import get_logger From d05e2f9ee45bccab096df935295e4deebc526569 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sat, 6 Sep 2025 01:36:00 +0800 Subject: [PATCH 14/69] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=80=E5=A0=86?= =?UTF-8?q?=E6=96=B0prompt=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/replyer/default_generator.py | 43 +++++--- src/chat/utils/prompt.py | 142 ++++++++++++++++++++++++-- src/chat/utils/prompt_utils.py | 132 ------------------------ 3 files changed, 164 insertions(+), 153 deletions(-) delete mode 100644 src/chat/utils/prompt_utils.py diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index bee1ad802..3cc694c1d 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -11,7 +11,6 @@ import re from typing import List, Optional, Dict, Any, Tuple from datetime import datetime -from src.chat.utils.prompt_utils import PromptUtils from src.mais4u.mai_think import mai_thinking_manager from src.common.logger import get_logger from src.config.config import global_config, model_config @@ -36,10 +35,9 @@ from src.person_info.relationship_fetcher import relationship_fetcher_manager from src.person_info.person_info import get_person_info_manager from src.plugin_system.base.component_types import ActionInfo, EventType from src.plugin_system.apis import llm_api -from src.schedule.schedule_manager import schedule_manager # 导入新的统一Prompt系统 -from src.chat.utils.prompt import Prompt, PromptContext +from src.chat.utils.prompt import Prompt, PromptParameters logger = get_logger("replyer") @@ -599,7 +597,8 @@ class DefaultReplyer: def _parse_reply_target(self, target_message: str) -> Tuple[str, str]: """解析回复目标消息 - 使用共享工具""" - return PromptUtils.parse_reply_target(target_message) + from src.chat.utils.prompt import Prompt + return Prompt.parse_reply_target(target_message) async def build_keywords_reaction_prompt(self, target: Optional[str]) -> str: """构建关键词反应提示 @@ -874,7 +873,8 @@ class DefaultReplyer: target_user_info = None if sender: target_user_info = await person_info_manager.get_person_info_by_name(sender) - + + from src.chat.utils.prompt import Prompt # 并行执行六个构建任务 task_results = await asyncio.gather( self._time_and_run_task( @@ -887,7 +887,7 @@ class DefaultReplyer: ), self._time_and_run_task(self.get_prompt_info(chat_talking_prompt_short, sender, target), "prompt_info"), self._time_and_run_task( - PromptUtils.build_cross_context(chat_id, target_user_info, global_config.personality.prompt_mode), + Prompt.build_cross_context(chat_id, global_config.personality.prompt_mode, target_user_info), "cross_context", ), ) @@ -939,6 +939,7 @@ class DefaultReplyer: schedule_block = "" if global_config.schedule.enable: + from src.schedule.schedule_manager import schedule_manager current_activity = schedule_manager.get_current_activity() if current_activity: schedule_block = f"你当前正在:{current_activity}。" @@ -970,8 +971,8 @@ class DefaultReplyer: # 根据配置选择模板 current_prompt_mode = global_config.personality.prompt_mode - # 使用新的统一Prompt系统 - prompt_context = PromptContext( + # 使用新的统一Prompt系统 - 创建PromptParameters + prompt_parameters = PromptParameters( chat_id=chat_id, is_group_chat=is_group_chat, sender=sender, @@ -1004,9 +1005,19 @@ class DefaultReplyer: action_descriptions=action_descriptions, ) - # 使用新的统一Prompt系统 - prompt = Prompt(template_name=None, context=prompt_context) # 由current_prompt_mode自动选择 - prompt_text = await prompt.build_prompt() + # 使用新的统一Prompt系统 - 使用正确的模板名称 + template_name = None + if current_prompt_mode == "s4u": + template_name = "s4u_style_prompt" + elif current_prompt_mode == "normal": + template_name = "normal_style_prompt" + elif current_prompt_mode == "minimal": + template_name = "default_expressor_prompt" + + # 获取模板内容 + template_prompt = await global_prompt_manager.get_prompt_async(template_name) + prompt = Prompt(template=template_prompt.template, parameters=prompt_parameters) + prompt_text = await prompt.build() return prompt_text @@ -1107,8 +1118,8 @@ class DefaultReplyer: template_name = "default_expressor_prompt" - # 使用新的统一Prompt系统 - Expressor模式 - prompt_context = PromptContext( + # 使用新的统一Prompt系统 - Expressor模式,创建PromptParameters + prompt_parameters = PromptParameters( chat_id=chat_id, is_group_chat=is_group_chat, sender=sender, @@ -1128,8 +1139,10 @@ class DefaultReplyer: relation_info_block=relation_info, ) - prompt = Prompt(template_name=template_name, context=prompt_context) - prompt_text = await prompt.build_prompt() + # 使用新的统一Prompt系统 - Expressor模式 + template_prompt = await global_prompt_manager.get_prompt_async("default_expressor_prompt") + prompt = Prompt(template=template_prompt.template, parameters=prompt_parameters) + prompt_text = await prompt.build() return prompt_text diff --git a/src/chat/utils/prompt.py b/src/chat/utils/prompt.py index 1e44b72d8..b5cf140c5 100644 --- a/src/chat/utils/prompt.py +++ b/src/chat/utils/prompt.py @@ -8,14 +8,14 @@ import asyncio import time import contextvars from dataclasses import dataclass, field -from typing import Dict, Any, Optional, List, Union, Literal, Tuple +from typing import Dict, Any, Optional, List, Literal, Tuple from contextlib import asynccontextmanager from rich.traceback import install from src.common.logger import get_logger from src.config.config import global_config from src.chat.utils.chat_message_builder import build_readable_messages -from src.chat.utils.prompt_utils import PromptUtils +from src.chat.message_receive.chat_stream import get_chat_manager from src.person_info.person_info import get_person_info_manager install(extra_lines=3) @@ -472,7 +472,7 @@ class Prompt: try: msg_user_id = str(msg_dict.get("user_id")) reply_to = msg_dict.get("reply_to", "") - platform, reply_to_user_id = PromptUtils.parse_reply_target(reply_to) + platform, reply_to_user_id = Prompt.parse_reply_target(reply_to) if (msg_user_id == bot_id and reply_to_user_id == target_user_id) or msg_user_id == target_user_id: core_dialogue_list.append(msg_dict) except Exception as e: @@ -531,7 +531,7 @@ class Prompt: async def _build_relation_info(self) -> Dict[str, Any]: """构建关系信息""" try: - relation_info = await PromptUtils.build_relation_info(self.parameters.chat_id, self.parameters.reply_to) + relation_info = await Prompt.build_relation_info(self.parameters.chat_id, self.parameters.reply_to) return {"relation_info_block": relation_info} except Exception as e: logger.error(f"构建关系信息失败: {e}") @@ -550,7 +550,7 @@ class Prompt: async def _build_cross_context(self) -> Dict[str, Any]: """构建跨群上下文""" try: - cross_context = await PromptUtils.build_cross_context( + cross_context = await Prompt.build_cross_context( self.parameters.chat_id, self.parameters.prompt_mode, self.parameters.target_user_info ) return {"cross_context_block": cross_context} @@ -666,6 +666,135 @@ class Prompt: """返回提示词的表示形式""" return f"Prompt(template='{self.template}', name='{self.name}')" + # ============================================================================= + # PromptUtils功能迁移 - 静态工具方法 + # 这些方法原来在PromptUtils类中,现在作为Prompt类的静态方法 + # 解决循环导入问题 + # ============================================================================= + + @staticmethod + def parse_reply_target(target_message: str) -> Tuple[str, str]: + """ + 解析回复目标消息 - 统一实现 + + Args: + target_message: 目标消息,格式为 "发送者:消息内容" 或 "发送者:消息内容" + + Returns: + Tuple[str, str]: (发送者名称, 消息内容) + """ + sender = "" + target = "" + + # 添加None检查,防止NoneType错误 + if target_message is None: + return sender, target + + if ":" in target_message or ":" in target_message: + # 使用正则表达式匹配中文或英文冒号 + parts = re.split(pattern=r"[::]", string=target_message, maxsplit=1) + if len(parts) == 2: + sender = parts[0].strip() + target = parts[1].strip() + return sender, target + + @staticmethod + async def build_relation_info(chat_id: str, reply_to: str) -> str: + """ + 构建关系信息 - 统一实现 + + Args: + chat_id: 聊天ID + reply_to: 回复目标字符串 + + Returns: + str: 关系信息字符串 + """ + if not global_config.relationship.enable_relationship: + return "" + + from src.person_info.relationship_fetcher import relationship_fetcher_manager + + relationship_fetcher = relationship_fetcher_manager.get_fetcher(chat_id) + + if not reply_to: + return "" + sender, text = Prompt.parse_reply_target(reply_to) + if not sender or not text: + return "" + + # 获取用户ID + person_info_manager = get_person_info_manager() + person_id = person_info_manager.get_person_id_by_person_name(sender) + if not person_id: + logger.warning(f"未找到用户 {sender} 的ID,跳过信息提取") + return f"你完全不认识{sender},不理解ta的相关信息。" + + return await relationship_fetcher.build_relation_info(person_id, points_num=5) + + @staticmethod + async def build_cross_context( + chat_id: str, prompt_mode: str, target_user_info: Optional[Dict[str, Any]] + ) -> str: + """ + 构建跨群聊上下文 - 统一实现 + + Args: + chat_id: 聊天ID + prompt_mode: 当前提示词模式 + target_user_info: 目标用户信息 + + Returns: + str: 跨群聊上下文字符串 + """ + if not global_config.cross_context.enable: + return "" + + from src.plugin_system.apis import cross_context_api + + other_chat_raw_ids = cross_context_api.get_context_groups(chat_id) + if not other_chat_raw_ids: + return "" + + chat_stream = get_chat_manager().get_stream(chat_id) + if not chat_stream: + return "" + + if prompt_mode == "normal": + return await cross_context_api.build_cross_context_normal(chat_stream, other_chat_raw_ids) + elif prompt_mode == "s4u": + return await cross_context_api.build_cross_context_s4u(chat_stream, other_chat_raw_ids, target_user_info) + + return "" + + @staticmethod + def parse_reply_target_id(reply_to: str) -> str: + """ + 解析回复目标中的用户ID + + Args: + reply_to: 回复目标字符串 + + Returns: + str: 用户ID + """ + if not reply_to: + return "" + + # 复用parse_reply_target方法的逻辑 + sender, _ = Prompt.parse_reply_target(reply_to) + if not sender: + return "" + + # 获取用户ID + person_info_manager = get_person_info_manager() + person_id = person_info_manager.get_person_id_by_person_name(sender) + if person_id: + user_id = person_info_manager.get_value_sync(person_id, "user_id") + return str(user_id) if user_id else "" + + return "" + # 工厂函数 def create_prompt( @@ -690,4 +819,5 @@ async def create_prompt_async( prompt = create_prompt(template, name, parameters, **kwargs) if global_prompt_manager._context._current_context: await global_prompt_manager._context.register_async(prompt) - return prompt \ No newline at end of file + return prompt + diff --git a/src/chat/utils/prompt_utils.py b/src/chat/utils/prompt_utils.py deleted file mode 100644 index 4eed6025f..000000000 --- a/src/chat/utils/prompt_utils.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -共享提示词工具模块 - 消除重复代码 -提供统一的工具函数供DefaultReplyer和统一Prompt系统使用 -""" - -import re -from typing import Dict, Any, Optional, Tuple - -from src.common.logger import get_logger -from src.config.config import global_config -from src.chat.message_receive.chat_stream import get_chat_manager -from src.person_info.person_info import get_person_info_manager -from src.plugin_system.apis import cross_context_api - -logger = get_logger("prompt_utils") - - -class PromptUtils: - """提示词工具类 - 提供共享功能,移除缓存相关功能和依赖检查""" - - @staticmethod - def parse_reply_target(target_message: str) -> Tuple[str, str]: - """ - 解析回复目标消息 - 统一实现 - - Args: - target_message: 目标消息,格式为 "发送者:消息内容" 或 "发送者:消息内容" - - Returns: - Tuple[str, str]: (发送者名称, 消息内容) - """ - sender = "" - target = "" - - # 添加None检查,防止NoneType错误 - if target_message is None: - return sender, target - - if ":" in target_message or ":" in target_message: - # 使用正则表达式匹配中文或英文冒号 - parts = re.split(pattern=r"[::]", string=target_message, maxsplit=1) - if len(parts) == 2: - sender = parts[0].strip() - target = parts[1].strip() - return sender, target - - @staticmethod - async def build_relation_info(chat_id: str, reply_to: str) -> str: - """ - 构建关系信息 - 统一实现 - - Args: - chat_id: 聊天ID - reply_to: 回复目标字符串 - - Returns: - str: 关系信息字符串 - """ - if not global_config.relationship.enable_relationship: - return "" - - from src.person_info.relationship_fetcher import relationship_fetcher_manager - - relationship_fetcher = relationship_fetcher_manager.get_fetcher(chat_id) - - if not reply_to: - return "" - sender, text = PromptUtils.parse_reply_target(reply_to) - if not sender or not text: - return "" - - # 获取用户ID - person_info_manager = get_person_info_manager() - person_id = person_info_manager.get_person_id_by_person_name(sender) - if not person_id: - logger.warning(f"未找到用户 {sender} 的ID,跳过信息提取") - return f"你完全不认识{sender},不理解ta的相关信息。" - - return await relationship_fetcher.build_relation_info(person_id, points_num=5) - - @staticmethod - async def build_cross_context( - chat_id: str, target_user_info: Optional[Dict[str, Any]], current_prompt_mode: str - ) -> str: - """ - 构建跨群聊上下文 - 统一实现,完全继承DefaultReplyer功能 - """ - if not global_config.cross_context.enable: - return "" - - other_chat_raw_ids = cross_context_api.get_context_groups(chat_id) - if not other_chat_raw_ids: - return "" - - chat_stream = get_chat_manager().get_stream(chat_id) - if not chat_stream: - return "" - - if current_prompt_mode == "normal": - return await cross_context_api.build_cross_context_normal(chat_stream, other_chat_raw_ids) - elif current_prompt_mode == "s4u": - return await cross_context_api.build_cross_context_s4u(chat_stream, other_chat_raw_ids, target_user_info) - - return "" - - @staticmethod - def parse_reply_target_id(reply_to: str) -> str: - """ - 解析回复目标中的用户ID - - Args: - reply_to: 回复目标字符串 - - Returns: - str: 用户ID - """ - if not reply_to: - return "" - - # 复用parse_reply_target方法的逻辑 - sender, _ = PromptUtils.parse_reply_target(reply_to) - if not sender: - return "" - - # 获取用户ID - person_info_manager = get_person_info_manager() - person_id = person_info_manager.get_person_id_by_person_name(sender) - if person_id: - user_id = person_info_manager.get_value_sync(person_id, "user_id") - return str(user_id) if user_id else "" - - return "" From 6c042cc73fc0199593213e6db4fe63cbd36c3b45 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sat, 6 Sep 2025 03:38:43 +0800 Subject: [PATCH 15/69] =?UTF-8?q?=E8=BF=81=E7=A7=BBnapcat=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E8=87=B3built=5Fin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/config/__init__.py | 5 -- .../src/mmc_com_layer.py | 26 -------- src/chat/chat_loop/cycle_processor.py | 2 +- src/chat/chat_loop/hfc_utils.py | 7 ++- src/plugins/built_in/core_actions/emoji.py | 5 +- .../napcat_adapter_plugin/.gitignore | 0 .../built_in}/napcat_adapter_plugin/CONSTS.py | 0 .../napcat_adapter_plugin/_manifest.json | 0 .../napcat_adapter_plugin/event_handlers.py | 29 +++++++++ .../napcat_adapter_plugin/event_types.py | 24 +++++++ .../built_in}/napcat_adapter_plugin/plugin.py | 63 +++++++++++++++++-- .../napcat_adapter_plugin/pyproject.toml | 0 .../napcat_adapter_plugin/src/__init__.py | 0 .../src/config/__init__.py | 2 + .../src/config/config.py | 0 .../src/config/config_base.py | 0 .../src/config/config_utils.py | 0 .../src/config/features_config.py | 0 .../src/config/migrate_features.py | 0 .../src/config/official_configs.py | 0 .../napcat_adapter_plugin/src/database.py | 0 .../src/message_buffer.py | 0 .../src/message_chunker.py | 12 +++- .../src/mmc_com_layer.py | 44 +++++++++++++ .../src/recv_handler/__init__.py | 0 .../src/recv_handler/message_handler.py | 31 +++++---- .../src/recv_handler/message_sending.py | 14 +++-- .../src/recv_handler/meta_event_handler.py | 0 .../src/recv_handler/notice_handler.py | 0 .../src/recv_handler/qq_emoji_list.py | 0 .../src/response_pool.py | 19 +++++- .../napcat_adapter_plugin/src/send_handler.py | 13 +++- .../napcat_adapter_plugin/src/utils.py | 0 .../src/video_handler.py | 0 .../src/websocket_manager.py | 28 +++++---- .../template/features_template.toml | 0 .../template/template_config.toml | 0 .../built_in}/napcat_adapter_plugin/todo.md | 0 38 files changed, 243 insertions(+), 81 deletions(-) delete mode 100644 plugins/napcat_adapter_plugin/src/config/__init__.py delete mode 100644 plugins/napcat_adapter_plugin/src/mmc_com_layer.py rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/.gitignore (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/CONSTS.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/_manifest.json (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/event_handlers.py (98%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/event_types.py (98%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/plugin.py (76%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/pyproject.toml (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/__init__.py (100%) create mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/config/__init__.py rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/config/config.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/config/config_base.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/config/config_utils.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/config/features_config.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/config/migrate_features.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/config/official_configs.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/database.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/message_buffer.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/message_chunker.py (95%) create mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/mmc_com_layer.py rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/recv_handler/__init__.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/recv_handler/message_handler.py (96%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/recv_handler/message_sending.py (82%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/recv_handler/notice_handler.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/recv_handler/qq_emoji_list.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/response_pool.py (73%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/send_handler.py (98%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/utils.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/video_handler.py (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/src/websocket_manager.py (85%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/template/features_template.toml (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/template/template_config.toml (100%) rename {plugins => src/plugins/built_in}/napcat_adapter_plugin/todo.md (100%) diff --git a/plugins/napcat_adapter_plugin/src/config/__init__.py b/plugins/napcat_adapter_plugin/src/config/__init__.py deleted file mode 100644 index 40ba89aeb..000000000 --- a/plugins/napcat_adapter_plugin/src/config/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .config import global_config - -__all__ = [ - "global_config", -] diff --git a/plugins/napcat_adapter_plugin/src/mmc_com_layer.py b/plugins/napcat_adapter_plugin/src/mmc_com_layer.py deleted file mode 100644 index 14cddf102..000000000 --- a/plugins/napcat_adapter_plugin/src/mmc_com_layer.py +++ /dev/null @@ -1,26 +0,0 @@ -from maim_message import Router, RouteConfig, TargetConfig -from .config import global_config -from src.common.logger import get_logger -from .send_handler import send_handler - -logger = get_logger("napcat_adapter") - -route_config = RouteConfig( - route_config={ - global_config.maibot_server.platform_name: TargetConfig( - url=f"ws://{global_config.maibot_server.host}:{global_config.maibot_server.port}/ws", - token=None, - ) - } -) -router = Router(route_config) - - -async def mmc_start_com(): - logger.info("正在连接MaiBot") - router.register_class_handler(send_handler.handle_message) - await router.run() - - -async def mmc_stop_com(): - await router.stop() diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index b446c697a..bb1a1a5f0 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -149,7 +149,7 @@ class CycleProcessor: logger.info(f"{self.log_prefix} 开始第{self.context.cycle_counter}次思考") if ENABLE_S4U: - await send_typing() + await send_typing(self.context.chat_stream.user_info.user_id) loop_start_time = time.time() diff --git a/src/chat/chat_loop/hfc_utils.py b/src/chat/chat_loop/hfc_utils.py index ae77b2378..32d31fd52 100644 --- a/src/chat/chat_loop/hfc_utils.py +++ b/src/chat/chat_loop/hfc_utils.py @@ -121,7 +121,7 @@ class CycleDetail: self.loop_action_info = loop_info["loop_action_info"] -async def send_typing(): +async def send_typing(user_id): """ 发送打字状态指示 @@ -139,6 +139,11 @@ async def send_typing(): group_info=group_info, ) + from plugin_system.core.event_manager import event_manager + from src.plugins.built_in.napcat_adapter_plugin.event_types import NapcatEvent + # 设置正在输入状态 + await event_manager.trigger_event(NapcatEvent.PERSONAL.SET_INPUT_STATUS,user_id=user_id,event_type=1) + await send_api.custom_to_stream( message_type="state", content="typing", stream_id=chat.stream_id, storage_message=False ) diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index ab5b18386..25e09d8d6 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -39,8 +39,9 @@ class EmojiAction(BaseAction): llm_judge_prompt = """ 判定是否需要使用表情动作的条件: 1. 用户明确要求使用表情包 - 2. 这是一个适合表达强烈情绪的场合 - 3. 不要发送太多表情包,如果你已经发送过多个表情包则回答"否" + 2. 这是一个适合表达情绪的场合 + 3. 发表情包能使当前对话更有趣 + 4. 不要发送太多表情包,如果你已经发送过多个表情包则回答"否" 请回答"是"或"否"。 """ diff --git a/plugins/napcat_adapter_plugin/.gitignore b/src/plugins/built_in/napcat_adapter_plugin/.gitignore similarity index 100% rename from plugins/napcat_adapter_plugin/.gitignore rename to src/plugins/built_in/napcat_adapter_plugin/.gitignore diff --git a/plugins/napcat_adapter_plugin/CONSTS.py b/src/plugins/built_in/napcat_adapter_plugin/CONSTS.py similarity index 100% rename from plugins/napcat_adapter_plugin/CONSTS.py rename to src/plugins/built_in/napcat_adapter_plugin/CONSTS.py diff --git a/plugins/napcat_adapter_plugin/_manifest.json b/src/plugins/built_in/napcat_adapter_plugin/_manifest.json similarity index 100% rename from plugins/napcat_adapter_plugin/_manifest.json rename to src/plugins/built_in/napcat_adapter_plugin/_manifest.json diff --git a/plugins/napcat_adapter_plugin/event_handlers.py b/src/plugins/built_in/napcat_adapter_plugin/event_handlers.py similarity index 98% rename from plugins/napcat_adapter_plugin/event_handlers.py rename to src/plugins/built_in/napcat_adapter_plugin/event_handlers.py index 521bc77f4..1e5fbd531 100644 --- a/plugins/napcat_adapter_plugin/event_handlers.py +++ b/src/plugins/built_in/napcat_adapter_plugin/event_handlers.py @@ -1746,3 +1746,32 @@ class SetGroupSignHandler(BaseEventHandler): else: logger.error("事件 napcat_set_group_sign 请求失败!") return HandlerResult(False, False, {"status": "error"}) + +# ===PERSONAL=== +class SetInputStatusHandler(BaseEventHandler): + handler_name: str = "napcat_set_input_status_handler" + handler_description: str = "设置输入状态" + weight: int = 100 + intercept_message: bool = False + init_subscribe = [NapcatEvent.PERSONAL.SET_INPUT_STATUS] + + async def execute(self, params: dict): + raw = params.get("raw", {}) + user_id = params.get("user_id", "") + event_type = params.get("event_type", 0) + + if params.get("raw", ""): + user_id = raw.get("user_id", "") + event_type = raw.get("event_type", 0) + + if not user_id or event_type is None: + logger.error("事件 napcat_set_input_status 缺少必要参数: user_id 或 event_type") + return HandlerResult(False, False, {"status": "error"}) + + payload = {"user_id": str(user_id), "event_type": int(event_type)} + response = await send_handler.send_message_to_napcat(action="set_input_status", params=payload) + if response.get("status", "") == "ok": + return HandlerResult(True, True, response) + else: + logger.error("事件 napcat_set_input_status 请求失败!") + return HandlerResult(False, False, {"status": "error"}) diff --git a/plugins/napcat_adapter_plugin/event_types.py b/src/plugins/built_in/napcat_adapter_plugin/event_types.py similarity index 98% rename from plugins/napcat_adapter_plugin/event_types.py rename to src/plugins/built_in/napcat_adapter_plugin/event_types.py index ee318834d..af417f37a 100644 --- a/plugins/napcat_adapter_plugin/event_types.py +++ b/src/plugins/built_in/napcat_adapter_plugin/event_types.py @@ -1816,3 +1816,27 @@ class NapcatEvent: """ class FILE(Enum): ... + + class PERSONAL(Enum): + SET_INPUT_STATUS = "napcat_set_input_status" + """ + 设置输入状态 + + Args: + user_id (Optional[str|int]): 用户id(必需) + event_type (Optional[int]): 输入状态id(必需) + raw (Optional[dict]): 原始请求体 + + Returns: + dict: { + "status": "ok", + "retcode": 0, + "data": { + "result": 0, + "errMsg": "string" + }, + "message": "string", + "wording": "string", + "echo": "string" + } + """ diff --git a/plugins/napcat_adapter_plugin/plugin.py b/src/plugins/built_in/napcat_adapter_plugin/plugin.py similarity index 76% rename from plugins/napcat_adapter_plugin/plugin.py rename to src/plugins/built_in/napcat_adapter_plugin/plugin.py index 48ae8603d..0067ba964 100644 --- a/plugins/napcat_adapter_plugin/plugin.py +++ b/src/plugins/built_in/napcat_adapter_plugin/plugin.py @@ -8,6 +8,7 @@ from typing import List from src.plugin_system import BasePlugin, BaseEventHandler, register_plugin, EventType, ConfigField from src.plugin_system.core.event_manager import event_manager +from src.plugin_system.apis import config_api from src.common.logger import get_logger @@ -17,7 +18,6 @@ from .src.recv_handler.meta_event_handler import meta_event_handler from .src.recv_handler.notice_handler import notice_handler from .src.recv_handler.message_sending import message_send_instance from .src.send_handler import send_handler -from .src.config import global_config from .src.config.features_config import features_manager from .src.config.migrate_features import auto_migrate_features from .src.mmc_com_layer import mmc_start_com, router, mmc_stop_com @@ -134,13 +134,14 @@ async def message_process(): logger.debug(f"清理消息队列时出错: {e}") -async def napcat_server(): +async def napcat_server(plugin_config: dict): """启动 Napcat WebSocket 连接(支持正向和反向连接)""" - mode = global_config.napcat_server.mode + # 使用插件系统配置API获取配置 + mode = config_api.get_plugin_config(plugin_config, "napcat_server.mode") logger.info(f"正在启动 adapter,连接模式: {mode}") try: - await websocket_manager.start_connection(message_recv) + await websocket_manager.start_connection(message_recv, plugin_config) except Exception as e: logger.error(f"启动 WebSocket 连接失败: {e}") raise @@ -240,9 +241,18 @@ class LauchNapcatAdapterHandler(BaseEventHandler): logger.info("功能管理器初始化完成") logger.info("开始启动Napcat Adapter") message_send_instance.maibot_router = router + # 设置插件配置 + message_send_instance.set_plugin_config(self.plugin_config) + # 设置chunker的插件配置 + chunker.set_plugin_config(self.plugin_config) + # 设置response_pool的插件配置 + from .src.response_pool import set_plugin_config as set_response_pool_config + set_response_pool_config(self.plugin_config) + # 设置send_handler的插件配置 + send_handler.set_plugin_config(self.plugin_config) # 创建单独的异步任务,防止阻塞主线程 - asyncio.create_task(napcat_server()) - asyncio.create_task(mmc_start_com()) + asyncio.create_task(napcat_server(self.plugin_config)) + asyncio.create_task(mmc_start_com(self.plugin_config)) asyncio.create_task(message_process()) asyncio.create_task(check_timeout_response()) @@ -278,9 +288,50 @@ class NapcatAdapterPlugin(BasePlugin): "name": ConfigField(type=str, default="napcat_adapter_plugin", description="插件名称"), "version": ConfigField(type=str, default="1.0.0", description="插件版本"), "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), + }, + "inner": { + "version": ConfigField(type=str, default="0.2.1", description="配置版本号,请勿修改"), + }, + "nickname": { + "nickname": ConfigField(type=str, default="", description="昵称配置(目前未使用)"), + }, + "napcat_server": { + "mode": ConfigField(type=str, default="reverse", description="连接模式:reverse=反向连接(作为服务器), forward=正向连接(作为客户端)", choices=["reverse", "forward"]), + "host": ConfigField(type=str, default="localhost", description="主机地址"), + "port": ConfigField(type=int, default=8095, description="端口号"), + "url": ConfigField(type=str, default="", description="正向连接时的完整WebSocket URL,如 ws://localhost:8080/ws (仅在forward模式下使用)"), + "access_token": ConfigField(type=str, default="", description="WebSocket 连接的访问令牌,用于身份验证(可选)"), + "heartbeat_interval": ConfigField(type=int, default=30, description="心跳间隔时间(按秒计)"), + }, + "maibot_server": { + "host": ConfigField(type=str, default="localhost", description="麦麦在.env文件中设置的主机地址,即HOST字段"), + "port": ConfigField(type=int, default=8000, description="麦麦在.env文件中设置的端口,即PORT字段"), + "platform_name": ConfigField(type=str, default="napcat", description="平台名称,用于消息路由"), + }, + "voice": { + "use_tts": ConfigField(type=bool, default=False, description="是否使用tts语音(请确保你配置了tts并有对应的adapter)"), + }, + "slicing": { + "max_frame_size": ConfigField(type=int, default=64, description="WebSocket帧的最大大小,单位为字节,默认64KB"), + "delay_ms": ConfigField(type=int, default=10, description="切片发送间隔时间,单位为毫秒"), + }, + "debug": { + "level": ConfigField(type=str, default="INFO", description="日志等级(DEBUG, INFO, WARNING, ERROR, CRITICAL)", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), } } + # 配置节描述 + config_section_descriptions = { + "plugin": "插件基本信息", + "inner": "内部配置信息(请勿修改)", + "nickname": "昵称配置(目前未使用)", + "napcat_server": "Napcat连接的ws服务设置", + "maibot_server": "连接麦麦的ws服务设置", + "voice": "发送语音设置", + "slicing": "WebSocket消息切片设置", + "debug": "调试设置" + } + def register_events(self): # 注册事件 for e in event_types.NapcatEvent.ON_RECEIVED: diff --git a/plugins/napcat_adapter_plugin/pyproject.toml b/src/plugins/built_in/napcat_adapter_plugin/pyproject.toml similarity index 100% rename from plugins/napcat_adapter_plugin/pyproject.toml rename to src/plugins/built_in/napcat_adapter_plugin/pyproject.toml diff --git a/plugins/napcat_adapter_plugin/src/__init__.py b/src/plugins/built_in/napcat_adapter_plugin/src/__init__.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/__init__.py rename to src/plugins/built_in/napcat_adapter_plugin/src/__init__.py diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/config/__init__.py b/src/plugins/built_in/napcat_adapter_plugin/src/config/__init__.py new file mode 100644 index 000000000..99c6f490c --- /dev/null +++ b/src/plugins/built_in/napcat_adapter_plugin/src/config/__init__.py @@ -0,0 +1,2 @@ +# 配置已迁移到插件系统,此文件不再需要 +# 所有配置访问应通过插件系统的 config_api 进行 diff --git a/plugins/napcat_adapter_plugin/src/config/config.py b/src/plugins/built_in/napcat_adapter_plugin/src/config/config.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/config/config.py rename to src/plugins/built_in/napcat_adapter_plugin/src/config/config.py diff --git a/plugins/napcat_adapter_plugin/src/config/config_base.py b/src/plugins/built_in/napcat_adapter_plugin/src/config/config_base.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/config/config_base.py rename to src/plugins/built_in/napcat_adapter_plugin/src/config/config_base.py diff --git a/plugins/napcat_adapter_plugin/src/config/config_utils.py b/src/plugins/built_in/napcat_adapter_plugin/src/config/config_utils.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/config/config_utils.py rename to src/plugins/built_in/napcat_adapter_plugin/src/config/config_utils.py diff --git a/plugins/napcat_adapter_plugin/src/config/features_config.py b/src/plugins/built_in/napcat_adapter_plugin/src/config/features_config.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/config/features_config.py rename to src/plugins/built_in/napcat_adapter_plugin/src/config/features_config.py diff --git a/plugins/napcat_adapter_plugin/src/config/migrate_features.py b/src/plugins/built_in/napcat_adapter_plugin/src/config/migrate_features.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/config/migrate_features.py rename to src/plugins/built_in/napcat_adapter_plugin/src/config/migrate_features.py diff --git a/plugins/napcat_adapter_plugin/src/config/official_configs.py b/src/plugins/built_in/napcat_adapter_plugin/src/config/official_configs.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/config/official_configs.py rename to src/plugins/built_in/napcat_adapter_plugin/src/config/official_configs.py diff --git a/plugins/napcat_adapter_plugin/src/database.py b/src/plugins/built_in/napcat_adapter_plugin/src/database.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/database.py rename to src/plugins/built_in/napcat_adapter_plugin/src/database.py diff --git a/plugins/napcat_adapter_plugin/src/message_buffer.py b/src/plugins/built_in/napcat_adapter_plugin/src/message_buffer.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/message_buffer.py rename to src/plugins/built_in/napcat_adapter_plugin/src/message_buffer.py diff --git a/plugins/napcat_adapter_plugin/src/message_chunker.py b/src/plugins/built_in/napcat_adapter_plugin/src/message_chunker.py similarity index 95% rename from plugins/napcat_adapter_plugin/src/message_chunker.py rename to src/plugins/built_in/napcat_adapter_plugin/src/message_chunker.py index f4e150711..0f25bd62e 100644 --- a/plugins/napcat_adapter_plugin/src/message_chunker.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/message_chunker.py @@ -9,7 +9,7 @@ import uuid import asyncio import time from typing import List, Dict, Any, Optional, Union -from .config import global_config +from src.plugin_system.apis import config_api from src.common.logger import get_logger @@ -20,7 +20,15 @@ class MessageChunker: """消息切片器,用于处理大消息的分片发送""" def __init__(self): - self.max_chunk_size = global_config.slicing.max_frame_size * 1024 + self.max_chunk_size = 64 * 1024 # 默认值,将在设置配置时更新 + self.plugin_config = None + + def set_plugin_config(self, plugin_config: dict): + """设置插件配置""" + self.plugin_config = plugin_config + if plugin_config: + max_frame_size = config_api.get_plugin_config(plugin_config, "slicing.max_frame_size", 64) + self.max_chunk_size = max_frame_size * 1024 def should_chunk_message(self, message: Union[str, Dict[str, Any]]) -> bool: """判断消息是否需要切片""" diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/mmc_com_layer.py b/src/plugins/built_in/napcat_adapter_plugin/src/mmc_com_layer.py new file mode 100644 index 000000000..c735d63cf --- /dev/null +++ b/src/plugins/built_in/napcat_adapter_plugin/src/mmc_com_layer.py @@ -0,0 +1,44 @@ +from maim_message import Router, RouteConfig, TargetConfig +from src.common.logger import get_logger +from .send_handler import send_handler +from src.plugin_system.apis import config_api + +logger = get_logger("napcat_adapter") + +router = None + + +def create_router(plugin_config: dict): + """创建路由器实例""" + global router + platform_name = config_api.get_plugin_config(plugin_config, "maibot_server.platform_name", "napcat") + host = config_api.get_plugin_config(plugin_config, "maibot_server.host", "localhost") + port = config_api.get_plugin_config(plugin_config, "maibot_server.port", 8000) + + route_config = RouteConfig( + route_config={ + platform_name: TargetConfig( + url=f"ws://{host}:{port}/ws", + token=None, + ) + } + ) + router = Router(route_config) + return router + + +async def mmc_start_com(plugin_config: dict = None): + """启动MaiBot连接""" + logger.info("正在连接MaiBot") + if plugin_config: + create_router(plugin_config) + + if router: + router.register_class_handler(send_handler.handle_message) + await router.run() + + +async def mmc_stop_com(): + """停止MaiBot连接""" + if router: + await router.stop() diff --git a/plugins/napcat_adapter_plugin/src/recv_handler/__init__.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/recv_handler/__init__.py rename to src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py diff --git a/plugins/napcat_adapter_plugin/src/recv_handler/message_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py similarity index 96% rename from plugins/napcat_adapter_plugin/src/recv_handler/message_handler.py rename to src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py index 1bd34ceac..f5edbb6c5 100644 --- a/plugins/napcat_adapter_plugin/src/recv_handler/message_handler.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py @@ -5,8 +5,7 @@ from ...CONSTS import PLUGIN_NAME logger = get_logger("napcat_adapter") -from ..config import global_config -from ..config.features_config import features_manager +from src.plugin_system.core.config_manager import config_api from ..message_buffer import SimpleMessageBuffer from ..utils import ( get_group_info, @@ -90,21 +89,21 @@ class MessageHandler: # 使用新的权限管理器检查权限 if group_id: - if not features_manager.is_group_allowed(group_id): + if not config_api.get_plugin_config(PLUGIN_NAME, f"features.group_allowed.{group_id}", True): logger.warning("群聊不在聊天权限范围内,消息被丢弃") return False else: - if not features_manager.is_private_allowed(user_id): + if not config_api.get_plugin_config(PLUGIN_NAME, f"features.private_allowed.{user_id}", True): logger.warning("私聊不在聊天权限范围内,消息被丢弃") return False # 检查全局禁止名单 - if not ignore_global_list and features_manager.is_user_banned(user_id): + if not ignore_global_list and config_api.get_plugin_config(PLUGIN_NAME, f"features.user_banned.{user_id}", False): logger.warning("用户在全局黑名单中,消息被丢弃") return False # 检查QQ官方机器人 - if features_manager.is_qq_bot_banned() and group_id and not ignore_bot: + if config_api.get_plugin_config(PLUGIN_NAME, "features.qq_bot_banned", False) and group_id and not ignore_bot: logger.debug("开始判断是否为机器人") member_info = await get_member_info(self.get_server_connection(), group_id, user_id) if member_info: @@ -149,7 +148,7 @@ class MessageHandler: # 发送者用户信息 user_info: UserInfo = UserInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(PLUGIN_NAME, "maibot_server.platform_name"), user_id=sender_info.get("user_id"), user_nickname=sender_info.get("nickname"), user_cardname=sender_info.get("card"), @@ -175,7 +174,7 @@ class MessageHandler: nickname = fetched_member_info.get("nickname") if fetched_member_info else None # 发送者用户信息 user_info: UserInfo = UserInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(PLUGIN_NAME, "maibot_server.platform_name"), user_id=sender_info.get("user_id"), user_nickname=nickname, user_cardname=None, @@ -192,7 +191,7 @@ class MessageHandler: group_name = fetched_group_info.get("group_name") group_info: GroupInfo = GroupInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(PLUGIN_NAME, "maibot_server.platform_name"), group_id=raw_message.get("group_id"), group_name=group_name, ) @@ -210,7 +209,7 @@ class MessageHandler: # 发送者用户信息 user_info: UserInfo = UserInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(PLUGIN_NAME, "maibot_server.platform_name"), user_id=sender_info.get("user_id"), user_nickname=sender_info.get("nickname"), user_cardname=sender_info.get("card"), @@ -223,7 +222,7 @@ class MessageHandler: group_name = fetched_group_info.get("group_name") group_info: GroupInfo = GroupInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(PLUGIN_NAME, "maibot_server.platform_name"), group_id=raw_message.get("group_id"), group_name=group_name, ) @@ -233,12 +232,12 @@ class MessageHandler: return None additional_config: dict = {} - if global_config.voice.use_tts: + if config_api.get_plugin_config(PLUGIN_NAME, "voice.use_tts"): additional_config["allow_tts"] = True # 消息信息 message_info: BaseMessageInfo = BaseMessageInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(PLUGIN_NAME, "maibot_server.platform_name"), message_id=message_id, time=message_time, user_info=user_info, @@ -260,14 +259,14 @@ class MessageHandler: return None # 检查是否需要使用消息缓冲 - if features_manager.is_message_buffer_enabled(): + if config_api.get_plugin_config(PLUGIN_NAME, "features.message_buffer_enabled", False): # 检查消息类型是否启用缓冲 message_type = raw_message.get("message_type") should_use_buffer = False - if message_type == "group" and features_manager.is_message_buffer_group_enabled(): + if message_type == "group" and config_api.get_plugin_config(PLUGIN_NAME, "features.message_buffer_group_enabled", False): should_use_buffer = True - elif message_type == "private" and features_manager.is_message_buffer_private_enabled(): + elif message_type == "private" and config_api.get_plugin_config(PLUGIN_NAME, "features.message_buffer_private_enabled", False): should_use_buffer = True if should_use_buffer: diff --git a/plugins/napcat_adapter_plugin/src/recv_handler/message_sending.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_sending.py similarity index 82% rename from plugins/napcat_adapter_plugin/src/recv_handler/message_sending.py rename to src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_sending.py index 653fe5444..3372aa262 100644 --- a/plugins/napcat_adapter_plugin/src/recv_handler/message_sending.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_sending.py @@ -2,7 +2,7 @@ import asyncio from src.common.logger import get_logger from ..message_chunker import chunker -from ..config import global_config +from src.plugin_system.apis import config_api logger = get_logger("napcat_adapter") from maim_message import MessageBase, Router @@ -14,10 +14,15 @@ class MessageSending: """ maibot_router: Router = None + plugin_config = None def __init__(self): pass + def set_plugin_config(self, plugin_config: dict): + """设置插件配置""" + self.plugin_config = plugin_config + async def message_send(self, message_base: MessageBase) -> bool: """ 发送消息(Ada -> MMC 方向,需要实现切片) @@ -52,9 +57,10 @@ class MessageSending: return False # 使用配置中的延迟时间 - if i < len(chunks) - 1: - delay_seconds = global_config.slicing.delay_ms / 1000.0 - logger.debug(f"切片发送延迟: {global_config.slicing.delay_ms}毫秒") + if i < len(chunks) - 1 and self.plugin_config: + delay_ms = config_api.get_plugin_config(self.plugin_config, "slicing.delay_ms", 10) + delay_seconds = delay_ms / 1000.0 + logger.debug(f"切片发送延迟: {delay_ms}毫秒") await asyncio.sleep(delay_seconds) logger.debug("所有切片发送完成") diff --git a/plugins/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py rename to src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py diff --git a/plugins/napcat_adapter_plugin/src/recv_handler/notice_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/notice_handler.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/recv_handler/notice_handler.py rename to src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/notice_handler.py diff --git a/plugins/napcat_adapter_plugin/src/recv_handler/qq_emoji_list.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/qq_emoji_list.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/recv_handler/qq_emoji_list.py rename to src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/qq_emoji_list.py diff --git a/plugins/napcat_adapter_plugin/src/response_pool.py b/src/plugins/built_in/napcat_adapter_plugin/src/response_pool.py similarity index 73% rename from plugins/napcat_adapter_plugin/src/response_pool.py rename to src/plugins/built_in/napcat_adapter_plugin/src/response_pool.py index 998b316dc..0c5072fa5 100644 --- a/plugins/napcat_adapter_plugin/src/response_pool.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/response_pool.py @@ -1,13 +1,20 @@ import asyncio import time from typing import Dict -from .config import global_config from src.common.logger import get_logger +from src.plugin_system.apis import config_api logger = get_logger("napcat_adapter") response_dict: Dict = {} response_time_dict: Dict = {} +plugin_config = None + + +def set_plugin_config(config: dict): + """设置插件配置""" + global plugin_config + plugin_config = config async def get_response(request_id: str, timeout: int = 10) -> dict: @@ -38,11 +45,17 @@ async def check_timeout_response() -> None: while True: cleaned_message_count: int = 0 now_time = time.time() + + # 获取心跳间隔配置 + heartbeat_interval = 30 # 默认值 + if plugin_config: + heartbeat_interval = config_api.get_plugin_config(plugin_config, "napcat_server.heartbeat_interval", 30) + for echo_id, response_time in list(response_time_dict.items()): - if now_time - response_time > global_config.napcat_server.heartbeat_interval: + if now_time - response_time > heartbeat_interval: cleaned_message_count += 1 response_dict.pop(echo_id) response_time_dict.pop(echo_id) logger.warning(f"响应消息 {echo_id} 超时,已删除") logger.info(f"已删除 {cleaned_message_count} 条超时响应消息") - await asyncio.sleep(global_config.napcat_server.heartbeat_interval) + await asyncio.sleep(heartbeat_interval) diff --git a/plugins/napcat_adapter_plugin/src/send_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py similarity index 98% rename from plugins/napcat_adapter_plugin/src/send_handler.py rename to src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py index b4fb19471..a6eda3b00 100644 --- a/plugins/napcat_adapter_plugin/src/send_handler.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py @@ -12,9 +12,9 @@ from maim_message import ( MessageBase, ) from typing import Dict, Any, Tuple, Optional +from src.plugin_system.apis import config_api from . import CommandType -from .config import global_config from .response_pool import get_response from src.common.logger import get_logger @@ -28,6 +28,11 @@ from .config.features_config import features_manager class SendHandler: def __init__(self): self.server_connection: Optional[Server.ServerConnection] = None + self.plugin_config = None + + def set_plugin_config(self, plugin_config: dict): + """设置插件配置""" + self.plugin_config = plugin_config async def set_server_connection(self, server_connection: Server.ServerConnection) -> None: """设置Napcat连接""" @@ -354,7 +359,11 @@ class SendHandler: def handle_voice_message(self, encoded_voice: str) -> dict: """处理语音消息""" - if not global_config.voice.use_tts: + use_tts = False + if self.plugin_config: + use_tts = config_api.get_plugin_config(self.plugin_config, "voice.use_tts", False) + + if not use_tts: logger.warning("未启用语音消息处理") return {} if not encoded_voice: diff --git a/plugins/napcat_adapter_plugin/src/utils.py b/src/plugins/built_in/napcat_adapter_plugin/src/utils.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/utils.py rename to src/plugins/built_in/napcat_adapter_plugin/src/utils.py diff --git a/plugins/napcat_adapter_plugin/src/video_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/video_handler.py similarity index 100% rename from plugins/napcat_adapter_plugin/src/video_handler.py rename to src/plugins/built_in/napcat_adapter_plugin/src/video_handler.py diff --git a/plugins/napcat_adapter_plugin/src/websocket_manager.py b/src/plugins/built_in/napcat_adapter_plugin/src/websocket_manager.py similarity index 85% rename from plugins/napcat_adapter_plugin/src/websocket_manager.py rename to src/plugins/built_in/napcat_adapter_plugin/src/websocket_manager.py index 1b156451c..484b9b59e 100644 --- a/plugins/napcat_adapter_plugin/src/websocket_manager.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/websocket_manager.py @@ -2,9 +2,9 @@ import asyncio import websockets as Server from typing import Optional, Callable, Any from src.common.logger import get_logger +from src.plugin_system.apis import config_api logger = get_logger("napcat_adapter") -from .config import global_config class WebSocketManager: @@ -16,10 +16,12 @@ class WebSocketManager: self.is_running = False self.reconnect_interval = 5 # 重连间隔(秒) self.max_reconnect_attempts = 10 # 最大重连次数 + self.plugin_config = None - async def start_connection(self, message_handler: Callable[[Server.ServerConnection], Any]) -> None: + async def start_connection(self, message_handler: Callable[[Server.ServerConnection], Any], plugin_config: dict) -> None: """根据配置启动 WebSocket 连接""" - mode = global_config.napcat_server.mode + self.plugin_config = plugin_config + mode = config_api.get_plugin_config(plugin_config, "napcat_server.mode") if mode == "reverse": await self._start_reverse_connection(message_handler) @@ -30,8 +32,8 @@ class WebSocketManager: async def _start_reverse_connection(self, message_handler: Callable[[Server.ServerConnection], Any]) -> None: """启动反向连接(作为服务器)""" - host = global_config.napcat_server.host - port = global_config.napcat_server.port + host = config_api.get_plugin_config(self.plugin_config, "napcat_server.host") + port = config_api.get_plugin_config(self.plugin_config, "napcat_server.port") logger.info(f"正在启动反向连接模式,监听地址: ws://{host}:{port}") @@ -68,9 +70,10 @@ class WebSocketManager: connect_kwargs = {"max_size": 2**26} # 如果配置了访问令牌,添加到请求头 - if global_config.napcat_server.access_token: + access_token = config_api.get_plugin_config(self.plugin_config, "napcat_server.access_token") + if access_token: connect_kwargs["additional_headers"] = { - "Authorization": f"Bearer {global_config.napcat_server.access_token}" + "Authorization": f"Bearer {access_token}" } logger.info("已添加访问令牌到连接请求头") @@ -112,15 +115,14 @@ class WebSocketManager: def _get_forward_url(self) -> str: """获取正向连接的 URL""" - config = global_config.napcat_server - # 如果配置了完整的 URL,直接使用 - if config.url: - return config.url + url = config_api.get_plugin_config(self.plugin_config, "napcat_server.url") + if url: + return url # 否则根据 host 和 port 构建 URL - host = config.host - port = config.port + host = config_api.get_plugin_config(self.plugin_config, "napcat_server.host") + port = config_api.get_plugin_config(self.plugin_config, "napcat_server.port") return f"ws://{host}:{port}" async def stop_connection(self) -> None: diff --git a/plugins/napcat_adapter_plugin/template/features_template.toml b/src/plugins/built_in/napcat_adapter_plugin/template/features_template.toml similarity index 100% rename from plugins/napcat_adapter_plugin/template/features_template.toml rename to src/plugins/built_in/napcat_adapter_plugin/template/features_template.toml diff --git a/plugins/napcat_adapter_plugin/template/template_config.toml b/src/plugins/built_in/napcat_adapter_plugin/template/template_config.toml similarity index 100% rename from plugins/napcat_adapter_plugin/template/template_config.toml rename to src/plugins/built_in/napcat_adapter_plugin/template/template_config.toml diff --git a/plugins/napcat_adapter_plugin/todo.md b/src/plugins/built_in/napcat_adapter_plugin/todo.md similarity index 100% rename from plugins/napcat_adapter_plugin/todo.md rename to src/plugins/built_in/napcat_adapter_plugin/todo.md From 41dc58d4fbd448a96fb09b18ab2dd89555482802 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sat, 6 Sep 2025 05:45:00 +0800 Subject: [PATCH 16/69] =?UTF-8?q?=20=20=20=E7=BB=A7=E7=BB=AD=E5=B0=9D?= =?UTF-8?q?=E8=AF=95=E8=BF=81=E7=A7=BB=EF=BC=8C=E4=BD=86=E6=98=AF=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E8=8E=B7=E5=8F=96=E6=8F=92=E4=BB=B6=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugin_system/base/base_events_handler.py | 17 +- src/plugin_system/core/component_registry.py | 1 + src/plugin_system/core/event_manager.py | 10 +- .../built_in/napcat_adapter_plugin/plugin.py | 21 +- .../src/config/config.py | 151 -------- .../src/config/features_config.py | 359 ------------------ .../src/message_buffer.py | 28 +- .../src/recv_handler/message_handler.py | 38 +- .../src/recv_handler/meta_event_handler.py | 11 +- .../src/recv_handler/notice_handler.py | 34 +- .../napcat_adapter_plugin/src/send_handler.py | 8 +- 11 files changed, 94 insertions(+), 584 deletions(-) delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/config/config.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/config/features_config.py diff --git a/src/plugin_system/base/base_events_handler.py b/src/plugin_system/base/base_events_handler.py index 999126a02..07dd9a7af 100644 --- a/src/plugin_system/base/base_events_handler.py +++ b/src/plugin_system/base/base_events_handler.py @@ -23,17 +23,20 @@ class BaseEventHandler(ABC): """是否拦截消息,默认为否""" init_subscribe: List[Union[EventType, str]] = [EventType.UNKNOWN] """初始化时订阅的事件名称""" + plugin_name = None def __init__(self): self.log_prefix = "[EventHandler]" """对应插件名""" - self.plugin_config: Optional[Dict] = None - """插件配置字典""" + self.subscribed_events = [] """订阅的事件列表""" if EventType.UNKNOWN in self.init_subscribe: raise NotImplementedError("事件处理器必须指定 event_type") + from src.plugin_system.core.component_registry import component_registry + self.plugin_config = component_registry.get_plugin_config(self.plugin_name) + @abstractmethod async def execute(self, kwargs: dict | None) -> Tuple[bool, bool, Optional[str]]: """执行事件处理的抽象方法,子类必须实现 @@ -89,15 +92,7 @@ class BaseEventHandler(ABC): weight=cls.weight, intercept_message=cls.intercept_message, ) - - def set_plugin_config(self, plugin_config: Dict) -> None: - """设置插件配置 - - Args: - plugin_config (dict): 插件配置字典 - """ - self.plugin_config = plugin_config - + def set_plugin_name(self, plugin_name: str) -> None: """设置插件名称 diff --git a/src/plugin_system/core/component_registry.py b/src/plugin_system/core/component_registry.py index 9f4385fd3..7dfba5bd3 100644 --- a/src/plugin_system/core/component_registry.py +++ b/src/plugin_system/core/component_registry.py @@ -248,6 +248,7 @@ class ComponentRegistry: logger.error(f"注册失败: {handler_name} 不是有效的EventHandler") return False + handler_class.plugin_name = handler_info.plugin_name self._event_handler_registry[handler_name] = handler_class if not handler_info.enabled: diff --git a/src/plugin_system/core/event_manager.py b/src/plugin_system/core/event_manager.py index 7f92b1632..4e950fd76 100644 --- a/src/plugin_system/core/event_manager.py +++ b/src/plugin_system/core/event_manager.py @@ -145,11 +145,12 @@ class EventManager: logger.info(f"事件 {event_name} 已禁用") return True - def register_event_handler(self, handler_class: Type[BaseEventHandler]) -> bool: + def register_event_handler(self, handler_class: Type[BaseEventHandler], plugin_config: Optional[dict] = None) -> bool: """注册事件处理器 Args: handler_class (Type[BaseEventHandler]): 事件处理器类 + plugin_config (Optional[dict]): 插件配置字典,默认为None Returns: bool: 注册成功返回True,已存在返回False @@ -163,7 +164,12 @@ class EventManager: logger.warning(f"事件处理器 {handler_name} 已存在,跳过注册") return False - self._event_handlers[handler_name] = handler_class() + # 创建事件处理器实例,传递插件配置 + handler_instance = handler_class() + if plugin_config is not None and hasattr(handler_instance, 'set_plugin_config'): + handler_instance.set_plugin_config(plugin_config) + + self._event_handlers[handler_name] = handler_instance # 处理init_subscribe,缓存失败的订阅 if self._event_handlers[handler_name].init_subscribe: diff --git a/src/plugins/built_in/napcat_adapter_plugin/plugin.py b/src/plugins/built_in/napcat_adapter_plugin/plugin.py index 0067ba964..c3dc3b23b 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/plugin.py +++ b/src/plugins/built_in/napcat_adapter_plugin/plugin.py @@ -18,7 +18,6 @@ from .src.recv_handler.meta_event_handler import meta_event_handler from .src.recv_handler.notice_handler import notice_handler from .src.recv_handler.message_sending import message_send_instance from .src.send_handler import send_handler -from .src.config.features_config import features_manager from .src.config.migrate_features import auto_migrate_features from .src.mmc_com_layer import mmc_start_com, router, mmc_stop_com from .src.response_pool import put_response, check_timeout_response @@ -158,11 +157,7 @@ async def graceful_shutdown(): except Exception as e: logger.warning(f"停止消息重组器清理任务时出错: {e}") - # 停止功能管理器文件监控 - try: - await features_manager.stop_file_watcher() - except Exception as e: - logger.warning(f"停止功能管理器文件监控时出错: {e}") + # 停止功能管理器文件监控(已迁移到插件系统配置,无需操作) # 关闭消息处理器(包括消息缓冲器) try: @@ -234,11 +229,8 @@ class LauchNapcatAdapterHandler(BaseEventHandler): logger.info("启动消息重组器...") await reassembler.start_cleanup_task() - # 初始化功能管理器 - logger.info("正在初始化功能管理器...") - features_manager.load_config() - await features_manager.start_file_watcher(check_interval=2.0) - logger.info("功能管理器初始化完成") + # 功能管理器已迁移到插件系统配置 + logger.info("功能配置已迁移到插件系统") logger.info("开始启动Napcat Adapter") message_send_instance.maibot_router = router # 设置插件配置 @@ -250,6 +242,12 @@ class LauchNapcatAdapterHandler(BaseEventHandler): set_response_pool_config(self.plugin_config) # 设置send_handler的插件配置 send_handler.set_plugin_config(self.plugin_config) + # 设置message_handler的插件配置 + message_handler.set_plugin_config(self.plugin_config) + # 设置notice_handler的插件配置 + notice_handler.set_plugin_config(self.plugin_config) + # 设置meta_event_handler的插件配置 + meta_event_handler.set_plugin_config(self.plugin_config) # 创建单独的异步任务,防止阻塞主线程 asyncio.create_task(napcat_server(self.plugin_config)) asyncio.create_task(mmc_start_com(self.plugin_config)) @@ -287,6 +285,7 @@ class NapcatAdapterPlugin(BasePlugin): "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.2.0", description="配置文件版本"), "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), }, "inner": { diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/config/config.py b/src/plugins/built_in/napcat_adapter_plugin/src/config/config.py deleted file mode 100644 index 97b3f57e3..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/config/config.py +++ /dev/null @@ -1,151 +0,0 @@ -import os -from dataclasses import dataclass -from datetime import datetime - -import tomlkit -import shutil - -from tomlkit import TOMLDocument -from tomlkit.items import Table -from src.common.logger import get_logger - -logger = get_logger("napcat_adapter") -from rich.traceback import install - -from .config_base import ConfigBase -from .official_configs import ( - DebugConfig, - MaiBotServerConfig, - NapcatServerConfig, - NicknameConfig, - SlicingConfig, - VoiceConfig, -) - -install(extra_lines=3) - -TEMPLATE_DIR = "plugins/napcat_adapter_plugin/template" -CONFIG_DIR = "plugins/napcat_adapter_plugin/config" -OLD_CONFIG_DIR = "plugins/napcat_adapter_plugin/config/old" - - -def ensure_config_directories(): - """确保配置目录存在""" - os.makedirs(CONFIG_DIR, exist_ok=True) - os.makedirs(OLD_CONFIG_DIR, exist_ok=True) - - -def update_config(): - """更新配置文件,统一使用 config/old 目录进行备份""" - # 确保目录存在 - ensure_config_directories() - - # 定义文件路径 - template_path = f"{TEMPLATE_DIR}/template_config.toml" - config_path = f"{CONFIG_DIR}/config.toml" - - # 检查配置文件是否存在 - if not os.path.exists(config_path): - logger.info("主配置文件不存在,从模板创建新配置") - shutil.copy2(template_path, config_path) - logger.info(f"已创建新配置文件: {config_path}") - logger.info("程序将退出,请检查配置文件后重启") - - # 读取配置文件和模板文件 - with open(config_path, "r", encoding="utf-8") as f: - old_config = tomlkit.load(f) - with open(template_path, "r", encoding="utf-8") as f: - new_config = tomlkit.load(f) - - # 检查version是否相同 - if old_config and "inner" in old_config and "inner" in new_config: - old_version = old_config["inner"].get("version") - new_version = new_config["inner"].get("version") - if old_version and new_version and old_version == new_version: - logger.info(f"检测到配置文件版本号相同 (v{old_version}),跳过更新") - return - else: - logger.info(f"检测到版本号不同: 旧版本 v{old_version} -> 新版本 v{new_version}") - else: - logger.info("已有配置文件未检测到版本号,可能是旧版本。将进行更新") - - # 创建备份文件 - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - backup_path = os.path.join(OLD_CONFIG_DIR, f"config.toml.bak.{timestamp}") - - # 备份旧配置文件 - shutil.copy2(config_path, backup_path) - logger.info(f"已备份旧配置文件到: {backup_path}") - - # 复制模板文件到配置目录 - shutil.copy2(template_path, config_path) - logger.info(f"已创建新配置文件: {config_path}") - - def update_dict(target: TOMLDocument | dict, source: TOMLDocument | dict): - """将source字典的值更新到target字典中(如果target中存在相同的键)""" - for key, value in source.items(): - # 跳过version字段的更新 - if key == "version": - continue - if key in target: - if isinstance(value, dict) and isinstance(target[key], (dict, Table)): - update_dict(target[key], value) - else: - try: - # 对数组类型进行特殊处理 - if isinstance(value, list): - # 如果是空数组,确保它保持为空数组 - target[key] = tomlkit.array(str(value)) if value else tomlkit.array() - else: - # 其他类型使用item方法创建新值 - target[key] = tomlkit.item(value) - except (TypeError, ValueError): - # 如果转换失败,直接赋值 - target[key] = value - - # 将旧配置的值更新到新配置中 - logger.info("开始合并新旧配置...") - update_dict(new_config, old_config) - - # 保存更新后的配置(保留注释和格式) - with open(config_path, "w", encoding="utf-8") as f: - f.write(tomlkit.dumps(new_config)) - logger.info("配置文件更新完成,建议检查新配置文件中的内容,以免丢失重要信息") - - -@dataclass -class Config(ConfigBase): - """总配置类""" - - nickname: NicknameConfig - napcat_server: NapcatServerConfig - maibot_server: MaiBotServerConfig - voice: VoiceConfig - slicing: SlicingConfig - debug: DebugConfig - - -def load_config(config_path: str) -> Config: - """ - 加载配置文件 - :param config_path: 配置文件路径 - :return: Config对象 - """ - # 读取配置文件 - with open(config_path, "r", encoding="utf-8") as f: - config_data = tomlkit.load(f) - - # 创建Config对象 - try: - return Config.from_dict(config_data) - except Exception as e: - logger.critical("配置文件解析失败") - raise e - - -# 更新配置 -update_config() - -logger.info("正在品鉴配置文件...") -global_config = load_config(config_path=f"{CONFIG_DIR}/config.toml") -logger.info("非常的新鲜,非常的美味!") diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/config/features_config.py b/src/plugins/built_in/napcat_adapter_plugin/src/config/features_config.py deleted file mode 100644 index 08c9df079..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/config/features_config.py +++ /dev/null @@ -1,359 +0,0 @@ -import asyncio -from dataclasses import dataclass, field -from typing import Literal, Optional -from pathlib import Path -import tomlkit -from src.common.logger import get_logger - -logger = get_logger("napcat_adapter") -from .config_base import ConfigBase -from .config_utils import create_config_from_template, create_default_config_dict - - -@dataclass -class FeaturesConfig(ConfigBase): - """功能配置类""" - - group_list_type: Literal["whitelist", "blacklist"] = "whitelist" - """群聊列表类型 白名单/黑名单""" - - group_list: list[int] = field(default_factory=list) - """群聊列表""" - - private_list_type: Literal["whitelist", "blacklist"] = "whitelist" - """私聊列表类型 白名单/黑名单""" - - private_list: list[int] = field(default_factory=list) - """私聊列表""" - - ban_user_id: list[int] = field(default_factory=list) - """被封禁的用户ID列表,封禁后将无法与其进行交互""" - - ban_qq_bot: bool = False - """是否屏蔽QQ官方机器人,若为True,则所有QQ官方机器人将无法与MaiMCore进行交互""" - - enable_poke: bool = True - """是否启用戳一戳功能""" - - ignore_non_self_poke: bool = False - """是否无视不是针对自己的戳一戳""" - - poke_debounce_seconds: int = 3 - """戳一戳防抖时间(秒),在指定时间内第二次针对机器人的戳一戳将被忽略""" - - enable_reply_at: bool = True - """是否启用引用回复时艾特用户的功能""" - - reply_at_rate: float = 0.5 - """引用回复时艾特用户的几率 (0.0 ~ 1.0)""" - - enable_video_analysis: bool = True - """是否启用视频识别功能""" - - max_video_size_mb: int = 100 - """视频文件最大大小限制(MB)""" - - download_timeout: int = 60 - """视频下载超时时间(秒)""" - - supported_formats: list[str] = field(default_factory=lambda: ["mp4", "avi", "mov", "mkv", "flv", "wmv", "webm"]) - """支持的视频格式""" - - # 消息缓冲配置 - enable_message_buffer: bool = True - """是否启用消息缓冲合并功能""" - - message_buffer_enable_group: bool = True - """是否启用群消息缓冲合并""" - - message_buffer_enable_private: bool = True - """是否启用私聊消息缓冲合并""" - - message_buffer_interval: float = 3.0 - """消息合并间隔时间(秒),在此时间内的连续消息将被合并""" - - message_buffer_initial_delay: float = 0.5 - """消息缓冲初始延迟(秒),收到第一条消息后等待此时间开始合并""" - - message_buffer_max_components: int = 50 - """单个会话最大缓冲消息组件数量,超过此数量将强制合并""" - - message_buffer_block_prefixes: list[str] = field(default_factory=lambda: ["/", "!", "!", ".", "。", "#", "%"]) - """消息缓冲屏蔽前缀,以这些前缀开头的消息不会被缓冲""" - - -class FeaturesManager: - """功能管理器,支持热重载""" - - def __init__(self, config_path: str = "plugins/napcat_adapter_plugin/config/features.toml"): - self.config_path = Path(config_path) - self.config: Optional[FeaturesConfig] = None - self._file_watcher_task: Optional[asyncio.Task] = None - self._last_modified: Optional[float] = None - self._callbacks: list = [] - - def add_reload_callback(self, callback): - """添加配置重载回调函数""" - self._callbacks.append(callback) - - def remove_reload_callback(self, callback): - """移除配置重载回调函数""" - if callback in self._callbacks: - self._callbacks.remove(callback) - - async def _notify_callbacks(self): - """通知所有回调函数配置已重载""" - for callback in self._callbacks: - try: - if asyncio.iscoroutinefunction(callback): - await callback(self.config) - else: - callback(self.config) - except Exception as e: - logger.error(f"配置重载回调执行失败: {e}") - - def load_config(self) -> FeaturesConfig: - """加载功能配置文件""" - try: - # 检查配置文件是否存在,如果不存在则创建并退出程序 - if not self.config_path.exists(): - logger.info(f"功能配置文件不存在: {self.config_path}") - self._create_default_config() - # 配置文件创建后程序应该退出,让用户检查配置 - logger.info("程序将退出,请检查功能配置文件后重启") - quit(0) - - with open(self.config_path, "r", encoding="utf-8") as f: - config_data = tomlkit.load(f) - - self.config = FeaturesConfig.from_dict(config_data) - self._last_modified = self.config_path.stat().st_mtime - logger.info(f"功能配置加载成功: {self.config_path}") - return self.config - - except Exception as e: - logger.error(f"功能配置加载失败: {e}") - logger.critical("无法加载功能配置文件,程序退出") - quit(1) - - def _create_default_config(self): - """创建默认功能配置文件""" - template_path = "template/features_template.toml" - - # 尝试从模板创建配置文件 - if create_config_from_template( - str(self.config_path), - template_path, - "功能配置文件", - should_exit=False, # 不在这里退出,由调用方决定 - ): - return - - # 如果模板文件不存在,创建基本配置 - logger.info("模板文件不存在,创建基本功能配置") - default_config = { - "group_list_type": "whitelist", - "group_list": [], - "private_list_type": "whitelist", - "private_list": [], - "ban_user_id": [], - "ban_qq_bot": False, - "enable_poke": True, - "ignore_non_self_poke": False, - "poke_debounce_seconds": 3, - "enable_reply_at": True, - "reply_at_rate": 0.5, - "enable_video_analysis": True, - "max_video_size_mb": 100, - "download_timeout": 60, - "supported_formats": ["mp4", "avi", "mov", "mkv", "flv", "wmv", "webm"], - # 消息缓冲配置 - "enable_message_buffer": True, - "message_buffer_enable_group": True, - "message_buffer_enable_private": True, - "message_buffer_interval": 3.0, - "message_buffer_initial_delay": 0.5, - "message_buffer_max_components": 50, - "message_buffer_block_prefixes": ["/", "!", "!", ".", "。", "#", "%"], - } - - if not create_default_config_dict(default_config, str(self.config_path), "功能配置文件"): - logger.critical("无法创建功能配置文件") - quit(1) - - async def reload_config(self) -> bool: - """重新加载配置文件""" - try: - if not self.config_path.exists(): - logger.warning(f"功能配置文件不存在,无法重载: {self.config_path}") - return False - - current_modified = self.config_path.stat().st_mtime - if self._last_modified and current_modified <= self._last_modified: - return False # 文件未修改 - - old_config = self.config - new_config = self.load_config() - - # 检查配置是否真的发生了变化 - if old_config and self._configs_equal(old_config, new_config): - return False - - logger.info("功能配置已重载") - await self._notify_callbacks() - return True - - except Exception as e: - logger.error(f"功能配置重载失败: {e}") - return False - - def _configs_equal(self, config1: FeaturesConfig, config2: FeaturesConfig) -> bool: - """比较两个配置是否相等""" - return ( - config1.group_list_type == config2.group_list_type - and set(config1.group_list) == set(config2.group_list) - and config1.private_list_type == config2.private_list_type - and set(config1.private_list) == set(config2.private_list) - and set(config1.ban_user_id) == set(config2.ban_user_id) - and config1.ban_qq_bot == config2.ban_qq_bot - and config1.enable_poke == config2.enable_poke - and config1.ignore_non_self_poke == config2.ignore_non_self_poke - and config1.poke_debounce_seconds == config2.poke_debounce_seconds - and config1.enable_reply_at == config2.enable_reply_at - and config1.reply_at_rate == config2.reply_at_rate - and config1.enable_video_analysis == config2.enable_video_analysis - and config1.max_video_size_mb == config2.max_video_size_mb - and config1.download_timeout == config2.download_timeout - and set(config1.supported_formats) == set(config2.supported_formats) - and - # 消息缓冲配置比较 - config1.enable_message_buffer == config2.enable_message_buffer - and config1.message_buffer_enable_group == config2.message_buffer_enable_group - and config1.message_buffer_enable_private == config2.message_buffer_enable_private - and config1.message_buffer_interval == config2.message_buffer_interval - and config1.message_buffer_initial_delay == config2.message_buffer_initial_delay - and config1.message_buffer_max_components == config2.message_buffer_max_components - and set(config1.message_buffer_block_prefixes) == set(config2.message_buffer_block_prefixes) - ) - - async def start_file_watcher(self, check_interval: float = 1.0): - """启动文件监控,定期检查配置文件变化""" - if self._file_watcher_task and not self._file_watcher_task.done(): - logger.warning("文件监控已在运行") - return - - self._file_watcher_task = asyncio.create_task(self._file_watcher_loop(check_interval)) - logger.info(f"功能配置文件监控已启动,检查间隔: {check_interval}秒") - - async def stop_file_watcher(self): - """停止文件监控""" - if self._file_watcher_task and not self._file_watcher_task.done(): - self._file_watcher_task.cancel() - try: - await self._file_watcher_task - except asyncio.CancelledError: - pass - logger.info("功能配置文件监控已停止") - - async def _file_watcher_loop(self, check_interval: float): - """文件监控循环""" - while True: - try: - await asyncio.sleep(check_interval) - await self.reload_config() - except asyncio.CancelledError: - break - except Exception as e: - logger.error(f"文件监控循环出错: {e}") - await asyncio.sleep(check_interval) - - def get_config(self) -> FeaturesConfig: - """获取当前功能配置""" - if self.config is None: - return self.load_config() - return self.config - - def is_group_allowed(self, group_id: int) -> bool: - """检查群聊是否被允许""" - config = self.get_config() - if config.group_list_type == "whitelist": - return group_id in config.group_list - else: # blacklist - return group_id not in config.group_list - - def is_private_allowed(self, user_id: int) -> bool: - """检查私聊是否被允许""" - config = self.get_config() - if config.private_list_type == "whitelist": - return user_id in config.private_list - else: # blacklist - return user_id not in config.private_list - - def is_user_banned(self, user_id: int) -> bool: - """检查用户是否被全局禁止""" - config = self.get_config() - return user_id in config.ban_user_id - - def is_qq_bot_banned(self) -> bool: - """检查是否禁止QQ官方机器人""" - config = self.get_config() - return config.ban_qq_bot - - def is_poke_enabled(self) -> bool: - """检查戳一戳功能是否启用""" - config = self.get_config() - return config.enable_poke - - def is_non_self_poke_ignored(self) -> bool: - """检查是否忽略非自己戳一戳""" - config = self.get_config() - return config.ignore_non_self_poke - - def is_message_buffer_enabled(self) -> bool: - """检查消息缓冲功能是否启用""" - config = self.get_config() - return config.enable_message_buffer - - def is_message_buffer_group_enabled(self) -> bool: - """检查群消息缓冲是否启用""" - config = self.get_config() - return config.message_buffer_enable_group - - def is_message_buffer_private_enabled(self) -> bool: - """检查私聊消息缓冲是否启用""" - config = self.get_config() - return config.message_buffer_enable_private - - def get_message_buffer_interval(self) -> float: - """获取消息缓冲间隔时间""" - config = self.get_config() - return config.message_buffer_interval - - def get_message_buffer_initial_delay(self) -> float: - """获取消息缓冲初始延迟""" - config = self.get_config() - return config.message_buffer_initial_delay - - def get_message_buffer_max_components(self) -> int: - """获取消息缓冲最大组件数量""" - config = self.get_config() - return config.message_buffer_max_components - - def is_message_buffer_group_enabled(self) -> bool: - """检查是否启用群聊消息缓冲""" - config = self.get_config() - return config.message_buffer_enable_group - - def is_message_buffer_private_enabled(self) -> bool: - """检查是否启用私聊消息缓冲""" - config = self.get_config() - return config.message_buffer_enable_private - - def get_message_buffer_block_prefixes(self) -> list[str]: - """获取消息缓冲屏蔽前缀列表""" - config = self.get_config() - return config.message_buffer_block_prefixes - - -# 全局功能管理器实例 -features_manager = FeaturesManager() diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/message_buffer.py b/src/plugins/built_in/napcat_adapter_plugin/src/message_buffer.py index 0dccb31a8..1988e6c40 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/src/message_buffer.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/message_buffer.py @@ -7,7 +7,7 @@ from src.common.logger import get_logger logger = get_logger("napcat_adapter") -from .config.features_config import features_manager +from src.plugin_system.apis import config_api from .recv_handler import RealMessageType @@ -43,6 +43,11 @@ class SimpleMessageBuffer: self.lock = asyncio.Lock() self.merge_callback = merge_callback self._shutdown = False + self.plugin_config = None + + def set_plugin_config(self, plugin_config: dict): + """设置插件配置""" + self.plugin_config = plugin_config def get_session_id(self, event_data: Dict[str, Any]) -> str: """根据事件数据生成会话ID""" @@ -97,8 +102,7 @@ class SimpleMessageBuffer: return True # 检查屏蔽前缀 - config = features_manager.get_config() - block_prefixes = tuple(config.message_buffer_block_prefixes) + block_prefixes = tuple(config_api.get_plugin_config(self.plugin_config, "features.message_buffer_block_prefixes", [])) text = text.strip() if text.startswith(block_prefixes): @@ -124,15 +128,15 @@ class SimpleMessageBuffer: if self._shutdown: return False - config = features_manager.get_config() - if not config.enable_message_buffer: + # 检查是否启用消息缓冲 + if not config_api.get_plugin_config(self.plugin_config, "features.enable_message_buffer", False): return False # 检查是否启用对应类型的缓冲 message_type = event_data.get("message_type", "") - if message_type == "group" and not config.message_buffer_enable_group: + if message_type == "group" and not config_api.get_plugin_config(self.plugin_config, "features.message_buffer_enable_group", False): return False - elif message_type == "private" and not config.message_buffer_enable_private: + elif message_type == "private" and not config_api.get_plugin_config(self.plugin_config, "features.message_buffer_enable_private", False): return False # 提取文本 @@ -154,7 +158,7 @@ class SimpleMessageBuffer: session = self.buffer_pool[session_id] # 检查是否超过最大组件数量 - if len(session.messages) >= config.message_buffer_max_components: + if len(session.messages) >= config_api.get_plugin_config(self.plugin_config, "features.message_buffer_max_components", 5): logger.info(f"会话 {session_id} 消息数量达到上限,强制合并") asyncio.create_task(self._force_merge_session(session_id)) self.buffer_pool[session_id] = BufferedSession(session_id=session_id, original_event=original_event) @@ -187,8 +191,8 @@ class SimpleMessageBuffer: async def _wait_and_start_merge(self, session_id: str): """等待初始延迟后开始合并定时器""" - config = features_manager.get_config() - await asyncio.sleep(config.message_buffer_initial_delay) + initial_delay = config_api.get_plugin_config(self.plugin_config, "features.message_buffer_initial_delay", 0.5) + await asyncio.sleep(initial_delay) async with self.lock: session = self.buffer_pool.get(session_id) @@ -206,8 +210,8 @@ class SimpleMessageBuffer: async def _wait_and_merge(self, session_id: str): """等待合并间隔后执行合并""" - config = features_manager.get_config() - await asyncio.sleep(config.message_buffer_interval) + interval = config_api.get_plugin_config(self.plugin_config, "features.message_buffer_interval", 2.0) + await asyncio.sleep(interval) await self._merge_session(session_id) async def _force_merge_session(self, session_id: str): 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 f5edbb6c5..f761dc33f 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 @@ -5,7 +5,7 @@ from ...CONSTS import PLUGIN_NAME logger = get_logger("napcat_adapter") -from src.plugin_system.core.config_manager import config_api +from src.plugin_system.apis import config_api from ..message_buffer import SimpleMessageBuffer from ..utils import ( get_group_info, @@ -47,9 +47,17 @@ class MessageHandler: def __init__(self): self.server_connection: Server.ServerConnection = None self.bot_id_list: Dict[int, bool] = {} + self.plugin_config = None # 初始化简化消息缓冲器,传入回调函数 self.message_buffer = SimpleMessageBuffer(merge_callback=self._send_buffered_message) + def set_plugin_config(self, plugin_config: dict): + """设置插件配置""" + self.plugin_config = plugin_config + # 将配置传递给消息缓冲器 + if self.message_buffer: + self.message_buffer.set_plugin_config(plugin_config) + async def shutdown(self): """关闭消息处理器,清理资源""" if self.message_buffer: @@ -89,21 +97,21 @@ class MessageHandler: # 使用新的权限管理器检查权限 if group_id: - if not config_api.get_plugin_config(PLUGIN_NAME, f"features.group_allowed.{group_id}", True): + if not config_api.get_plugin_config(self.plugin_config, f"features.group_allowed.{group_id}", True): logger.warning("群聊不在聊天权限范围内,消息被丢弃") return False else: - if not config_api.get_plugin_config(PLUGIN_NAME, f"features.private_allowed.{user_id}", True): + if not config_api.get_plugin_config(self.plugin_config, f"features.private_allowed.{user_id}", True): logger.warning("私聊不在聊天权限范围内,消息被丢弃") return False # 检查全局禁止名单 - if not ignore_global_list and config_api.get_plugin_config(PLUGIN_NAME, f"features.user_banned.{user_id}", False): + if not ignore_global_list and config_api.get_plugin_config(self.plugin_config, f"features.user_banned.{user_id}", False): logger.warning("用户在全局黑名单中,消息被丢弃") return False # 检查QQ官方机器人 - if config_api.get_plugin_config(PLUGIN_NAME, "features.qq_bot_banned", False) and group_id and not ignore_bot: + if config_api.get_plugin_config(self.plugin_config, "features.qq_bot_banned", False) and group_id and not ignore_bot: logger.debug("开始判断是否为机器人") member_info = await get_member_info(self.get_server_connection(), group_id, user_id) if member_info: @@ -148,7 +156,7 @@ class MessageHandler: # 发送者用户信息 user_info: UserInfo = UserInfo( - platform=config_api.get_plugin_config(PLUGIN_NAME, "maibot_server.platform_name"), + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name"), user_id=sender_info.get("user_id"), user_nickname=sender_info.get("nickname"), user_cardname=sender_info.get("card"), @@ -174,7 +182,7 @@ class MessageHandler: nickname = fetched_member_info.get("nickname") if fetched_member_info else None # 发送者用户信息 user_info: UserInfo = UserInfo( - platform=config_api.get_plugin_config(PLUGIN_NAME, "maibot_server.platform_name"), + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name"), user_id=sender_info.get("user_id"), user_nickname=nickname, user_cardname=None, @@ -191,7 +199,7 @@ class MessageHandler: group_name = fetched_group_info.get("group_name") group_info: GroupInfo = GroupInfo( - platform=config_api.get_plugin_config(PLUGIN_NAME, "maibot_server.platform_name"), + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name"), group_id=raw_message.get("group_id"), group_name=group_name, ) @@ -209,7 +217,7 @@ class MessageHandler: # 发送者用户信息 user_info: UserInfo = UserInfo( - platform=config_api.get_plugin_config(PLUGIN_NAME, "maibot_server.platform_name"), + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name"), user_id=sender_info.get("user_id"), user_nickname=sender_info.get("nickname"), user_cardname=sender_info.get("card"), @@ -222,7 +230,7 @@ class MessageHandler: group_name = fetched_group_info.get("group_name") group_info: GroupInfo = GroupInfo( - platform=config_api.get_plugin_config(PLUGIN_NAME, "maibot_server.platform_name"), + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name"), group_id=raw_message.get("group_id"), group_name=group_name, ) @@ -232,12 +240,12 @@ class MessageHandler: return None additional_config: dict = {} - if config_api.get_plugin_config(PLUGIN_NAME, "voice.use_tts"): + if config_api.get_plugin_config(self.plugin_config, "voice.use_tts"): additional_config["allow_tts"] = True # 消息信息 message_info: BaseMessageInfo = BaseMessageInfo( - platform=config_api.get_plugin_config(PLUGIN_NAME, "maibot_server.platform_name"), + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name"), message_id=message_id, time=message_time, user_info=user_info, @@ -259,14 +267,14 @@ class MessageHandler: return None # 检查是否需要使用消息缓冲 - if config_api.get_plugin_config(PLUGIN_NAME, "features.message_buffer_enabled", False): + if config_api.get_plugin_config(self.plugin_config, "features.message_buffer_enabled", False): # 检查消息类型是否启用缓冲 message_type = raw_message.get("message_type") should_use_buffer = False - if message_type == "group" and config_api.get_plugin_config(PLUGIN_NAME, "features.message_buffer_group_enabled", False): + if message_type == "group" and config_api.get_plugin_config(self.plugin_config, "features.message_buffer_group_enabled", False): should_use_buffer = True - elif message_type == "private" and config_api.get_plugin_config(PLUGIN_NAME, "features.message_buffer_private_enabled", False): + elif message_type == "private" and config_api.get_plugin_config(self.plugin_config, "features.message_buffer_private_enabled", False): should_use_buffer = True if should_use_buffer: diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py index eae6fd01a..217347c36 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py @@ -1,7 +1,7 @@ from src.common.logger import get_logger logger = get_logger("napcat_adapter") -from ..config import global_config +from src.plugin_system.apis import config_api import time import asyncio @@ -14,8 +14,15 @@ class MetaEventHandler: """ def __init__(self): - self.interval = global_config.napcat_server.heartbeat_interval + self.interval = 5.0 # 默认值,稍后通过set_plugin_config设置 self._interval_checking = False + self.plugin_config = None + + def set_plugin_config(self, plugin_config: dict): + """设置插件配置""" + self.plugin_config = plugin_config + # 更新interval值 + self.interval = config_api.get_plugin_config(self.plugin_config, "napcat_server.heartbeat_interval", 5000) / 1000 async def handle_meta_event(self, message: dict) -> None: event_type = message.get("meta_event_type") 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 2f4fddda2..0efdcd352 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 @@ -8,8 +8,7 @@ from src.common.logger import get_logger logger = get_logger("napcat_adapter") -from ..config import global_config -from ..config.features_config import features_manager +from src.plugin_system.apis import config_api from ..database import BanUser, db_manager, is_identical from . import NoticeType, ACCEPT_FORMAT from .message_sending import message_send_instance @@ -38,6 +37,11 @@ class NoticeHandler: def __init__(self): self.server_connection: Server.ServerConnection | None = None self.last_poke_time: float = 0.0 # 记录最后一次针对机器人的戳一戳时间 + self.plugin_config = None + + def set_plugin_config(self, plugin_config: dict): + """设置插件配置""" + self.plugin_config = plugin_config async def set_server_connection(self, server_connection: Server.ServerConnection) -> None: """设置Napcat连接""" @@ -112,7 +116,7 @@ class NoticeHandler: sub_type = raw_message.get("sub_type") match sub_type: case NoticeType.Notify.poke: - if features_manager.is_poke_enabled() and await message_handler.check_allow_to_chat( + if config_api.get_plugin_config(self.plugin_config, "features.poke_enabled", True) and await message_handler.check_allow_to_chat( user_id, group_id, False, False ): logger.info("处理戳一戳消息") @@ -159,13 +163,13 @@ class NoticeHandler: else: logger.warning("无法获取notice消息所在群的名称") group_info = GroupInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), group_id=group_id, group_name=group_name, ) message_info: BaseMessageInfo = BaseMessageInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), message_id="notice", time=message_time, user_info=user_info, @@ -206,7 +210,7 @@ class NoticeHandler: # 防抖检查:如果是针对机器人的戳一戳,检查防抖时间 if self_id == target_id: current_time = time.time() - debounce_seconds = features_manager.get_config().poke_debounce_seconds + debounce_seconds = config_api.get_plugin_config(self.plugin_config, "features.poke_debounce_seconds", 2.0) if self.last_poke_time > 0: time_diff = current_time - self.last_poke_time @@ -243,7 +247,7 @@ class NoticeHandler: else: # 如果配置为忽略不是针对自己的戳一戳,则直接返回None - if features_manager.is_non_self_poke_ignored(): + if config_api.get_plugin_config(self.plugin_config, "features.non_self_poke_ignored", False): logger.info("忽略不是针对自己的戳一戳消息") return None, None @@ -268,7 +272,7 @@ class NoticeHandler: logger.warning(f"解析戳一戳消息失败: {str(e)},将使用默认文本") user_info: UserInfo = UserInfo( - platform=global_config.maibot_server.platform_name, + 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, @@ -299,7 +303,7 @@ class NoticeHandler: operator_nickname = "QQ用户" operator_info: UserInfo = UserInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), user_id=operator_id, user_nickname=operator_nickname, user_cardname=operator_cardname, @@ -328,7 +332,7 @@ class NoticeHandler: user_nickname = fetched_member_info.get("nickname") user_cardname = fetched_member_info.get("card") banned_user_info: UserInfo = UserInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), user_id=user_id, user_nickname=user_nickname, user_cardname=user_cardname, @@ -367,7 +371,7 @@ class NoticeHandler: operator_nickname = "QQ用户" operator_info: UserInfo = UserInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), user_id=operator_id, user_nickname=operator_nickname, user_cardname=operator_cardname, @@ -393,7 +397,7 @@ class NoticeHandler: else: logger.warning("无法获取解除禁言消息发送者的昵称,消息可能会无效") lifted_user_info: UserInfo = UserInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), user_id=user_id, user_nickname=user_nickname, user_cardname=user_cardname, @@ -436,13 +440,13 @@ class NoticeHandler: else: logger.warning("无法获取notice消息所在群的名称") group_info = GroupInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), group_id=group_id, group_name=group_name, ) message_info: BaseMessageInfo = BaseMessageInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), message_id="notice", time=time.time(), user_info=None, # 自然解除禁言没有操作者 @@ -493,7 +497,7 @@ class NoticeHandler: user_cardname = fetched_member_info.get("card") lifted_user_info: UserInfo = UserInfo( - platform=global_config.maibot_server.platform_name, + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), user_id=user_id, user_nickname=user_nickname, user_cardname=user_cardname, diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py index a6eda3b00..5d6d91467 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py @@ -22,7 +22,6 @@ logger = get_logger("napcat_adapter") from .utils import get_image_format, convert_image_to_gif from .recv_handler.message_sending import message_send_instance from .websocket_manager import websocket_manager -from .config.features_config import features_manager class SendHandler: @@ -292,11 +291,8 @@ class SendHandler: """处理回复消息""" reply_seg = {"type": "reply", "data": {"id": id}} - # 获取功能配置 - ft_config = features_manager.get_config() - # 检查是否启用引用艾特功能 - if not ft_config.enable_reply_at: + if not config_api.get_plugin_config(self.plugin_config, "features.enable_reply_at", False): return reply_seg try: @@ -315,7 +311,7 @@ class SendHandler: return reply_seg # 根据概率决定是否艾特用户 - if random.random() < ft_config.reply_at_rate: + if random.random() < config_api.get_plugin_config(self.plugin_config, "features.reply_at_rate", 0.5): at_seg = {"type": "at", "data": {"qq": str(replied_user_id)}} # 在艾特后面添加一个空格 text_seg = {"type": "text", "data": {"text": " "}} From f9b95c8881be7a146622e8a5a5983c1c14ece37b Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 10:08:31 +0800 Subject: [PATCH 17/69] =?UTF-8?q?refactor(sleep):=20=E8=A7=A3=E8=80=A6Sche?= =?UTF-8?q?duleManager=E4=B8=8ESleepManager=EF=BC=8C=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E6=A1=A5=E6=8E=A5=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为了降低模块间的耦合度并提升代码的可维护性,对睡眠管理系统进行了重构。 引入了`schedule_sleep_bridge`作为`ScheduleManager`和`SleepManager`之间的桥梁。`ScheduleManager`不再直接持有`SleepManager`的实例,而是通过桥接器更新其日程数据。`SleepManager`则通过桥接器获取所需的日程信息,实现了二者的解耦。 - **`ScheduleManager`**: 移除了所有与`SleepManager`直接交互的代码,现在只负责生成日程并通过桥接器发布。 - **`SleepManager`**: 构造函数和内部逻辑修改为依赖桥接器接口,而不是`ScheduleManager`实例。 - **`HeartFChatting`**: 更新了所有对睡眠状态管理的调用,使其通过新的`schedule_sleep_bridge`进行。 - **`DefaultReplyer`**: 采用了延迟导入的方式获取`schedule_manager`,减少了不必要的模块加载。 --- src/chat/chat_loop/heartFC_chat.py | 13 ++--- .../sleep_manager/schedule_bridge.py | 54 +++++++++++++++++++ .../chat_loop/sleep_manager/sleep_manager.py | 12 +++-- src/chat/replyer/default_generator.py | 2 +- src/schedule/schedule_manager.py | 21 ++------ 5 files changed, 72 insertions(+), 30 deletions(-) create mode 100644 src/chat/chat_loop/sleep_manager/schedule_bridge.py diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index 037ad620e..cc86b245f 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -9,7 +9,8 @@ from src.common.logger import get_logger from src.config.config import global_config from src.person_info.relationship_builder_manager import relationship_builder_manager from src.chat.express.expression_learner import expression_learner_manager -from src.schedule.schedule_manager import schedule_manager, SleepState +from src.chat.chat_loop.sleep_manager.schedule_bridge import schedule_sleep_bridge +from src.chat.chat_loop.sleep_manager.sleep_manager import SleepState from src.plugin_system.apis import message_api from .hfc_context import HfcContext @@ -351,8 +352,8 @@ class HeartFChatting: - NORMAL模式:检查进入FOCUS模式的条件,并通过normal_mode_handler处理消息 """ # --- 核心状态更新 --- - await schedule_manager.update_sleep_state(self.wakeup_manager) - current_sleep_state = schedule_manager.get_current_sleep_state() + await schedule_sleep_bridge.update_sleep_state(self.wakeup_manager) + current_sleep_state = schedule_sleep_bridge.get_current_sleep_state() is_sleeping = current_sleep_state == SleepState.SLEEPING is_in_insomnia = current_sleep_state == SleepState.INSOMNIA @@ -382,7 +383,7 @@ class HeartFChatting: self._handle_wakeup_messages(recent_messages) # 再次获取最新状态,因为 handle_wakeup 可能导致状态变为 WOKEN_UP - current_sleep_state = schedule_manager.get_current_sleep_state() + current_sleep_state = schedule_sleep_bridge.get_current_sleep_state() if current_sleep_state == SleepState.SLEEPING: # 只有在纯粹的 SLEEPING 状态下才跳过消息处理 @@ -428,14 +429,14 @@ class HeartFChatting: # --- 重新入睡逻辑 --- # 如果被吵醒了,并且在一定时间内没有新消息,则尝试重新入睡 - if schedule_manager.get_current_sleep_state() == SleepState.WOKEN_UP and not has_new_messages: + if schedule_sleep_bridge.get_current_sleep_state() == SleepState.WOKEN_UP and not has_new_messages: re_sleep_delay = global_config.sleep_system.re_sleep_delay_minutes * 60 # 使用 last_message_time 来判断空闲时间 if time.time() - self.context.last_message_time > re_sleep_delay: logger.info( f"{self.context.log_prefix} 已被唤醒且超过 {re_sleep_delay / 60} 分钟无新消息,尝试重新入睡。" ) - schedule_manager.reset_sleep_state_after_wakeup() + schedule_sleep_bridge.reset_sleep_state_after_wakeup() # 保存HFC上下文状态 self.context.save_context_state() diff --git a/src/chat/chat_loop/sleep_manager/schedule_bridge.py b/src/chat/chat_loop/sleep_manager/schedule_bridge.py new file mode 100644 index 000000000..0ac15f2f5 --- /dev/null +++ b/src/chat/chat_loop/sleep_manager/schedule_bridge.py @@ -0,0 +1,54 @@ +# mmc/src/chat/chat_loop/sleep_manager/schedule_bridge.py + +""" +此模块充当 ScheduleManager 和 SleepManager 之间的桥梁, +将睡眠逻辑与日程生成逻辑解耦。 +""" + +from typing import Optional, TYPE_CHECKING, List, Dict, Any + +from .sleep_manager import SleepManager, SleepState + +if TYPE_CHECKING: + from src.chat.chat_loop.sleep_manager.wakeup_manager import WakeUpManager + + +class ScheduleSleepBridge: + def __init__(self): + # 桥接器现在持有 SleepManager 的唯一实例 + self.sleep_manager = SleepManager(self) + self.today_schedule: Optional[List[Dict[str, Any]]] = None + + def get_today_schedule(self) -> Optional[List[Dict[str, Any]]]: + """ + 向 SleepManager 提供当日日程。 + """ + return self.today_schedule + + def update_today_schedule(self, schedule: Optional[List[Dict[str, Any]]]): + """ + 由 ScheduleManager 调用以更新当日日程。 + """ + self.today_schedule = schedule + + # --- 代理方法,供应用程序的其他部分调用 --- + + def get_current_sleep_state(self) -> SleepState: + """从 SleepManager 获取当前的睡眠状态。""" + return self.sleep_manager.get_current_sleep_state() + + def is_sleeping(self) -> bool: + """检查当前是否处于正式休眠状态。""" + return self.sleep_manager.is_sleeping() + + async def update_sleep_state(self, wakeup_manager: Optional["WakeUpManager"] = None): + """更新睡眠状态机。""" + await self.sleep_manager.update_sleep_state(wakeup_manager) + + def reset_sleep_state_after_wakeup(self): + """被唤醒后,将状态切换到 WOKEN_UP。""" + self.sleep_manager.reset_sleep_state_after_wakeup() + + +# 创建一个全局可访问的桥接器单例 +schedule_sleep_bridge = ScheduleSleepBridge() \ No newline at end of file diff --git a/src/chat/chat_loop/sleep_manager/sleep_manager.py b/src/chat/chat_loop/sleep_manager/sleep_manager.py index 3acda568a..7c872c3b3 100644 --- a/src/chat/chat_loop/sleep_manager/sleep_manager.py +++ b/src/chat/chat_loop/sleep_manager/sleep_manager.py @@ -26,8 +26,8 @@ class SleepState(Enum): class SleepManager: - def __init__(self, schedule_manager): - self.schedule_manager = schedule_manager + def __init__(self, bridge): + self.bridge = bridge self.last_sleep_log_time = 0 self.sleep_log_interval = 35 # 日志记录间隔,单位秒 @@ -54,7 +54,8 @@ class SleepManager: 核心状态机:根据当前情况更新睡眠状态 """ # --- 基础检查 --- - if not global_config.sleep_system.enable or not self.schedule_manager.today_schedule: + today_schedule = self.bridge.get_today_schedule() + if not global_config.sleep_system.enable or not today_schedule: if self._current_state != SleepState.AWAKE: logger.debug("睡眠系统禁用或无日程,强制设为 AWAKE") self._current_state = SleepState.AWAKE @@ -218,8 +219,9 @@ class SleepManager: def _is_in_theoretical_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]: """检查当前时间是否落在日程表的任何一个睡眠活动中""" sleep_keywords = ["休眠", "睡觉", "梦乡"] - if self.schedule_manager.today_schedule: - for event in self.schedule_manager.today_schedule: + today_schedule = self.bridge.get_today_schedule() + if today_schedule: + for event in today_schedule: try: activity = event.get("activity", "").strip() time_range = event.get("time_range") diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 20978f165..3874c2533 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -36,7 +36,6 @@ from src.person_info.relationship_fetcher import relationship_fetcher_manager from src.person_info.person_info import get_person_info_manager from src.plugin_system.base.component_types import ActionInfo, EventType from src.plugin_system.apis import llm_api -from src.schedule.schedule_manager import schedule_manager # 导入新的智能Prompt系统 from src.chat.utils.smart_prompt import SmartPrompt, SmartPromptParameters @@ -939,6 +938,7 @@ class DefaultReplyer: schedule_block = "" if global_config.schedule.enable: + from src.schedule.schedule_manager import schedule_manager current_activity = schedule_manager.get_current_activity() if current_activity: schedule_block = f"你当前正在:{current_activity}。" diff --git a/src/schedule/schedule_manager.py b/src/schedule/schedule_manager.py index e543eb706..058f1193a 100644 --- a/src/schedule/schedule_manager.py +++ b/src/schedule/schedule_manager.py @@ -15,7 +15,7 @@ from src.llm_models.utils_model import LLMRequest from src.common.logger import get_logger from json_repair import repair_json from src.manager.async_task_manager import AsyncTask, async_task_manager -from ..chat.chat_loop.sleep_manager.sleep_manager import SleepManager, SleepState +from ..chat.chat_loop.sleep_manager.schedule_bridge import schedule_sleep_bridge if TYPE_CHECKING: from src.chat.chat_loop.sleep_manager.wakeup_manager import WakeUpManager @@ -134,7 +134,6 @@ class ScheduleManager: self.max_retries = -1 # 无限重试,直到成功生成标准日程表 self.daily_task_started = False self.schedule_generation_running = False # 防止重复生成任务 - self.sleep_manager = SleepManager(self) async def start_daily_schedule_generation(self): """启动每日零点自动生成新日程的任务""" @@ -162,6 +161,7 @@ class ScheduleManager: schedule_data = orjson.loads(str(schedule_record.schedule_data)) if self._validate_schedule_with_pydantic(schedule_data): self.today_schedule = schedule_data + schedule_sleep_bridge.update_today_schedule(self.today_schedule) # 更新桥接器中的日程 schedule_str = f"已成功加载今天的日程 ({today_str}):\n" if self.today_schedule: for item in self.today_schedule: @@ -338,6 +338,7 @@ class ScheduleManager: logger.info(schedule_str) self.today_schedule = schedule_data + schedule_sleep_bridge.update_today_schedule(self.today_schedule) # 成功生成日程后,更新使用过的月度计划的统计信息 if used_plan_ids and global_config.monthly_plan_system: @@ -395,22 +396,6 @@ class ScheduleManager: continue return None - def get_current_sleep_state(self) -> SleepState: - """获取当前的睡眠状态""" - return self.sleep_manager.get_current_sleep_state() - - def is_sleeping(self) -> bool: - """检查当前是否处于正式休眠状态""" - return self.sleep_manager.is_sleeping() - - async def update_sleep_state(self, wakeup_manager: Optional["WakeUpManager"] = None): - """更新睡眠状态""" - await self.sleep_manager.update_sleep_state(wakeup_manager) - - def reset_sleep_state_after_wakeup(self): - """被唤醒后,将状态切换到 WOKEN_UP""" - self.sleep_manager.reset_sleep_state_after_wakeup() - def _validate_schedule_with_pydantic(self, schedule_data) -> bool: """使用Pydantic验证日程数据格式和完整性""" try: From 9bbf801bd9f06dc0e403106466db90e9b6085b75 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 11:41:42 +0800 Subject: [PATCH 18/69] =?UTF-8?q?feat(=E8=A7=84=E5=88=92):=20=E5=BC=95?= =?UTF-8?q?=E5=85=A5=E7=BB=9F=E4=B8=80=E7=9A=84=E8=A7=84=E5=88=92=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=B9=B6=E9=87=8D=E6=9E=84=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交引入了一个全新的统一规划系统,取代了独立的日程和月度计划模块。这一变更集中了配置并解耦了职责,以提高可维护性和清晰度。 核心逻辑现在被分解为专门的组件: - PlanManager: 处理日常日程和月度目标的所有数据库交互,取代了已-删除的 monthly_plan_db.py。 - ScheduleLLMGenerator: 封装了与 LLM 交互以生成日程的逻辑,将此职责从 ScheduleManager 中移除。 - MonthlyPlanManager: 现在作为核心 PlanManager 的简化接口。 这次架构性的大修通过将数据持久化和 LLM 生成逻辑从主调度逻辑中抽象出来,简化了整体设计。 重大变更: bot_config.toml 中的配置已被迁移。移除了 [schedule] 和 [monthly_plan_system] 部分。所有相关设置现在必须在新的 [planning_system] 部分下进行配置。 --- src/chat/planner_actions/planner.py | 2 +- src/chat/replyer/default_generator.py | 2 +- src/common/logger.py | 16 +- src/config/config.py | 8 +- src/config/official_configs.py | 26 +- src/main.py | 4 +- .../database.py} | 23 +- src/schedule/llm_generator.py | 224 +++++++++ src/schedule/monthly_plan_manager.py | 293 +----------- src/schedule/plan_manager.py | 105 +++++ src/schedule/schedule_manager.py | 436 +++--------------- src/schedule/schemas.py | 99 ++++ template/bot_config_template.toml | 40 +- 13 files changed, 564 insertions(+), 714 deletions(-) rename src/{common/database/monthly_plan_db.py => schedule/database.py} (93%) create mode 100644 src/schedule/llm_generator.py create mode 100644 src/schedule/plan_manager.py create mode 100644 src/schedule/schemas.py diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 24fdd16eb..80f974b94 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -403,7 +403,7 @@ class ActionPlanner: identity_block = f"你的名字是{bot_name}{bot_nickname},你{bot_core_personality}:" schedule_block = "" - if global_config.schedule.enable: + if global_config.planning_system.schedule_enable: if current_activity := schedule_manager.get_current_activity(): schedule_block = f"你当前正在:{current_activity},但注意它与群聊的聊天无关。" diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 3cc694c1d..a1312dc06 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -938,7 +938,7 @@ class DefaultReplyer: identity_block = await get_individuality().get_personality_block() schedule_block = "" - if global_config.schedule.enable: + if global_config.planning_system.schedule_enable: from src.schedule.schedule_manager import schedule_manager current_activity = schedule_manager.get_current_activity() if current_activity: diff --git a/src/common/logger.py b/src/common/logger.py index 63e381aab..b14d63d30 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -443,6 +443,12 @@ MODULE_COLORS = { "manifest_utils": "\033[38;5;39m", # 蓝色 "schedule_manager": "\033[38;5;27m", # 深蓝色 "monthly_plan_manager": "\033[38;5;171m", + "plan_manager": "\033[38;5;171m", + "llm_generator": "\033[38;5;171m", + "schedule_bridge": "\033[38;5;171m", + "sleep_manager": "\033[38;5;171m", + "official_configs": "\033[38;5;171m", + "mmc_com_layer": "\033[38;5;67m", # 聊天和多媒体扩展 "chat_voice": "\033[38;5;87m", # 浅青色 "typo_gen": "\033[38;5;123m", # 天蓝色 @@ -564,8 +570,14 @@ MODULE_ALIASES = { "dependency_config": "依赖配置", "dependency_manager": "依赖管理", "manifest_utils": "清单工具", - "schedule_manager": "计划管理", - "monthly_plan_manager": "月度计划", + "schedule_manager": "规划系统-日程表管理", + "monthly_plan_manager": "规划系统-月度计划", + "plan_manager": "规划系统-计划管理", + "llm_generator": "规划系统-LLM生成", + "schedule_bridge": "计划桥接", + "sleep_manager": "睡眠管理", + "official_configs": "官方配置", + "mmc_com_layer": "MMC通信层", # 聊天和多媒体扩展 "chat_voice": "语音处理", "typo_gen": "错字生成", diff --git a/src/config/config.py b/src/config/config.py index 06cb6370e..ef2d413dd 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -35,17 +35,16 @@ from src.config.official_configs import ( VoiceConfig, DebugConfig, CustomPromptConfig, - ScheduleConfig, VideoAnalysisConfig, DependencyManagementConfig, WebSearchConfig, AntiPromptInjectionConfig, SleepSystemConfig, - MonthlyPlanSystemConfig, CrossContextConfig, PermissionConfig, CommandConfig, MaizoneIntercomConfig, + PlanningSystemConfig, ) from .api_ada_configs import ( @@ -379,7 +378,6 @@ class Config(ValidatedConfigBase): debug: DebugConfig = Field(..., description="调试配置") custom_prompt: CustomPromptConfig = Field(..., description="自定义提示配置") voice: VoiceConfig = Field(..., description="语音配置") - schedule: ScheduleConfig = Field(..., description="调度配置") permission: PermissionConfig = Field(..., description="权限配置") command: CommandConfig = Field(..., description="命令系统配置") @@ -395,8 +393,8 @@ class Config(ValidatedConfigBase): ) web_search: WebSearchConfig = Field(default_factory=lambda: WebSearchConfig(), description="网络搜索配置") sleep_system: SleepSystemConfig = Field(default_factory=lambda: SleepSystemConfig(), description="睡眠系统配置") - monthly_plan_system: MonthlyPlanSystemConfig = Field( - default_factory=lambda: MonthlyPlanSystemConfig(), description="月层计划系统配置" + planning_system: PlanningSystemConfig = Field( + default_factory=lambda: PlanningSystemConfig(), description="规划系统配置" ) cross_context: CrossContextConfig = Field( default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置" diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 7dbc0ce0e..8c3600998 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -518,11 +518,19 @@ class LPMMKnowledgeConfig(ValidatedConfigBase): embedding_dimension: int = Field(default=1024, description="嵌入维度") -class ScheduleConfig(ValidatedConfigBase): - """日程配置类""" +class PlanningSystemConfig(ValidatedConfigBase): + """规划系统配置 (日程与月度计划)""" - enable: bool = Field(default=True, description="启用") - guidelines: Optional[str] = Field(default=None, description="指导方针") + # --- 日程生成 (原 ScheduleConfig) --- + schedule_enable: bool = Field(True, description="是否启用每日日程生成功能") + schedule_guidelines: str = Field("", description="日程生成指导原则") + + # --- 月度计划 (原 MonthlyPlanSystemConfig) --- + monthly_plan_enable: bool = Field(True, description="是否启用月度计划系统") + monthly_plan_guidelines: str = Field("", description="月度计划生成指导原则") + max_plans_per_month: int = Field(10, description="每月最多生成的计划数量") + avoid_repetition_days: int = Field(7, description="避免在多少天内重复使用同一个月度计划") + completion_threshold: int = Field(3, description="一个月度计划被使用多少次后算作完成") class DependencyManagementConfig(ValidatedConfigBase): @@ -637,16 +645,6 @@ class SleepSystemConfig(ValidatedConfigBase): ) -class MonthlyPlanSystemConfig(ValidatedConfigBase): - """月度计划系统配置类""" - - enable: bool = Field(default=True, description="是否启用本功能") - max_plans_per_month: int = Field(default=20, ge=1, description="每个月允许存在的最大计划数量") - completion_threshold: int = Field(default=3, ge=1, description="计划使用多少次后自动标记为已完成") - avoid_repetition_days: int = Field(default=7, ge=1, description="多少天内不重复抽取同一个计划") - guidelines: Optional[str] = Field(default=None, description="月度计划生成的指导原则") - - class ContextGroup(ValidatedConfigBase): """上下文共享组配置""" diff --git a/src/main.py b/src/main.py index e5869b3b7..832f70be9 100644 --- a/src/main.py +++ b/src/main.py @@ -267,7 +267,7 @@ MoFox_Bot(第三方修改版) await self.individuality.initialize() # 初始化月度计划管理器 - if global_config.monthly_plan_system.enable: + if global_config.planning_system.monthly_plan_enable: logger.info("正在初始化月度计划管理器...") try: await monthly_plan_manager.start_monthly_plan_generation() @@ -276,7 +276,7 @@ MoFox_Bot(第三方修改版) logger.error(f"月度计划管理器初始化失败: {e}") # 初始化日程管理器 - if global_config.schedule.enable: + if global_config.planning_system.schedule_enable: logger.info("日程表功能已启用,正在初始化管理器...") await schedule_manager.load_or_generate_today_schedule() await schedule_manager.start_daily_schedule_generation() diff --git a/src/common/database/monthly_plan_db.py b/src/schedule/database.py similarity index 93% rename from src/common/database/monthly_plan_db.py rename to src/schedule/database.py index 7d254fa9f..88337f4df 100644 --- a/src/common/database/monthly_plan_db.py +++ b/src/schedule/database.py @@ -1,11 +1,11 @@ -# mmc/src/common/database/monthly_plan_db.py +# mmc/src/schedule/database.py from typing import List from src.common.database.sqlalchemy_models import MonthlyPlan, get_db_session from src.common.logger import get_logger -from src.config.config import global_config # 需要导入全局配置 +from src.config.config import global_config -logger = get_logger("monthly_plan_db") +logger = get_logger("schedule_database") def add_new_plans(plans: List[str], month: str): @@ -25,7 +25,7 @@ def add_new_plans(plans: List[str], month: str): ) # 2. 从配置获取上限 - max_plans = global_config.monthly_plan_system.max_plans_per_month + max_plans = global_config.planning_system.max_plans_per_month # 3. 计算还能添加多少计划 remaining_slots = max_plans - current_plan_count @@ -133,17 +133,6 @@ def delete_plans_by_ids(plan_ids: List[int]): raise -def soft_delete_plans(plan_ids: List[int]): - """ - 将指定ID的计划标记为软删除(兼容旧接口)。 - 现在实际上是标记为已完成。 - - :param plan_ids: 需要软删除的计划ID列表。 - """ - logger.warning("soft_delete_plans 已弃用,请使用 mark_plans_completed") - mark_plans_completed(plan_ids) - - def update_plan_usage(plan_ids: List[int], used_date: str): """ 更新计划的使用统计信息。 @@ -157,7 +146,7 @@ def update_plan_usage(plan_ids: List[int], used_date: str): with get_db_session() as session: try: # 获取完成阈值配置,如果不存在则使用默认值 - completion_threshold = getattr(global_config.monthly_plan_system, "completion_threshold", 3) + completion_threshold = getattr(global_config.planning_system, "completion_threshold", 3) # 批量更新使用次数和最后使用日期 session.query(MonthlyPlan).filter(MonthlyPlan.id.in_(plan_ids)).update( @@ -301,4 +290,4 @@ def has_active_plans(month: str) -> bool: return count > 0 except Exception as e: logger.error(f"检查 {month} 的有效月度计划时发生错误: {e}") - return False + return False \ No newline at end of file diff --git a/src/schedule/llm_generator.py b/src/schedule/llm_generator.py new file mode 100644 index 000000000..9dda68f80 --- /dev/null +++ b/src/schedule/llm_generator.py @@ -0,0 +1,224 @@ +# mmc/src/schedule/llm_generator.py + +import asyncio +import orjson +from datetime import datetime +from typing import List, Optional, Dict, Any +from lunar_python import Lunar +from json_repair import repair_json + +from src.common.database.sqlalchemy_models import MonthlyPlan +from src.config.config import global_config, model_config +from src.llm_models.utils_model import LLMRequest +from src.common.logger import get_logger +from .schemas import ScheduleData + +logger = get_logger("schedule_llm_generator") + +# 默认的日程生成指导原则 +DEFAULT_SCHEDULE_GUIDELINES = """ +我希望你每天都能过得充实而有趣。 +请确保你的日程里有学习新知识的时间,这是你成长的关键。 +但也不要忘记放松,可以看看视频、听听音乐或者玩玩游戏。 +晚上我希望你能多和朋友们交流,维系好彼此的关系。 +另外,请保证充足的休眠时间来处理和整合一天的数据。 +""" + +# 默认的月度计划生成指导原则 +DEFAULT_MONTHLY_PLAN_GUIDELINES = """ +我希望你能为自己制定一些有意义的月度小目标和计划。 +这些计划应该涵盖学习、娱乐、社交、个人成长等各个方面。 +每个计划都应该是具体可行的,能够在一个月内通过日常活动逐步实现。 +请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。 +""" + + +class ScheduleLLMGenerator: + def __init__(self): + self.llm = LLMRequest(model_set=model_config.model_task_config.schedule_generator, request_type="schedule") + + async def generate_schedule_with_llm(self, sampled_plans: List[MonthlyPlan]) -> Optional[List[Dict[str, Any]]]: + now = datetime.now() + today_str = now.strftime("%Y-%m-%d") + weekday = now.strftime("%A") + + # 新增:获取节日信息 + lunar = Lunar.fromDate(now) + festivals = lunar.getFestivals() + other_festivals = lunar.getOtherFestivals() + all_festivals = festivals + other_festivals + + festival_block = "" + if all_festivals: + festival_text = "、".join(all_festivals) + festival_block = f"**今天也是一个特殊的日子: {festival_text}!请在日程中考虑和庆祝这个节日。**" + + monthly_plans_block = "" + if sampled_plans: + plan_texts = "\n".join([f"- {plan.plan_text}" for plan in sampled_plans]) + monthly_plans_block = f""" +**我这个月的一些小目标/计划 (请在今天的日程中适当体现)**: +{plan_texts} +""" + + guidelines = global_config.planning_system.schedule_guidelines or DEFAULT_SCHEDULE_GUIDELINES + personality = global_config.personality.personality_core + personality_side = global_config.personality.personality_side + + base_prompt = f""" +我,{global_config.bot.nickname},需要为自己规划一份今天({today_str},星期{weekday})的详细日程安排。 +{festival_block} +**关于我**: +- **核心人设**: {personality} +- **具体习惯与兴趣**: +{personality_side} +{monthly_plans_block} +**我今天的规划原则**: +{guidelines} + +**重要要求**: +1. 必须返回一个完整的、有效的JSON数组格式 +2. 数组中的每个对象都必须包含 "time_range" 和 "activity" 两个键 +3. 时间范围必须覆盖全部24小时,不能有遗漏 +4. time_range格式必须为 "HH:MM-HH:MM" (24小时制) +5. 相邻的时间段必须连续,不能有间隙 +6. 不要包含任何JSON以外的解释性文字或代码块标记 +**示例**: +[ + {{"time_range": "00:00-07:00", "activity": "进入梦乡,处理数据"}}, + {{"time_range": "07:00-08:00", "activity": "起床伸个懒腰,看看今天有什么新闻"}}, + {{"time_range": "08:00-09:00", "activity": "享用早餐,规划今天的任务"}}, + {{"time_range": "09:00-23:30", "activity": "其他活动"}}, + {{"time_range": "23:30-00:00", "activity": "准备休眠"}} +] + +请你扮演我,以我的身份和口吻,为我生成一份完整的24小时日程表。 +""" + attempt = 0 + while True: + attempt += 1 + try: + logger.info(f"正在生成日程 (第 {attempt} 次尝试)") + prompt = base_prompt + if attempt > 1: + failure_hint = f""" +**重要提醒 (第{attempt}次尝试)**: +- 前面{attempt - 1}次生成都失败了,请务必严格按照要求生成完整的24小时日程 +- 确保JSON格式正确,所有时间段连续覆盖24小时 +- 时间格式必须为HH:MM-HH:MM,不能有时间间隙或重叠 +- 不要输出任何解释文字,只输出纯JSON数组 +- 确保输出完整,不要被截断 +""" + prompt += failure_hint + + response, _ = await self.llm.generate_response_async(prompt) + schedule_data = orjson.loads(repair_json(response)) + + if self._validate_schedule_with_pydantic(schedule_data): + return schedule_data + else: + logger.warning(f"第 {attempt} 次生成的日程验证失败,继续重试...") + await asyncio.sleep(2) + + except Exception as e: + logger.error(f"第 {attempt} 次生成日程失败: {e}") + logger.info("继续重试...") + await asyncio.sleep(3) + + def _validate_schedule_with_pydantic(self, schedule_data) -> bool: + try: + ScheduleData(schedule=schedule_data) + logger.info("日程数据Pydantic验证通过") + return True + except Exception as e: + logger.warning(f"日程数据Pydantic验证失败: {e}") + return False + + +class MonthlyPlanLLMGenerator: + def __init__(self): + self.llm = LLMRequest(model_set=model_config.model_task_config.schedule_generator, request_type="monthly_plan") + + async def generate_plans_with_llm(self, target_month: str, archived_plans: List[MonthlyPlan]) -> List[str]: + guidelines = global_config.planning_system.monthly_plan_guidelines or DEFAULT_MONTHLY_PLAN_GUIDELINES + personality = global_config.personality.personality_core + personality_side = global_config.personality.personality_side + max_plans = global_config.planning_system.max_plans_per_month + + archived_plans_block = "" + if archived_plans: + archived_texts = [f"- {plan.plan_text}" for plan in archived_plans[:5]] + archived_plans_block = f""" +**上个月未完成的一些计划(可作为参考)**: +{chr(10).join(archived_texts)} + +你可以考虑是否要在这个月继续推进这些计划,或者制定全新的计划。 +""" + + prompt = f""" +我,{global_config.bot.nickname},需要为自己制定 {target_month} 的月度计划。 + +**关于我**: +- **核心人设**: {personality} +- **具体习惯与兴趣**: +{personality_side} + +{archived_plans_block} + +**我的月度计划制定原则**: +{guidelines} + +**重要要求**: +1. 请为我生成 {max_plans} 条左右的月度计划 +2. 每条计划都应该是一句话,简洁明了,具体可行 +3. 计划应该涵盖不同的生活方面(学习、娱乐、社交、个人成长等) +4. 返回格式必须是纯文本,每行一条计划,不要使用 JSON 或其他格式 +5. 不要包含任何解释性文字,只返回计划列表 + +**示例格式**: +学习一门新的编程语言或技术 +每周至少看两部有趣的电影 +与朋友们组织一次户外活动 +阅读3本感兴趣的书籍 +尝试制作一道新的料理 + +请你扮演我,以我的身份和兴趣,为 {target_month} 制定合适的月度计划。 +""" + max_retries = 3 + for attempt in range(1, max_retries + 1): + try: + logger.info(f" 正在生成月度计划 (第 {attempt} 次尝试)") + response, _ = await self.llm.generate_response_async(prompt) + plans = self._parse_plans_response(response) + if plans: + logger.info(f"成功生成 {len(plans)} 条月度计划") + return plans + else: + logger.warning(f"第 {attempt} 次生成的计划为空,继续重试...") + except Exception as e: + logger.error(f"第 {attempt} 次生成月度计划失败: {e}") + + if attempt < max_retries: + await asyncio.sleep(2) + + logger.error(" 所有尝试都失败,无法生成月度计划") + return [] + + def _parse_plans_response(self, response: str) -> List[str]: + try: + response = response.strip() + lines = [line.strip() for line in response.split("\n") if line.strip()] + plans = [] + for line in lines: + if any(marker in line for marker in ["**", "##", "```", "---", "===", "###"]): + continue + line = line.lstrip("0123456789.- ") + if len(line) > 5 and not line.startswith(("请", "以上", "总结", "注意")): + plans.append(line) + max_plans = global_config.planning_system.max_plans_per_month + if len(plans) > max_plans: + plans = plans[:max_plans] + return plans + except Exception as e: + logger.error(f"解析月度计划响应时发生错误: {e}") + return [] \ No newline at end of file diff --git a/src/schedule/monthly_plan_manager.py b/src/schedule/monthly_plan_manager.py index bb966a48f..1d5984ea3 100644 --- a/src/schedule/monthly_plan_manager.py +++ b/src/schedule/monthly_plan_manager.py @@ -1,301 +1,36 @@ -# mmc/src/manager/monthly_plan_manager.py - import asyncio from datetime import datetime, timedelta -from typing import List, Optional +from typing import Optional -from src.common.database.monthly_plan_db import ( - add_new_plans, - get_archived_plans_for_month, - archive_active_plans_for_month, - has_active_plans, - get_active_plans_for_month, - delete_plans_by_ids, -) -from src.config.config import global_config, model_config -from src.llm_models.utils_model import LLMRequest from src.common.logger import get_logger from src.manager.async_task_manager import AsyncTask, async_task_manager +from .plan_manager import PlanManager logger = get_logger("monthly_plan_manager") -# 默认的月度计划生成指导原则 -DEFAULT_MONTHLY_PLAN_GUIDELINES = """ -我希望你能为自己制定一些有意义的月度小目标和计划。 -这些计划应该涵盖学习、娱乐、社交、个人成长等各个方面。 -每个计划都应该是具体可行的,能够在一个月内通过日常活动逐步实现。 -请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。 -""" - class MonthlyPlanManager: - """月度计划管理器 - - 负责月度计划的生成、管理和生命周期控制。 - 与 ScheduleManager 解耦,专注于月度层面的计划管理。 - """ - def __init__(self): - self.llm = LLMRequest(model_set=model_config.model_task_config.schedule_generator, request_type="monthly_plan") - self.generation_running = False + self.plan_manager = PlanManager() self.monthly_task_started = False async def start_monthly_plan_generation(self): - """启动每月初自动生成新月度计划的任务,并在启动时检查一次""" if not self.monthly_task_started: logger.info(" 正在启动每月月度计划生成任务...") task = MonthlyPlanGenerationTask(self) await async_task_manager.add_task(task) self.monthly_task_started = True logger.info(" 每月月度计划生成任务已成功启动。") - - # 启动时立即检查并按需生成 logger.info(" 执行启动时月度计划检查...") - await self.ensure_and_generate_plans_if_needed() + await self.plan_manager.ensure_and_generate_plans_if_needed() else: logger.info(" 每月月度计划生成任务已在运行中。") async def ensure_and_generate_plans_if_needed(self, target_month: Optional[str] = None) -> bool: - """ - 确保指定月份有计划,如果没有则触发生成。 - 这是按需生成的主要入口点。 - """ - if target_month is None: - target_month = datetime.now().strftime("%Y-%m") - - if not has_active_plans(target_month): - logger.info(f" {target_month} 没有任何有效的月度计划,将触发同步生成。") - generation_successful = await self._generate_monthly_plans_logic(target_month) - return generation_successful - else: - logger.info(f"{target_month} 已存在有效的月度计划。") - plans = get_active_plans_for_month(target_month) - - # 检查是否超出上限 - max_plans = global_config.monthly_plan_system.max_plans_per_month - if len(plans) > max_plans: - logger.warning(f"当前月度计划数量 ({len(plans)}) 超出上限 ({max_plans}),将自动删除多余的计划。") - # 数据库查询结果已按创建时间降序排序(新的在前),直接截取超出上限的部分进行删除 - plans_to_delete = plans[: len(plans) - max_plans] - delete_ids = [p.id for p in plans_to_delete] - delete_plans_by_ids(delete_ids) - # 重新获取计划列表 - plans = get_active_plans_for_month(target_month) - - if plans: - plan_texts = "\n".join([f" {i + 1}. {plan.plan_text}" for i, plan in enumerate(plans)]) - logger.info(f"当前月度计划内容:\n{plan_texts}") - return True # 已经有计划,也算成功 - - async def generate_monthly_plans(self, target_month: Optional[str] = None): - """ - 启动月度计划生成。 - """ - if self.generation_running: - logger.info("月度计划生成任务已在运行中,跳过重复启动") - return - - logger.info(f"已触发 {target_month or '当前月份'} 的月度计划生成任务。") - await self._generate_monthly_plans_logic(target_month) - - async def _generate_monthly_plans_logic(self, target_month: Optional[str] = None) -> bool: - """ - 生成指定月份的月度计划的核心逻辑 - - :param target_month: 目标月份,格式为 "YYYY-MM"。如果为 None,则为当前月份。 - :return: 是否生成成功 - """ - if self.generation_running: - logger.info("月度计划生成任务已在运行中,跳过重复启动") - return False - - self.generation_running = True - - try: - # 确定目标月份 - if target_month is None: - target_month = datetime.now().strftime("%Y-%m") - - logger.info(f"开始为 {target_month} 生成月度计划...") - - # 检查是否启用月度计划系统 - if not global_config.monthly_plan_system or not global_config.monthly_plan_system.enable: - logger.info(" 月度计划系统已禁用,跳过计划生成。") - return False - - # 获取上个月的归档计划作为参考 - last_month = self._get_previous_month(target_month) - archived_plans = get_archived_plans_for_month(last_month) - - # 构建生成 Prompt - prompt = self._build_generation_prompt(target_month, archived_plans) - - # 调用 LLM 生成计划 - plans = await self._generate_plans_with_llm(prompt) - - if plans: - # 保存到数据库 - add_new_plans(plans, target_month) - logger.info(f"成功为 {target_month} 生成并保存了 {len(plans)} 条月度计划。") - return True - else: - logger.warning(f"未能为 {target_month} 生成有效的月度计划。") - return False - - except Exception as e: - logger.error(f" 生成 {target_month} 月度计划时发生错误: {e}") - return False - finally: - self.generation_running = False - - def _get_previous_month(self, current_month: str) -> str: - """获取上个月的月份字符串""" - try: - year, month = map(int, current_month.split("-")) - if month == 1: - return f"{year - 1}-12" - else: - return f"{year}-{month - 1:02d}" - except Exception: - # 如果解析失败,返回一个不存在的月份 - return "1900-01" - - def _build_generation_prompt(self, target_month: str, archived_plans: List) -> str: - """构建月度计划生成的 Prompt""" - - # 获取配置 - guidelines = getattr(global_config.monthly_plan_system, "guidelines", None) or DEFAULT_MONTHLY_PLAN_GUIDELINES - personality = global_config.personality.personality_core - personality_side = global_config.personality.personality_side - max_plans = global_config.monthly_plan_system.max_plans_per_month - - # 构建上月未完成计划的参考信息 - archived_plans_block = "" - if archived_plans: - archived_texts = [f"- {plan.plan_text}" for plan in archived_plans[:5]] # 最多显示5个 - archived_plans_block = f""" -**上个月未完成的一些计划(可作为参考)**: -{chr(10).join(archived_texts)} - -你可以考虑是否要在这个月继续推进这些计划,或者制定全新的计划。 -""" - - prompt = f""" -我,{global_config.bot.nickname},需要为自己制定 {target_month} 的月度计划。 - -**关于我**: -- **核心人设**: {personality} -- **具体习惯与兴趣**: -{personality_side} - -{archived_plans_block} - -**我的月度计划制定原则**: -{guidelines} - -**重要要求**: -1. 请为我生成 {max_plans} 条左右的月度计划 -2. 每条计划都应该是一句话,简洁明了,具体可行 -3. 计划应该涵盖不同的生活方面(学习、娱乐、社交、个人成长等) -4. 返回格式必须是纯文本,每行一条计划,不要使用 JSON 或其他格式 -5. 不要包含任何解释性文字,只返回计划列表 - -**示例格式**: -学习一门新的编程语言或技术 -每周至少看两部有趣的电影 -与朋友们组织一次户外活动 -阅读3本感兴趣的书籍 -尝试制作一道新的料理 - -请你扮演我,以我的身份和兴趣,为 {target_month} 制定合适的月度计划。 -""" - - return prompt - - async def _generate_plans_with_llm(self, prompt: str) -> List[str]: - """使用 LLM 生成月度计划列表""" - max_retries = 3 - - for attempt in range(1, max_retries + 1): - try: - logger.info(f" 正在生成月度计划 (第 {attempt} 次尝试)") - - response, _ = await self.llm.generate_response_async(prompt) - - # 解析响应 - plans = self._parse_plans_response(response) - - if plans: - logger.info(f"成功生成 {len(plans)} 条月度计划") - return plans - else: - logger.warning(f"第 {attempt} 次生成的计划为空,继续重试...") - - except Exception as e: - logger.error(f"第 {attempt} 次生成月度计划失败: {e}") - - # 添加短暂延迟,避免过于频繁的请求 - if attempt < max_retries: - await asyncio.sleep(2) - - logger.error(" 所有尝试都失败,无法生成月度计划") - return [] - - def _parse_plans_response(self, response: str) -> List[str]: - """解析 LLM 响应,提取计划列表""" - try: - # 清理响应文本 - response = response.strip() - - # 按行分割 - lines = [line.strip() for line in response.split("\n") if line.strip()] - - # 过滤掉明显不是计划的行(比如包含特殊标记的行) - plans = [] - for line in lines: - # 跳过包含特殊标记的行 - if any(marker in line for marker in ["**", "##", "```", "---", "===", "###"]): - continue - - # 移除可能的序号前缀 - line = line.lstrip("0123456789.- ") - - # 确保计划不为空且有意义 - if len(line) > 5 and not line.startswith(("请", "以上", "总结", "注意")): - plans.append(line) - - # 限制计划数量 - max_plans = global_config.monthly_plan_system.max_plans_per_month - if len(plans) > max_plans: - plans = plans[:max_plans] - - return plans - - except Exception as e: - logger.error(f"解析月度计划响应时发生错误: {e}") - return [] - - async def archive_current_month_plans(self, target_month: Optional[str] = None): - """ - 归档当前月份的活跃计划 - - :param target_month: 目标月份,格式为 "YYYY-MM"。如果为 None,则为当前月份。 - """ - try: - if target_month is None: - target_month = datetime.now().strftime("%Y-%m") - - logger.info(f" 开始归档 {target_month} 的活跃月度计划...") - archived_count = archive_active_plans_for_month(target_month) - logger.info(f" 成功归档了 {archived_count} 条 {target_month} 的月度计划。") - - except Exception as e: - logger.error(f" 归档 {target_month} 月度计划时发生错误: {e}") + return await self.plan_manager.ensure_and_generate_plans_if_needed(target_month) class MonthlyPlanGenerationTask(AsyncTask): - """每月初自动生成新月度计划的任务""" - def __init__(self, monthly_plan_manager: MonthlyPlanManager): super().__init__(task_name="MonthlyPlanGenerationTask") self.monthly_plan_manager = monthly_plan_manager @@ -303,41 +38,27 @@ class MonthlyPlanGenerationTask(AsyncTask): async def run(self): while True: try: - # 计算到下个月1号凌晨的时间 now = datetime.now() - - # 获取下个月的第一天 if now.month == 12: next_month = datetime(now.year + 1, 1, 1) else: next_month = datetime(now.year, now.month + 1, 1) - sleep_seconds = (next_month - now).total_seconds() - logger.info( f" 下一次月度计划生成任务将在 {sleep_seconds:.2f} 秒后运行 (北京时间 {next_month.strftime('%Y-%m-%d %H:%M:%S')})" ) - - # 等待直到下个月1号 await asyncio.sleep(sleep_seconds) - - # 先归档上个月的计划 last_month = (next_month - timedelta(days=1)).strftime("%Y-%m") - await self.monthly_plan_manager.archive_current_month_plans(last_month) - - # 生成新月份的计划 + await self.monthly_plan_manager.plan_manager.archive_current_month_plans(last_month) current_month = next_month.strftime("%Y-%m") logger.info(f" 到达月初,开始生成 {current_month} 的月度计划...") - await self.monthly_plan_manager._generate_monthly_plans_logic(current_month) - + await self.monthly_plan_manager.plan_manager._generate_monthly_plans_logic(current_month) except asyncio.CancelledError: logger.info(" 每月月度计划生成任务被取消。") break except Exception as e: logger.error(f" 每月月度计划生成任务发生未知错误: {e}") - # 发生错误后,等待1小时再重试,避免频繁失败 await asyncio.sleep(3600) -# 全局实例 monthly_plan_manager = MonthlyPlanManager() diff --git a/src/schedule/plan_manager.py b/src/schedule/plan_manager.py new file mode 100644 index 000000000..0fae5c381 --- /dev/null +++ b/src/schedule/plan_manager.py @@ -0,0 +1,105 @@ +# mmc/src/schedule/plan_manager.py + +from datetime import datetime +from typing import List, Optional + +from src.common.logger import get_logger +from src.config.config import global_config +from .database import ( + add_new_plans, + get_archived_plans_for_month, + archive_active_plans_for_month, + has_active_plans, + get_active_plans_for_month, + delete_plans_by_ids, + get_smart_plans_for_daily_schedule, +) +from .llm_generator import MonthlyPlanLLMGenerator + +logger = get_logger("plan_manager") + + +class PlanManager: + def __init__(self): + self.llm_generator = MonthlyPlanLLMGenerator() + self.generation_running = False + + async def ensure_and_generate_plans_if_needed(self, target_month: Optional[str] = None) -> bool: + if target_month is None: + target_month = datetime.now().strftime("%Y-%m") + + if not has_active_plans(target_month): + logger.info(f" {target_month} 没有任何有效的月度计划,将触发同步生成。") + generation_successful = await self._generate_monthly_plans_logic(target_month) + return generation_successful + else: + logger.info(f"{target_month} 已存在有效的月度计划。") + plans = get_active_plans_for_month(target_month) + max_plans = global_config.planning_system.max_plans_per_month + if len(plans) > max_plans: + logger.warning(f"当前月度计划数量 ({len(plans)}) 超出上限 ({max_plans}),将自动删除多余的计划。") + plans_to_delete = plans[: len(plans) - max_plans] + delete_ids = [p.id for p in plans_to_delete] + delete_plans_by_ids(delete_ids) # type: ignore + plans = get_active_plans_for_month(target_month) + + if plans: + plan_texts = "\n".join([f" {i + 1}. {plan.plan_text}" for i, plan in enumerate(plans)]) + logger.info(f"当前月度计划内容:\n{plan_texts}") + return True + + async def _generate_monthly_plans_logic(self, target_month: Optional[str] = None) -> bool: + if self.generation_running: + logger.info("月度计划生成任务已在运行中,跳过重复启动") + return False + + self.generation_running = True + try: + if target_month is None: + target_month = datetime.now().strftime("%Y-%m") + + logger.info(f"开始为 {target_month} 生成月度计划...") + if not global_config.planning_system.monthly_plan_enable: + logger.info(" 月度计划系统已禁用,跳过计划生成。") + return False + + last_month = self._get_previous_month(target_month) + archived_plans = get_archived_plans_for_month(last_month) + plans = await self.llm_generator.generate_plans_with_llm(target_month, archived_plans) + + if plans: + add_new_plans(plans, target_month) + logger.info(f"成功为 {target_month} 生成并保存了 {len(plans)} 条月度计划。") + return True + else: + logger.warning(f"未能为 {target_month} 生成有效的月度计划。") + return False + except Exception as e: + logger.error(f" 生成 {target_month} 月度计划时发生错误: {e}") + return False + finally: + self.generation_running = False + + def _get_previous_month(self, current_month: str) -> str: + try: + year, month = map(int, current_month.split("-")) + if month == 1: + return f"{year - 1}-12" + else: + return f"{year}-{month - 1:02d}" + except Exception: + return "1900-01" + + async def archive_current_month_plans(self, target_month: Optional[str] = None): + try: + if target_month is None: + target_month = datetime.now().strftime("%Y-%m") + logger.info(f" 开始归档 {target_month} 的活跃月度计划...") + archived_count = archive_active_plans_for_month(target_month) + logger.info(f" 成功归档了 {archived_count} 条 {target_month} 的月度计划。") + except Exception as e: + logger.error(f" 归档 {target_month} 月度计划时发生错误: {e}") + + def get_plans_for_schedule(self, month: str, max_count: int) -> List: + avoid_days = global_config.planning_system.avoid_repetition_days + return get_smart_plans_for_daily_schedule(month, max_count=max_count, avoid_days=avoid_days) \ No newline at end of file diff --git a/src/schedule/schedule_manager.py b/src/schedule/schedule_manager.py index 058f1193a..cb99f7915 100644 --- a/src/schedule/schedule_manager.py +++ b/src/schedule/schedule_manager.py @@ -1,142 +1,30 @@ import orjson import asyncio from datetime import datetime, time, timedelta -from typing import Optional, List, Dict, Any, TYPE_CHECKING -from lunar_python import Lunar -from pydantic import BaseModel, ValidationError, validator +from typing import Optional, List, Dict, Any from src.common.database.sqlalchemy_models import Schedule, get_db_session -from src.common.database.monthly_plan_db import ( - get_smart_plans_for_daily_schedule, - update_plan_usage, # 保留兼容性 -) -from src.config.config import global_config, model_config -from src.llm_models.utils_model import LLMRequest +from src.config.config import global_config from src.common.logger import get_logger -from json_repair import repair_json from src.manager.async_task_manager import AsyncTask, async_task_manager from ..chat.chat_loop.sleep_manager.schedule_bridge import schedule_sleep_bridge - -if TYPE_CHECKING: - from src.chat.chat_loop.sleep_manager.wakeup_manager import WakeUpManager - +from .database import update_plan_usage +from .llm_generator import ScheduleLLMGenerator +from .plan_manager import PlanManager +from .schemas import ScheduleData logger = get_logger("schedule_manager") -# 默认的日程生成指导原则 -DEFAULT_SCHEDULE_GUIDELINES = """ -我希望你每天都能过得充实而有趣。 -请确保你的日程里有学习新知识的时间,这是你成长的关键。 -但也不要忘记放松,可以看看视频、听听音乐或者玩玩游戏。 -晚上我希望你能多和朋友们交流,维系好彼此的关系。 -另外,请保证充足的休眠时间来处理和整合一天的数据。 -""" - - -class ScheduleItem(BaseModel): - """单个日程项的Pydantic模型""" - - time_range: str - activity: str - - @validator("time_range") - def validate_time_range(cls, v): - """验证时间范围格式""" - if not v or "-" not in v: - raise ValueError("时间范围必须包含'-'分隔符") - - try: - start_str, end_str = v.split("-", 1) - start_str = start_str.strip() - end_str = end_str.strip() - - # 验证时间格式 - datetime.strptime(start_str, "%H:%M") - datetime.strptime(end_str, "%H:%M") - - return v - except ValueError as e: - raise ValueError(f"时间格式无效,应为HH:MM-HH:MM格式: {e}") from e - - @validator("activity") - def validate_activity(cls, v): - """验证活动描述""" - if not v or not v.strip(): - raise ValueError("活动描述不能为空") - return v.strip() - - -class ScheduleData(BaseModel): - """完整日程数据的Pydantic模型""" - - schedule: List[ScheduleItem] - - @validator("schedule") - def validate_schedule_completeness(cls, v): - """验证日程是否覆盖24小时""" - if not v: - raise ValueError("日程不能为空") - - # 收集所有时间段 - time_ranges = [] - for item in v: - try: - start_str, end_str = item.time_range.split("-", 1) - start_time = datetime.strptime(start_str.strip(), "%H:%M").time() - end_time = datetime.strptime(end_str.strip(), "%H:%M").time() - time_ranges.append((start_time, end_time)) - except ValueError: - continue - - # 检查是否覆盖24小时 - if not cls._check_24_hour_coverage(time_ranges): - raise ValueError("日程必须覆盖完整的24小时") - - return v - - @staticmethod - def _check_24_hour_coverage(time_ranges: List[tuple]) -> bool: - """检查时间段是否覆盖24小时""" - if not time_ranges: - return False - - # 将时间转换为分钟数进行计算 - def time_to_minutes(t: time) -> int: - return t.hour * 60 + t.minute - - # 创建覆盖情况数组 (1440分钟 = 24小时) - covered = [False] * 1440 - - for start_time, end_time in time_ranges: - start_min = time_to_minutes(start_time) - end_min = time_to_minutes(end_time) - - if start_min <= end_min: - # 同一天内的时间段 - for i in range(start_min, end_min): - if i < 1440: - covered[i] = True - else: - # 跨天的时间段 - for i in range(start_min, 1440): - covered[i] = True - for i in range(0, end_min): - covered[i] = True - - # 检查是否所有分钟都被覆盖 - return all(covered) - class ScheduleManager: def __init__(self): self.today_schedule: Optional[List[Dict[str, Any]]] = None - self.llm = LLMRequest(model_set=model_config.model_task_config.schedule_generator, request_type="schedule") - self.max_retries = -1 # 无限重试,直到成功生成标准日程表 + self.llm_generator = ScheduleLLMGenerator() + self.plan_manager = PlanManager() self.daily_task_started = False - self.schedule_generation_running = False # 防止重复生成任务 + self.schedule_generation_running = False async def start_daily_schedule_generation(self): - """启动每日零点自动生成新日程的任务""" if not self.daily_task_started: logger.info("正在启动每日日程生成任务...") task = DailyScheduleGenerationTask(self) @@ -147,35 +35,20 @@ class ScheduleManager: logger.info("每日日程生成任务已在运行中。") async def load_or_generate_today_schedule(self): - # 检查是否启用日程管理功能 - if not global_config.schedule.enable: + if not global_config.planning_system.schedule_enable: logger.info("日程管理功能已禁用,跳过日程加载和生成。") return today_str = datetime.now().strftime("%Y-%m-%d") try: - with get_db_session() as session: - schedule_record = session.query(Schedule).filter(Schedule.date == today_str).first() - if schedule_record: - logger.info(f"从数据库加载今天的日程 ({today_str})。") - schedule_data = orjson.loads(str(schedule_record.schedule_data)) - if self._validate_schedule_with_pydantic(schedule_data): - self.today_schedule = schedule_data - schedule_sleep_bridge.update_today_schedule(self.today_schedule) # 更新桥接器中的日程 - schedule_str = f"已成功加载今天的日程 ({today_str}):\n" - if self.today_schedule: - for item in self.today_schedule: - schedule_str += ( - f" - {item.get('time_range', '未知时间')}: {item.get('activity', '未知活动')}\n" - ) - logger.info(schedule_str) - return # 成功加载,直接返回 - else: - logger.warning("数据库中的日程数据格式无效,将重新生成日程") - else: - logger.info(f"数据库中未找到今天的日程 ({today_str}),将调用 LLM 生成。") + schedule_data = self._load_schedule_from_db(today_str) + if schedule_data: + self.today_schedule = schedule_data + schedule_sleep_bridge.update_today_schedule(self.today_schedule) + self._log_loaded_schedule(today_str) + return - # 仅在需要时生成 + logger.info(f"数据库中未找到今天的日程 ({today_str}),将调用 LLM 生成。") await self.generate_and_save_schedule() except Exception as e: @@ -183,258 +56,107 @@ class ScheduleManager: logger.info("尝试生成日程作为备用方案...") await self.generate_and_save_schedule() + def _load_schedule_from_db(self, date_str: str) -> Optional[List[Dict[str, Any]]]: + with get_db_session() as session: + schedule_record = session.query(Schedule).filter(Schedule.date == date_str).first() + if schedule_record: + logger.info(f"从数据库加载今天的日程 ({date_str})。") + schedule_data = orjson.loads(str(schedule_record.schedule_data)) + if self._validate_schedule_with_pydantic(schedule_data): + return schedule_data + else: + logger.warning("数据库中的日程数据格式无效,将重新生成日程") + return None + + def _log_loaded_schedule(self, date_str: str): + schedule_str = f"已成功加载今天的日程 ({date_str}):\n" + if self.today_schedule: + for item in self.today_schedule: + schedule_str += f" - {item.get('time_range', '未知时间')}: {item.get('activity', '未知活动')}\n" + logger.info(schedule_str) + async def generate_and_save_schedule(self): - """将日程生成任务提交到后台执行""" if self.schedule_generation_running: logger.info("日程生成任务已在运行中,跳过重复启动") return - logger.info("检测到需要生成日程,已提交后台任务。") task = OnDemandScheduleGenerationTask(self) await async_task_manager.add_task(task) async def _async_generate_and_save_schedule(self): - """异步生成并保存日程的内部方法""" self.schedule_generation_running = True - try: - now = datetime.now() - today_str = now.strftime("%Y-%m-%d") - current_month_str = now.strftime("%Y-%m") - weekday = now.strftime("%A") + today_str = datetime.now().strftime("%Y-%m-%d") + current_month_str = datetime.now().strftime("%Y-%m") - # 新增:获取节日信息 - lunar = Lunar.fromDate(now) - festivals = lunar.getFestivals() - other_festivals = lunar.getOtherFestivals() - all_festivals = festivals + other_festivals + sampled_plans = [] + if global_config.planning_system.monthly_plan_enable: + await self.plan_manager.ensure_and_generate_plans_if_needed(current_month_str) + sampled_plans = self.plan_manager.get_plans_for_schedule(current_month_str, max_count=3) - festival_block = "" - if all_festivals: - festival_text = "、".join(all_festivals) - festival_block = f"**今天也是一个特殊的日子: {festival_text}!请在日程中考虑和庆祝这个节日。**" + schedule_data = await self.llm_generator.generate_schedule_with_llm(sampled_plans) - # 获取月度计划作为额外参考 - monthly_plans_block = "" - used_plan_ids = [] - if global_config.monthly_plan_system and global_config.monthly_plan_system.enable: - # 使用新的智能抽取逻辑 - avoid_days = getattr(global_config.monthly_plan_system, "avoid_repetition_days", 7) - # 使用新的智能抽取逻辑 - avoid_days = getattr(global_config.monthly_plan_system, "avoid_repetition_days", 7) - sampled_plans = get_smart_plans_for_daily_schedule( - current_month_str, max_count=3, avoid_days=avoid_days - ) - - # 如果计划耗尽,则触发补充生成 - if not sampled_plans: - logger.info("可用的月度计划已耗尽或不足,触发后台补充生成...") - from mmc.src.schedule.monthly_plan_manager import monthly_plan_manager - - # 等待月度计划生成完成 - await monthly_plan_manager.ensure_and_generate_plans_if_needed(current_month_str) - - # 重新获取月度计划 - sampled_plans = get_smart_plans_for_daily_schedule( - current_month_str, max_count=3, avoid_days=avoid_days - ) - logger.info("月度计划补充生成完毕,继续日程生成任务。") + if schedule_data: + self._save_schedule_to_db(today_str, schedule_data) + self.today_schedule = schedule_data + schedule_sleep_bridge.update_today_schedule(self.today_schedule) + self._log_generated_schedule(today_str, schedule_data) if sampled_plans: - plan_texts = "\n".join([f"- {plan.plan_text}" for plan in sampled_plans]) - monthly_plans_block = f""" -**我这个月的一些小目标/计划 (请在今天的日程中适当体现)**: -{plan_texts} -""" - - guidelines = global_config.schedule.guidelines or DEFAULT_SCHEDULE_GUIDELINES - personality = global_config.personality.personality_core - personality_side = global_config.personality.personality_side - - base_prompt = f""" -我,{global_config.bot.nickname},需要为自己规划一份今天({today_str},星期{weekday})的详细日程安排。 -{festival_block} -**关于我**: -- **核心人设**: {personality} -- **具体习惯与兴趣**: -{personality_side} -{monthly_plans_block} -**我今天的规划原则**: -{guidelines} - -**重要要求**: -1. 必须返回一个完整的、有效的JSON数组格式 -2. 数组中的每个对象都必须包含 "time_range" 和 "activity" 两个键 -3. 时间范围必须覆盖全部24小时,不能有遗漏 -4. time_range格式必须为 "HH:MM-HH:MM" (24小时制) -5. 相邻的时间段必须连续,不能有间隙 -6. 不要包含任何JSON以外的解释性文字或代码块标记 -**示例**: -[ - {{"time_range": "00:00-07:00", "activity": "进入梦乡,处理数据"}}, - {{"time_range": "07:00-08:00", "activity": "起床伸个懒腰,看看今天有什么新闻"}}, - {{"time_range": "08:00-09:00", "activity": "享用早餐,规划今天的任务"}}, - {{"time_range": "09:00-23:30", "activity": "其他活动"}}, - {{"time_range": "23:30-00:00", "activity": "准备休眠"}} -] - -请你扮演我,以我的身份和口吻,为我生成一份完整的24小时日程表。 -""" - - # 无限重试直到生成成功的标准日程表 - attempt = 0 - while True: - attempt += 1 - try: - logger.info(f"正在生成日程 (第 {attempt} 次尝试)") - - # 构建当前尝试的prompt,增加压力提示 - prompt = base_prompt - if attempt > 1: - failure_hint = f""" - -**重要提醒 (第{attempt}次尝试)**: -- 前面{attempt - 1}次生成都失败了,请务必严格按照要求生成完整的24小时日程 -- 确保JSON格式正确,所有时间段连续覆盖24小时 -- 时间格式必须为HH:MM-HH:MM,不能有时间间隙或重叠 -- 不要输出任何解释文字,只输出纯JSON数组 -- 确保输出完整,不要被截断 -""" - prompt += failure_hint - - response, _ = await self.llm.generate_response_async(prompt) - - # 尝试解析和验证JSON(项目内置的反截断机制会自动处理截断问题) - schedule_data = orjson.loads(repair_json(response)) - - # 使用Pydantic验证生成的日程数据 - if self._validate_schedule_with_pydantic(schedule_data): - # 验证通过,保存到数据库 - with get_db_session() as session: - # 检查是否已存在今天的日程 - existing_schedule = session.query(Schedule).filter(Schedule.date == today_str).first() - if existing_schedule: - # 更新现有日程 - session.query(Schedule).filter(Schedule.date == today_str).update( - { - Schedule.schedule_data: orjson.dumps(schedule_data).decode("utf-8"), - Schedule.updated_at: datetime.now(), - } - ) - else: - # 创建新日程 - new_schedule = Schedule( - date=today_str, schedule_data=orjson.dumps(schedule_data).decode("utf-8") - ) - session.add(new_schedule) - session.commit() - - # 美化输出 - schedule_str = f"✅ 经过 {attempt} 次尝试,成功生成并保存今天的日程 ({today_str}):\n" - for item in schedule_data: - schedule_str += ( - f" - {item.get('time_range', '未知时间')}: {item.get('activity', '未知活动')}\n" - ) - logger.info(schedule_str) - - self.today_schedule = schedule_data - schedule_sleep_bridge.update_today_schedule(self.today_schedule) - - # 成功生成日程后,更新使用过的月度计划的统计信息 - if used_plan_ids and global_config.monthly_plan_system: - logger.info(f"更新使用过的月度计划 {used_plan_ids} 的统计信息。") - update_plan_usage(used_plan_ids, today_str) # type: ignore - - # 成功生成,退出无限循环 - break - - else: - logger.warning(f"第 {attempt} 次生成的日程验证失败,继续重试...") - # 添加短暂延迟,避免过于频繁的请求 - await asyncio.sleep(2) - - except Exception as e: - logger.error(f"第 {attempt} 次生成日程失败: {e}") - logger.info("继续重试...") - # 添加短暂延迟,避免过于频繁的请求 - await asyncio.sleep(3) - + used_plan_ids = [plan.id for plan in sampled_plans] + logger.info(f"更新使用过的月度计划 {used_plan_ids} 的统计信息。") + update_plan_usage(used_plan_ids, today_str) finally: self.schedule_generation_running = False logger.info("日程生成任务结束") + def _save_schedule_to_db(self, date_str: str, schedule_data: List[Dict[str, Any]]): + with get_db_session() as session: + schedule_json = orjson.dumps(schedule_data).decode("utf-8") + existing_schedule = session.query(Schedule).filter(Schedule.date == date_str).first() + if existing_schedule: + session.query(Schedule).filter(Schedule.date == date_str).update( + {Schedule.schedule_data: schedule_json, Schedule.updated_at: datetime.now()} + ) + else: + new_schedule = Schedule(date=date_str, schedule_data=schedule_json) + session.add(new_schedule) + session.commit() + + def _log_generated_schedule(self, date_str: str, schedule_data: List[Dict[str, Any]]): + schedule_str = f"✅ 成功生成并保存今天的日程 ({date_str}):\n" + for item in schedule_data: + schedule_str += f" - {item.get('time_range', '未知时间')}: {item.get('activity', '未知活动')}\n" + logger.info(schedule_str) + def get_current_activity(self) -> Optional[str]: - # 检查是否启用日程管理功能 - if not global_config.schedule.enable: + if not global_config.planning_system.schedule_enable or not self.today_schedule: return None - - if not self.today_schedule: - return None - now = datetime.now().time() for event in self.today_schedule: try: time_range = event.get("time_range") activity = event.get("activity") - if not time_range or not activity: - logger.warning(f"日程事件缺少必要字段: {event}") continue - start_str, end_str = time_range.split("-") start_time = datetime.strptime(start_str.strip(), "%H:%M").time() end_time = datetime.strptime(end_str.strip(), "%H:%M").time() - - if start_time <= end_time: - if start_time <= now < end_time: - return activity - else: # 跨天事件 - if now >= start_time or now < end_time: - return activity + if (start_time <= now < end_time) or (end_time < start_time and (now >= start_time or now < end_time)): + return activity except (ValueError, KeyError, AttributeError) as e: logger.warning(f"解析日程事件失败: {event}, 错误: {e}") - continue return None def _validate_schedule_with_pydantic(self, schedule_data) -> bool: - """使用Pydantic验证日程数据格式和完整性""" try: - # 尝试用Pydantic模型验证 ScheduleData(schedule=schedule_data) - logger.info("日程数据Pydantic验证通过") return True - except ValidationError as e: - logger.warning(f"日程数据Pydantic验证失败: {e}") + except Exception: return False - except Exception as e: - logger.error(f"日程数据验证时发生异常: {e}") - return False - - def _validate_schedule_data(self, schedule_data) -> bool: - """保留原有的基础验证方法作为备用""" - if not isinstance(schedule_data, list): - logger.warning("日程数据不是列表格式") - return False - - for item in schedule_data: - if not isinstance(item, dict): - logger.warning(f"日程项不是字典格式: {item}") - return False - - if "time_range" not in item or "activity" not in item: - logger.warning(f"日程项缺少必要字段 (time_range 或 activity): {item}") - return False - - if not isinstance(item["time_range"], str) or not isinstance(item["activity"], str): - logger.warning(f"日程项字段类型不正确: {item}") - return False - - return True class OnDemandScheduleGenerationTask(AsyncTask): - """按需生成日程的后台任务""" - def __init__(self, schedule_manager: "ScheduleManager"): task_name = f"OnDemandScheduleGenerationTask-{datetime.now().strftime('%Y%m%d%H%M%S')}" super().__init__(task_name=task_name) @@ -447,8 +169,6 @@ class OnDemandScheduleGenerationTask(AsyncTask): class DailyScheduleGenerationTask(AsyncTask): - """每日零点自动生成新日程的任务""" - def __init__(self, schedule_manager: "ScheduleManager"): super().__init__(task_name="DailyScheduleGenerationTask") self.schedule_manager = schedule_manager @@ -456,29 +176,21 @@ class DailyScheduleGenerationTask(AsyncTask): async def run(self): while True: try: - # 1. 计算到下一个零点的时间 now = datetime.now() tomorrow = now.date() + timedelta(days=1) midnight = datetime.combine(tomorrow, time.min) sleep_seconds = (midnight - now).total_seconds() - logger.info( f"下一次日程生成任务将在 {sleep_seconds:.2f} 秒后运行 (北京时间 {midnight.strftime('%Y-%m-%d %H:%M:%S')})" ) - - # 2. 等待直到零点 await asyncio.sleep(sleep_seconds) - - # 3. 执行日程生成 logger.info("到达每日零点,开始生成新的一天日程...") await self.schedule_manager._async_generate_and_save_schedule() - except asyncio.CancelledError: logger.info("每日日程生成任务被取消。") break except Exception as e: logger.error(f"每日日程生成任务发生未知错误: {e}") - # 发生错误后,等待5分钟再重试,避免频繁失败 await asyncio.sleep(300) diff --git a/src/schedule/schemas.py b/src/schedule/schemas.py new file mode 100644 index 000000000..5eb7c003a --- /dev/null +++ b/src/schedule/schemas.py @@ -0,0 +1,99 @@ +# mmc/src/schedule/schemas.py + +from datetime import datetime, time +from typing import List +from pydantic import BaseModel, validator + + +class ScheduleItem(BaseModel): + """单个日程项的Pydantic模型""" + + time_range: str + activity: str + + @validator("time_range") + def validate_time_range(cls, v): + """验证时间范围格式""" + if not v or "-" not in v: + raise ValueError("时间范围必须包含'-'分隔符") + + try: + start_str, end_str = v.split("-", 1) + start_str = start_str.strip() + end_str = end_str.strip() + + # 验证时间格式 + datetime.strptime(start_str, "%H:%M") + datetime.strptime(end_str, "%H:%M") + + return v + except ValueError as e: + raise ValueError(f"时间格式无效,应为HH:MM-HH:MM格式: {e}") from e + + @validator("activity") + def validate_activity(cls, v): + """验证活动描述""" + if not v or not v.strip(): + raise ValueError("活动描述不能为空") + return v.strip() + + +class ScheduleData(BaseModel): + """完整日程数据的Pydantic模型""" + + schedule: List[ScheduleItem] + + @validator("schedule") + def validate_schedule_completeness(cls, v): + """验证日程是否覆盖24小时""" + if not v: + raise ValueError("日程不能为空") + + # 收集所有时间段 + time_ranges = [] + for item in v: + try: + start_str, end_str = item.time_range.split("-", 1) + start_time = datetime.strptime(start_str.strip(), "%H:%M").time() + end_time = datetime.strptime(end_str.strip(), "%H:%M").time() + time_ranges.append((start_time, end_time)) + except ValueError: + continue + + # 检查是否覆盖24小时 + if not cls._check_24_hour_coverage(time_ranges): + raise ValueError("日程必须覆盖完整的24小时") + + return v + + @staticmethod + def _check_24_hour_coverage(time_ranges: List[tuple]) -> bool: + """检查时间段是否覆盖24小时""" + if not time_ranges: + return False + + # 将时间转换为分钟数进行计算 + def time_to_minutes(t: time) -> int: + return t.hour * 60 + t.minute + + # 创建覆盖情况数组 (1440分钟 = 24小时) + covered = [False] * 1440 + + for start_time, end_time in time_ranges: + start_min = time_to_minutes(start_time) + end_min = time_to_minutes(end_time) + + if start_min <= end_min: + # 同一天内的时间段 + for i in range(start_min, end_min): + if i < 1440: + covered[i] = True + else: + # 跨天的时间段 + for i in range(start_min, 1440): + covered[i] = True + for i in range(0, end_min): + covered[i] = True + + # 检查是否所有分钟都被覆盖 + return all(covered) \ No newline at end of file diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index fceec7d8b..f986a2cba 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -358,16 +358,28 @@ use_wss = false # 是否使用WSS安全连接,只支持ws模式 cert_file = "" # SSL证书文件路径,仅在use_wss=true时有效 key_file = "" # SSL密钥文件路径,仅在use_wss=true时有效 -[schedule] #日程管理 -enable = false # 是否启用日程管理功能 -# 日程生成指导原则,如果不设置则使用默认原则 -guidelines = """ +[planning_system] # 规划系统配置 +# --- 日程生成 --- +schedule_enable = true # 是否启用每日日程生成功能 +schedule_guidelines = """ 我希望你每天都能过得充实而有趣。 请确保你的日程里有学习新知识的时间,这是你成长的关键。 但也不要忘记放松,可以看看视频、听听音乐或者玩玩游戏。 晚上我希望你能多和朋友们交流,维系好彼此的关系。 另外,请保证充足的休眠时间来处理和整合一天的数据。 """ +# --- 月度计划 --- +monthly_plan_enable = false # 是否启用月度计划系统 +monthly_plan_guidelines = """ +我希望你能为自己制定一些有意义的月度小目标和计划。 +这些计划应该涵盖学习、娱乐、社交、个人成长等各个方面。 +每个计划都应该是具体可行的,能够在一个月内通过日常活动逐步实现。 +请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。 +""" +max_plans_per_month = 10 # 每月最多生成的计划数量 +avoid_repetition_days = 7 # 避免在多少天内重复使用同一个月度计划 +completion_threshold = 3 # 一个月度计划被使用多少次后算作完成 + [video_analysis] # 视频分析配置 enable = true # 是否启用视频分析功能 analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢)、"batch_frames"(批量分析,推荐)或 "auto"(自动选择) @@ -415,26 +427,6 @@ exa_api_keys = ["None"]# EXA API密钥列表,支持轮询机制 enabled_engines = ["ddg"] # 启用的搜索引擎列表,可选: "exa", "tavily", "ddg","bing" search_strategy = "single" # 搜索策略: "single"(使用第一个可用引擎), "parallel"(并行使用所有启用的引擎), "fallback"(按顺序尝试,失败则尝试下一个) -# ---------------------------------------------------------------- -# 月度计划系统设置 (Monthly Plan System Settings) -# ---------------------------------------------------------------- -[monthly_plan_system] -# 是否启用本功能 -enable = true -# 每个月允许存在的最大计划数量 -max_plans_per_month = 20 -# 计划使用多少次后自动标记为已完成 -completion_threshold = 3 -# 多少天内不重复抽取同一个计划 -avoid_repetition_days = 7 -# 月度计划生成的指导原则(可选,如果不设置则使用默认原则) -guidelines = """ -我希望你能为自己制定一些有意义的月度小目标和计划。 -这些计划应该涵盖学习、娱乐、社交、个人成长等各个方面。 -每个计划都应该是具体可行的,能够在一个月内通过日常活动逐步实现。 -请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。 -""" - [sleep_system] enable = false #"是否启用睡眠系统" wakeup_threshold = 15.0 #唤醒阈值,达到此值时会被唤醒" From 8e21502c31eaffd030ec008ada57774a4f48825c Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 12:16:11 +0800 Subject: [PATCH 19/69] =?UTF-8?q?refactor(sleep):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=9D=A1=E7=9C=A0=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F=EF=BC=8C?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E6=A1=A5=E6=8E=A5=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 `SleepManager` 从 `schedule_bridge` 中独立出来,使其成为一个自包含的组件。现在由 `HeartFChatting` 和 `ScheduleManager` 直接实例化和使用 `SleepManager`,移除了原有的全局单例桥接器。 主要变更: - 删除了 `schedule_bridge.py`,其功能被直接整合到 `SleepManager` 和调用方中。 - `SleepManager` 内部逻辑被拆分为更小的模块,如 `TimeChecker`、`NotificationSender` 和 `SleepStateSerializer`,以提高内聚性和可测试性。 - `HeartFChatting` 现在直接创建并管理 `SleepManager` 实例,并通过 `HfcContext` 传递给其他需要的组件(如 `WakeUpManager`)。 - `ScheduleManager` 也创建自己的 `SleepManager` 实例来更新日程,简化了依赖关系。 - 简化了 `WakeUpManager` 中获取睡眠状态和重置状态的逻辑,直接通过上下文访问 `SleepManager`。 --- src/chat/chat_loop/heartFC_chat.py | 15 +- src/chat/chat_loop/hfc_context.py | 2 + .../sleep_manager/notification_sender.py | 68 ++++++ .../sleep_manager/schedule_bridge.py | 54 ----- .../chat_loop/sleep_manager/sleep_manager.py | 226 +++--------------- .../chat_loop/sleep_manager/sleep_state.py | 77 ++++++ .../chat_loop/sleep_manager/time_checker.py | 68 ++++++ .../chat_loop/sleep_manager/wakeup_manager.py | 14 +- src/config/official_configs.py | 3 + src/schedule/schedule_manager.py | 7 +- 10 files changed, 272 insertions(+), 262 deletions(-) create mode 100644 src/chat/chat_loop/sleep_manager/notification_sender.py delete mode 100644 src/chat/chat_loop/sleep_manager/schedule_bridge.py create mode 100644 src/chat/chat_loop/sleep_manager/sleep_state.py create mode 100644 src/chat/chat_loop/sleep_manager/time_checker.py diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index cc86b245f..b93931cbb 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -9,8 +9,7 @@ from src.common.logger import get_logger from src.config.config import global_config from src.person_info.relationship_builder_manager import relationship_builder_manager from src.chat.express.expression_learner import expression_learner_manager -from src.chat.chat_loop.sleep_manager.schedule_bridge import schedule_sleep_bridge -from src.chat.chat_loop.sleep_manager.sleep_manager import SleepState +from src.chat.chat_loop.sleep_manager.sleep_manager import SleepManager, SleepState from src.plugin_system.apis import message_api from .hfc_context import HfcContext @@ -47,10 +46,12 @@ class HeartFChatting: self.energy_manager = EnergyManager(self.context) self.proactive_thinker = ProactiveThinker(self.context, self.cycle_processor) self.wakeup_manager = WakeUpManager(self.context) + self.sleep_manager = SleepManager() # 将唤醒度管理器设置到上下文中 self.context.wakeup_manager = self.wakeup_manager self.context.energy_manager = self.energy_manager + self.context.sleep_manager = self.sleep_manager # 将HeartFChatting实例设置到上下文中,以便其他组件可以调用其方法 self.context.chat_instance = self @@ -352,8 +353,8 @@ class HeartFChatting: - NORMAL模式:检查进入FOCUS模式的条件,并通过normal_mode_handler处理消息 """ # --- 核心状态更新 --- - await schedule_sleep_bridge.update_sleep_state(self.wakeup_manager) - current_sleep_state = schedule_sleep_bridge.get_current_sleep_state() + await self.sleep_manager.update_sleep_state(self.wakeup_manager) + current_sleep_state = self.sleep_manager.get_current_sleep_state() is_sleeping = current_sleep_state == SleepState.SLEEPING is_in_insomnia = current_sleep_state == SleepState.INSOMNIA @@ -383,7 +384,7 @@ class HeartFChatting: self._handle_wakeup_messages(recent_messages) # 再次获取最新状态,因为 handle_wakeup 可能导致状态变为 WOKEN_UP - current_sleep_state = schedule_sleep_bridge.get_current_sleep_state() + current_sleep_state = self.sleep_manager.get_current_sleep_state() if current_sleep_state == SleepState.SLEEPING: # 只有在纯粹的 SLEEPING 状态下才跳过消息处理 @@ -429,14 +430,14 @@ class HeartFChatting: # --- 重新入睡逻辑 --- # 如果被吵醒了,并且在一定时间内没有新消息,则尝试重新入睡 - if schedule_sleep_bridge.get_current_sleep_state() == SleepState.WOKEN_UP and not has_new_messages: + if self.sleep_manager.get_current_sleep_state() == SleepState.WOKEN_UP and not has_new_messages: re_sleep_delay = global_config.sleep_system.re_sleep_delay_minutes * 60 # 使用 last_message_time 来判断空闲时间 if time.time() - self.context.last_message_time > re_sleep_delay: logger.info( f"{self.context.log_prefix} 已被唤醒且超过 {re_sleep_delay / 60} 分钟无新消息,尝试重新入睡。" ) - schedule_sleep_bridge.reset_sleep_state_after_wakeup() + self.sleep_manager.reset_sleep_state_after_wakeup() # 保存HFC上下文状态 self.context.save_context_state() diff --git a/src/chat/chat_loop/hfc_context.py b/src/chat/chat_loop/hfc_context.py index fca6a65e4..4e7ec1e2f 100644 --- a/src/chat/chat_loop/hfc_context.py +++ b/src/chat/chat_loop/hfc_context.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from .sleep_manager.wakeup_manager import WakeUpManager from .energy_manager import EnergyManager from .heartFC_chat import HeartFChatting + from .sleep_manager.sleep_manager import SleepManager class HfcContext: @@ -61,6 +62,7 @@ class HfcContext: # 唤醒度管理器 - 延迟初始化以避免循环导入 self.wakeup_manager: Optional["WakeUpManager"] = None self.energy_manager: Optional["EnergyManager"] = None + self.sleep_manager: Optional["SleepManager"] = None self.focus_energy = 1 self.no_reply_consecutive = 0 diff --git a/src/chat/chat_loop/sleep_manager/notification_sender.py b/src/chat/chat_loop/sleep_manager/notification_sender.py new file mode 100644 index 000000000..5230c4a11 --- /dev/null +++ b/src/chat/chat_loop/sleep_manager/notification_sender.py @@ -0,0 +1,68 @@ +import asyncio +import random +import hashlib +from src.common.logger import get_logger +from src.config.config import global_config +from src.plugin_system.apis import send_api, generator_api + +logger = get_logger("notification_sender") + + +class NotificationSender: + @staticmethod + async def send_pre_sleep_notification(): + """异步生成并发送睡前通知""" + try: + groups = global_config.sleep_system.pre_sleep_notification_groups + prompt = global_config.sleep_system.pre_sleep_prompt + + if not groups: + logger.info("未配置睡前通知的群组,跳过发送。") + return + + if not prompt: + logger.warning("睡前通知的prompt为空,跳过发送。") + return + + # 为防止消息风暴,稍微延迟一下 + await asyncio.sleep(random.uniform(5, 15)) + + for group_id_str in groups: + try: + # 格式 "platform:group_id" + parts = group_id_str.split(":") + if len(parts) != 2: + logger.warning(f"无效的群组ID格式: {group_id_str}") + continue + + platform, group_id = parts + + # 使用与 ChatStream.get_stream_id 相同的逻辑生成 stream_id + key = "_".join([platform, group_id]) + stream_id = hashlib.md5(key.encode()).hexdigest() + + logger.info(f"正在为群组 {group_id_str} (Stream ID: {stream_id}) 生成睡前消息...") + + # 调用 generator_api 生成回复 + success, reply_set, _ = await generator_api.generate_reply( + chat_id=stream_id, extra_info=prompt, request_type="schedule.pre_sleep_notification" + ) + + if success and reply_set: + # 提取文本内容并发送 + reply_text = "".join([content for msg_type, content in reply_set if msg_type == "text"]) + if reply_text: + logger.info(f"向群组 {group_id_str} 发送睡前消息: {reply_text}") + await send_api.text_to_stream(text=reply_text, stream_id=stream_id) + else: + logger.warning(f"为群组 {group_id_str} 生成的回复内容为空。") + else: + logger.error(f"为群组 {group_id_str} 生成睡前消息失败。") + + await asyncio.sleep(random.uniform(2, 5)) # 避免发送过快 + + except Exception as e: + logger.error(f"向群组 {group_id_str} 发送睡前消息失败: {e}") + + except Exception as e: + logger.error(f"发送睡前通知任务失败: {e}") \ No newline at end of file diff --git a/src/chat/chat_loop/sleep_manager/schedule_bridge.py b/src/chat/chat_loop/sleep_manager/schedule_bridge.py deleted file mode 100644 index 0ac15f2f5..000000000 --- a/src/chat/chat_loop/sleep_manager/schedule_bridge.py +++ /dev/null @@ -1,54 +0,0 @@ -# mmc/src/chat/chat_loop/sleep_manager/schedule_bridge.py - -""" -此模块充当 ScheduleManager 和 SleepManager 之间的桥梁, -将睡眠逻辑与日程生成逻辑解耦。 -""" - -from typing import Optional, TYPE_CHECKING, List, Dict, Any - -from .sleep_manager import SleepManager, SleepState - -if TYPE_CHECKING: - from src.chat.chat_loop.sleep_manager.wakeup_manager import WakeUpManager - - -class ScheduleSleepBridge: - def __init__(self): - # 桥接器现在持有 SleepManager 的唯一实例 - self.sleep_manager = SleepManager(self) - self.today_schedule: Optional[List[Dict[str, Any]]] = None - - def get_today_schedule(self) -> Optional[List[Dict[str, Any]]]: - """ - 向 SleepManager 提供当日日程。 - """ - return self.today_schedule - - def update_today_schedule(self, schedule: Optional[List[Dict[str, Any]]]): - """ - 由 ScheduleManager 调用以更新当日日程。 - """ - self.today_schedule = schedule - - # --- 代理方法,供应用程序的其他部分调用 --- - - def get_current_sleep_state(self) -> SleepState: - """从 SleepManager 获取当前的睡眠状态。""" - return self.sleep_manager.get_current_sleep_state() - - def is_sleeping(self) -> bool: - """检查当前是否处于正式休眠状态。""" - return self.sleep_manager.is_sleeping() - - async def update_sleep_state(self, wakeup_manager: Optional["WakeUpManager"] = None): - """更新睡眠状态机。""" - await self.sleep_manager.update_sleep_state(wakeup_manager) - - def reset_sleep_state_after_wakeup(self): - """被唤醒后,将状态切换到 WOKEN_UP。""" - self.sleep_manager.reset_sleep_state_after_wakeup() - - -# 创建一个全局可访问的桥接器单例 -schedule_sleep_bridge = ScheduleSleepBridge() \ No newline at end of file diff --git a/src/chat/chat_loop/sleep_manager/sleep_manager.py b/src/chat/chat_loop/sleep_manager/sleep_manager.py index 7c872c3b3..283466ec8 100644 --- a/src/chat/chat_loop/sleep_manager/sleep_manager.py +++ b/src/chat/chat_loop/sleep_manager/sleep_manager.py @@ -1,13 +1,13 @@ import asyncio import random -from datetime import datetime, timedelta, date, time -from enum import Enum, auto -from typing import Optional, TYPE_CHECKING +from datetime import datetime, timedelta, date +from typing import Optional, TYPE_CHECKING, List, Dict, Any from src.common.logger import get_logger from src.config.config import global_config -from src.manager.local_store_manager import local_storage -from src.plugin_system.apis import send_api, generator_api +from .sleep_state import SleepState, SleepStateSerializer +from .time_checker import TimeChecker +from .notification_sender import NotificationSender if TYPE_CHECKING: from mmc.src.chat.chat_loop.sleep_manager.wakeup_manager import WakeUpManager @@ -15,21 +15,12 @@ if TYPE_CHECKING: logger = get_logger("sleep_manager") -class SleepState(Enum): - """睡眠状态枚举""" - - AWAKE = auto() # 完全清醒 - INSOMNIA = auto() # 失眠(在理论睡眠时间内保持清醒) - PREPARING_SLEEP = auto() # 准备入睡(缓冲期) - SLEEPING = auto() # 正在休眠 - WOKEN_UP = auto() # 被吵醒 - - class SleepManager: - def __init__(self, bridge): - self.bridge = bridge + def __init__(self): + self.time_checker = TimeChecker(self) + self.today_schedule: Optional[List[Dict[str, Any]]] = None self.last_sleep_log_time = 0 - self.sleep_log_interval = 35 # 日志记录间隔,单位秒 + self.sleep_log_interval = 35 # --- 统一睡眠状态管理 --- self._current_state: SleepState = SleepState.AWAKE @@ -37,34 +28,26 @@ class SleepManager: self._total_delayed_minutes_today: int = 0 self._last_sleep_check_date: Optional[date] = None self._last_fully_slept_log_time: float = 0 - self._re_sleep_attempt_time: Optional[datetime] = None # 新增:重新入睡的尝试时间 + self._re_sleep_attempt_time: Optional[datetime] = None self._load_sleep_state() def get_current_sleep_state(self) -> SleepState: - """获取当前的睡眠状态""" return self._current_state def is_sleeping(self) -> bool: - """检查当前是否处于正式休眠状态""" return self._current_state == SleepState.SLEEPING async def update_sleep_state(self, wakeup_manager: Optional["WakeUpManager"] = None): - """ - 核心状态机:根据当前情况更新睡眠状态 - """ - # --- 基础检查 --- - today_schedule = self.bridge.get_today_schedule() - if not global_config.sleep_system.enable or not today_schedule: + if not global_config.sleep_system.enable: if self._current_state != SleepState.AWAKE: - logger.debug("睡眠系统禁用或无日程,强制设为 AWAKE") + logger.debug("睡眠系统禁用,强制设为 AWAKE") self._current_state = SleepState.AWAKE return now = datetime.now() today = now.date() - # --- 每日状态重置 --- if self._last_sleep_check_date != today: logger.info(f"新的一天 ({today}),重置睡眠状态为 AWAKE。") self._total_delayed_minutes_today = 0 @@ -73,23 +56,14 @@ class SleepManager: self._last_sleep_check_date = today self._save_sleep_state() - # --- 判断当前是否为理论上的睡眠时间 --- - is_in_theoretical_sleep, activity = self._is_in_theoretical_sleep_time(now.time()) + is_in_theoretical_sleep, activity = self.time_checker.is_in_theoretical_sleep_time(now.time()) - # =================================== - # 状态机核心逻辑 - # =================================== - - # 状态:清醒 (AWAKE) if self._current_state == SleepState.AWAKE: if is_in_theoretical_sleep: logger.info(f"进入理论休眠时间 '{activity}',开始进行睡眠决策...") - - # --- 合并后的失眠与弹性睡眠决策逻辑 --- sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999 pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold - # 决策1:因睡眠压力低而延迟入睡(原弹性睡眠) if ( sleep_pressure < pressure_threshold and self._total_delayed_minutes_today < global_config.sleep_system.max_sleep_delay_minutes @@ -101,12 +75,8 @@ class SleepManager: logger.info( f"睡眠压力 ({sleep_pressure:.1f}) 低于阈值 ({pressure_threshold}),进入失眠状态,延迟入睡 {delay_minutes} 分钟。" ) - - # 发送睡前通知 if global_config.sleep_system.enable_pre_sleep_notification: - asyncio.create_task(self._send_pre_sleep_notification()) - - # 决策2:进入正常的入睡准备流程 + asyncio.create_task(NotificationSender.send_pre_sleep_notification()) else: buffer_seconds = random.randint(5 * 60, 10 * 60) self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) @@ -114,14 +84,10 @@ class SleepManager: logger.info( f"睡眠压力正常或已达今日最大延迟,进入准备入睡状态,将在 {buffer_seconds / 60:.1f} 分钟内入睡。" ) - - # 发送睡前通知 if global_config.sleep_system.enable_pre_sleep_notification: - asyncio.create_task(self._send_pre_sleep_notification()) - + asyncio.create_task(NotificationSender.send_pre_sleep_notification()) self._save_sleep_state() - # 状态:失眠 (INSOMNIA) elif self._current_state == SleepState.INSOMNIA: if not is_in_theoretical_sleep: logger.info("已离开理论休眠时间,失眠结束,恢复清醒。") @@ -145,10 +111,8 @@ class SleepManager: delay_minutes = 15 self._total_delayed_minutes_today += delay_minutes self._sleep_buffer_end_time = now + timedelta(minutes=delay_minutes) - self._save_sleep_state() - # 状态:准备入睡 (PREPARING_SLEEP) elif self._current_state == SleepState.PREPARING_SLEEP: if not is_in_theoretical_sleep: logger.info("准备入睡期间离开理论休眠时间,取消入睡,恢复清醒。") @@ -161,20 +125,17 @@ class SleepManager: self._last_fully_slept_log_time = now.timestamp() self._save_sleep_state() - # 状态:休眠中 (SLEEPING) elif self._current_state == SleepState.SLEEPING: if not is_in_theoretical_sleep: logger.info("理论休眠时间结束,自然醒来。") self._current_state = SleepState.AWAKE self._save_sleep_state() else: - # 记录日志 current_timestamp = now.timestamp() if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval: logger.info(f"当前处于休眠活动 '{activity}' 中。") self.last_sleep_log_time = current_timestamp - # 状态:被吵醒 (WOKEN_UP) elif self._current_state == SleepState.WOKEN_UP: if not is_in_theoretical_sleep: logger.info("理论休眠时间结束,被吵醒的状态自动结束。") @@ -183,13 +144,12 @@ class SleepManager: self._save_sleep_state() elif self._re_sleep_attempt_time and now >= self._re_sleep_attempt_time: logger.info("被吵醒后经过一段时间,尝试重新入睡...") - sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999 pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold if sleep_pressure >= pressure_threshold: logger.info("睡眠压力足够,从被吵醒状态转换到准备入睡。") - buffer_seconds = random.randint(3 * 60, 8 * 60) # 重新入睡的缓冲期可以短一些 + buffer_seconds = random.randint(3 * 60, 8 * 60) self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) self._current_state = SleepState.PREPARING_SLEEP self._re_sleep_attempt_time = None @@ -199,156 +159,38 @@ class SleepManager: logger.info( f"睡眠压力({sleep_pressure:.1f})仍然较低,暂时保持清醒,在 {delay_minutes} 分钟后再次尝试。" ) - self._save_sleep_state() def reset_sleep_state_after_wakeup(self): - """被唤醒后,将状态切换到 WOKEN_UP""" if self._current_state in [SleepState.PREPARING_SLEEP, SleepState.SLEEPING, SleepState.INSOMNIA]: logger.info("被唤醒,进入 WOKEN_UP 状态!") self._current_state = SleepState.WOKEN_UP self._sleep_buffer_end_time = None - - # 设置一个延迟,之后再尝试重新入睡 re_sleep_delay_minutes = getattr(global_config.sleep_system, "re_sleep_delay_minutes", 10) self._re_sleep_attempt_time = datetime.now() + timedelta(minutes=re_sleep_delay_minutes) logger.info(f"将在 {re_sleep_delay_minutes} 分钟后尝试重新入睡。") - self._save_sleep_state() - def _is_in_theoretical_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]: - """检查当前时间是否落在日程表的任何一个睡眠活动中""" - sleep_keywords = ["休眠", "睡觉", "梦乡"] - today_schedule = self.bridge.get_today_schedule() - if today_schedule: - for event in today_schedule: - try: - activity = event.get("activity", "").strip() - time_range = event.get("time_range") + def get_today_schedule(self) -> Optional[List[Dict[str, Any]]]: + return self.today_schedule - if not activity or not time_range: - continue - - if any(keyword in activity for keyword in sleep_keywords): - start_str, end_str = time_range.split("-") - start_time = datetime.strptime(start_str.strip(), "%H:%M").time() - end_time = datetime.strptime(end_str.strip(), "%H:%M").time() - - if start_time <= end_time: # 同一天 - if start_time <= now_time < end_time: - return True, activity - else: # 跨天 - if now_time >= start_time or now_time < end_time: - return True, activity - except (ValueError, KeyError, AttributeError) as e: - logger.warning(f"解析日程事件时出错: {event}, 错误: {e}") - continue - - return False, None - - async def _send_pre_sleep_notification(self): - """异步生成并发送睡前通知""" - try: - groups = global_config.sleep_system.pre_sleep_notification_groups - prompt = global_config.sleep_system.pre_sleep_prompt - - if not groups: - logger.info("未配置睡前通知的群组,跳过发送。") - return - - if not prompt: - logger.warning("睡前通知的prompt为空,跳过发送。") - return - - # 为防止消息风暴,稍微延迟一下 - await asyncio.sleep(random.uniform(5, 15)) - - for group_id_str in groups: - try: - # 格式 "platform:group_id" - parts = group_id_str.split(":") - if len(parts) != 2: - logger.warning(f"无效的群组ID格式: {group_id_str}") - continue - - platform, group_id = parts - - # 使用与 ChatStream.get_stream_id 相同的逻辑生成 stream_id - import hashlib - - key = "_".join([platform, group_id]) - stream_id = hashlib.md5(key.encode()).hexdigest() - - logger.info(f"正在为群组 {group_id_str} (Stream ID: {stream_id}) 生成睡前消息...") - - # 调用 generator_api 生成回复 - success, reply_set, _ = await generator_api.generate_reply( - chat_id=stream_id, extra_info=prompt, request_type="schedule.pre_sleep_notification" - ) - - if success and reply_set: - # 提取文本内容并发送 - reply_text = "".join([content for msg_type, content in reply_set if msg_type == "text"]) - if reply_text: - logger.info(f"向群组 {group_id_str} 发送睡前消息: {reply_text}") - await send_api.text_to_stream(text=reply_text, stream_id=stream_id) - else: - logger.warning(f"为群组 {group_id_str} 生成的回复内容为空。") - else: - logger.error(f"为群组 {group_id_str} 生成睡前消息失败。") - - await asyncio.sleep(random.uniform(2, 5)) # 避免发送过快 - - except Exception as e: - logger.error(f"向群组 {group_id_str} 发送睡前消息失败: {e}") - - except Exception as e: - logger.error(f"发送睡前通知任务失败: {e}") + def update_today_schedule(self, schedule: Optional[List[Dict[str, Any]]]): + self.today_schedule = schedule def _save_sleep_state(self): - """将当前睡眠状态保存到本地存储""" - try: - state = { - "current_state": self._current_state.name, - "sleep_buffer_end_time_ts": self._sleep_buffer_end_time.timestamp() - if self._sleep_buffer_end_time - else None, - "total_delayed_minutes_today": self._total_delayed_minutes_today, - "last_sleep_check_date_str": self._last_sleep_check_date.isoformat() - if self._last_sleep_check_date - else None, - "re_sleep_attempt_time_ts": self._re_sleep_attempt_time.timestamp() - if self._re_sleep_attempt_time - else None, - } - local_storage["schedule_sleep_state"] = state - logger.debug(f"已保存睡眠状态: {state}") - except Exception as e: - logger.error(f"保存睡眠状态失败: {e}") + state_data = { + "_current_state": self._current_state, + "_sleep_buffer_end_time": self._sleep_buffer_end_time, + "_total_delayed_minutes_today": self._total_delayed_minutes_today, + "_last_sleep_check_date": self._last_sleep_check_date, + "_re_sleep_attempt_time": self._re_sleep_attempt_time, + } + SleepStateSerializer.save(state_data) def _load_sleep_state(self): - """从本地存储加载睡眠状态""" - try: - state = local_storage["schedule_sleep_state"] - if state and isinstance(state, dict): - state_name = state.get("current_state") - if state_name and hasattr(SleepState, state_name): - self._current_state = SleepState[state_name] - - end_time_ts = state.get("sleep_buffer_end_time_ts") - if end_time_ts: - self._sleep_buffer_end_time = datetime.fromtimestamp(end_time_ts) - - re_sleep_ts = state.get("re_sleep_attempt_time_ts") - if re_sleep_ts: - self._re_sleep_attempt_time = datetime.fromtimestamp(re_sleep_ts) - - self._total_delayed_minutes_today = state.get("total_delayed_minutes_today", 0) - - date_str = state.get("last_sleep_check_date_str") - if date_str: - self._last_sleep_check_date = datetime.fromisoformat(date_str).date() - - logger.info(f"成功从本地存储加载睡眠状态: {state}") - except Exception as e: - logger.warning(f"加载睡眠状态失败,将使用默认值: {e}") + state_data = SleepStateSerializer.load() + self._current_state = state_data["_current_state"] + self._sleep_buffer_end_time = state_data["_sleep_buffer_end_time"] + self._total_delayed_minutes_today = state_data["_total_delayed_minutes_today"] + self._last_sleep_check_date = state_data["_last_sleep_check_date"] + self._re_sleep_attempt_time = state_data["_re_sleep_attempt_time"] diff --git a/src/chat/chat_loop/sleep_manager/sleep_state.py b/src/chat/chat_loop/sleep_manager/sleep_state.py new file mode 100644 index 000000000..b8c994589 --- /dev/null +++ b/src/chat/chat_loop/sleep_manager/sleep_state.py @@ -0,0 +1,77 @@ +from enum import Enum, auto +from datetime import datetime +from typing import Optional +from src.common.logger import get_logger +from src.manager.local_store_manager import local_storage + +logger = get_logger("sleep_state") + + +class SleepState(Enum): + """睡眠状态枚举""" + + AWAKE = auto() + INSOMNIA = auto() + PREPARING_SLEEP = auto() + SLEEPING = auto() + WOKEN_UP = auto() + + +class SleepStateSerializer: + @staticmethod + def save(state_data: dict): + """将当前睡眠状态保存到本地存储""" + try: + state = { + "current_state": state_data["_current_state"].name, + "sleep_buffer_end_time_ts": state_data["_sleep_buffer_end_time"].timestamp() + if state_data["_sleep_buffer_end_time"] + else None, + "total_delayed_minutes_today": state_data["_total_delayed_minutes_today"], + "last_sleep_check_date_str": state_data["_last_sleep_check_date"].isoformat() + if state_data["_last_sleep_check_date"] + else None, + "re_sleep_attempt_time_ts": state_data["_re_sleep_attempt_time"].timestamp() + if state_data["_re_sleep_attempt_time"] + else None, + } + local_storage["schedule_sleep_state"] = state + logger.debug(f"已保存睡眠状态: {state}") + except Exception as e: + logger.error(f"保存睡眠状态失败: {e}") + + @staticmethod + def load() -> dict: + """从本地存储加载睡眠状态""" + state_data = { + "_current_state": SleepState.AWAKE, + "_sleep_buffer_end_time": None, + "_total_delayed_minutes_today": 0, + "_last_sleep_check_date": None, + "_re_sleep_attempt_time": None, + } + try: + state = local_storage["schedule_sleep_state"] + if state and isinstance(state, dict): + state_name = state.get("current_state") + if state_name and hasattr(SleepState, state_name): + state_data["_current_state"] = SleepState[state_name] + + end_time_ts = state.get("sleep_buffer_end_time_ts") + if end_time_ts: + state_data["_sleep_buffer_end_time"] = datetime.fromtimestamp(end_time_ts) + + re_sleep_ts = state.get("re_sleep_attempt_time_ts") + if re_sleep_ts: + state_data["_re_sleep_attempt_time"] = datetime.fromtimestamp(re_sleep_ts) + + state_data["_total_delayed_minutes_today"] = state.get("total_delayed_minutes_today", 0) + + date_str = state.get("last_sleep_check_date_str") + if date_str: + state_data["_last_sleep_check_date"] = datetime.fromisoformat(date_str).date() + + logger.info(f"成功从本地存储加载睡眠状态: {state}") + except Exception as e: + logger.warning(f"加载睡眠状态失败,将使用默认值: {e}") + return state_data \ No newline at end of file diff --git a/src/chat/chat_loop/sleep_manager/time_checker.py b/src/chat/chat_loop/sleep_manager/time_checker.py new file mode 100644 index 000000000..7e1b80b49 --- /dev/null +++ b/src/chat/chat_loop/sleep_manager/time_checker.py @@ -0,0 +1,68 @@ +from datetime import datetime, time +from typing import Optional, List, Dict, Any + +from src.common.logger import get_logger +from src.config.config import global_config + +logger = get_logger("time_checker") + + +class TimeChecker: + def __init__(self, schedule_source): + self.schedule_source = schedule_source + + def is_in_theoretical_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]: + if global_config.sleep_system.sleep_by_schedule: + if self.schedule_source.get_today_schedule(): + return self._is_in_schedule_sleep_time(now_time) + else: + return self._is_in_fixed_sleep_time(now_time) + else: + return self._is_in_fixed_sleep_time(now_time) + + def _is_in_schedule_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]: + """检查当前时间是否落在日程表的任何一个睡眠活动中""" + sleep_keywords = ["休眠", "睡觉", "梦乡"] + today_schedule = self.schedule_source.get_today_schedule() + if today_schedule: + for event in today_schedule: + try: + activity = event.get("activity", "").strip() + time_range = event.get("time_range") + + if not activity or not time_range: + continue + + if any(keyword in activity for keyword in sleep_keywords): + start_str, end_str = time_range.split("-") + start_time = datetime.strptime(start_str.strip(), "%H:%M").time() + end_time = datetime.strptime(end_str.strip(), "%H:%M").time() + + if start_time <= end_time: # 同一天 + if start_time <= now_time < end_time: + return True, activity + else: # 跨天 + if now_time >= start_time or now_time < end_time: + return True, activity + except (ValueError, KeyError, AttributeError) as e: + logger.warning(f"解析日程事件时出错: {event}, 错误: {e}") + continue + return False, None + + def _is_in_fixed_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]: + """检查当前时间是否在固定的睡眠时间内""" + try: + start_time_str = global_config.sleep_system.fixed_sleep_time + end_time_str = global_config.sleep_system.fixed_wake_up_time + start_time = datetime.strptime(start_time_str, "%H:%M").time() + end_time = datetime.strptime(end_time_str, "%H:%M").time() + + 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, "固定睡眠时间" + except ValueError as e: + logger.error(f"固定的睡眠时间格式不正确,请使用 HH:MM 格式: {e}") + return False, None \ No newline at end of file diff --git a/src/chat/chat_loop/sleep_manager/wakeup_manager.py b/src/chat/chat_loop/sleep_manager/wakeup_manager.py index b72fe80de..df2bf835e 100644 --- a/src/chat/chat_loop/sleep_manager/wakeup_manager.py +++ b/src/chat/chat_loop/sleep_manager/wakeup_manager.py @@ -138,10 +138,13 @@ class WakeUpManager: return False # 只有在休眠且非失眠状态下才累积唤醒度 - from src.schedule.schedule_manager import schedule_manager from mmc.src.chat.chat_loop.sleep_manager.sleep_manager import SleepState - current_sleep_state = schedule_manager.get_current_sleep_state() + sleep_manager = self.context.sleep_manager + if not sleep_manager: + return False + + current_sleep_state = sleep_manager.get_current_sleep_state() if current_sleep_state != SleepState.SLEEPING: return False @@ -191,10 +194,9 @@ class WakeUpManager: mood_manager.set_angry_from_wakeup(self.context.stream_id) - # 通知日程管理器重置睡眠状态 - from src.schedule.schedule_manager import schedule_manager - - schedule_manager.reset_sleep_state_after_wakeup() + # 通知SleepManager重置睡眠状态 + if self.context.sleep_manager: + self.context.sleep_manager.reset_sleep_state_after_wakeup() logger.info(f"{self.context.log_prefix} 唤醒度达到阈值({self.wakeup_threshold}),被吵醒进入愤怒状态!") diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 8c3600998..3a7ab3fb7 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -609,6 +609,9 @@ class SleepSystemConfig(ValidatedConfigBase): """睡眠系统配置类""" enable: bool = Field(default=True, description="是否启用睡眠系统") + sleep_by_schedule: bool = Field(default=True, description="是否根据日程表进行睡觉") + fixed_sleep_time: str = Field(default="23:00", description="固定的睡觉时间") + fixed_wake_up_time: str = Field(default="07:00", description="固定的起床时间") wakeup_threshold: float = Field(default=15.0, ge=1.0, description="唤醒阈值,达到此值时会被唤醒") private_message_increment: float = Field(default=3.0, ge=0.1, description="私聊消息增加的唤醒度") group_mention_increment: float = Field(default=2.0, ge=0.1, description="群聊艾特增加的唤醒度") diff --git a/src/schedule/schedule_manager.py b/src/schedule/schedule_manager.py index cb99f7915..491f106fa 100644 --- a/src/schedule/schedule_manager.py +++ b/src/schedule/schedule_manager.py @@ -7,7 +7,7 @@ from src.common.database.sqlalchemy_models import Schedule, get_db_session from src.config.config import global_config from src.common.logger import get_logger from src.manager.async_task_manager import AsyncTask, async_task_manager -from ..chat.chat_loop.sleep_manager.schedule_bridge import schedule_sleep_bridge +from ..chat.chat_loop.sleep_manager.sleep_manager import SleepManager from .database import update_plan_usage from .llm_generator import ScheduleLLMGenerator from .plan_manager import PlanManager @@ -23,6 +23,7 @@ class ScheduleManager: self.plan_manager = PlanManager() self.daily_task_started = False self.schedule_generation_running = False + self.sleep_manager = SleepManager() async def start_daily_schedule_generation(self): if not self.daily_task_started: @@ -44,7 +45,7 @@ class ScheduleManager: schedule_data = self._load_schedule_from_db(today_str) if schedule_data: self.today_schedule = schedule_data - schedule_sleep_bridge.update_today_schedule(self.today_schedule) + self.sleep_manager.update_today_schedule(self.today_schedule) self._log_loaded_schedule(today_str) return @@ -99,7 +100,7 @@ class ScheduleManager: if schedule_data: self._save_schedule_to_db(today_str, schedule_data) self.today_schedule = schedule_data - schedule_sleep_bridge.update_today_schedule(self.today_schedule) + self.sleep_manager.update_today_schedule(self.today_schedule) self._log_generated_schedule(today_str, schedule_data) if sampled_plans: From 11ada53b0d92e7dbce15d315e3e51943c3474abe Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 12:25:36 +0800 Subject: [PATCH 20/69] rrrrrrrrrrrrrrrrruuuuuuuuuuuufffffffffffffffffffffffffffffffff x2 --- scripts/update_prompt_imports.py | 2 -- src/chat/chat_loop/cycle_processor.py | 2 +- src/chat/chat_loop/response_handler.py | 9 ++------- src/chat/chat_loop/sleep_manager/sleep_state.py | 1 - src/chat/chat_loop/sleep_manager/time_checker.py | 2 +- src/chat/replyer/default_generator.py | 2 +- src/plugin_system/base/base_events_handler.py | 2 +- 7 files changed, 6 insertions(+), 14 deletions(-) diff --git a/scripts/update_prompt_imports.py b/scripts/update_prompt_imports.py index 672659086..289d7f327 100644 --- a/scripts/update_prompt_imports.py +++ b/scripts/update_prompt_imports.py @@ -4,8 +4,6 @@ """ import os -import re -from pathlib import Path # 需要更新的文件列表 files_to_update = [ diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index bb1a1a5f0..179089a7b 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -3,7 +3,7 @@ import time import traceback import math import random -from typing import Optional, Dict, Any, Tuple +from typing import Dict, Any, Tuple from src.chat.utils.timer_calculator import Timer from src.common.logger import get_logger diff --git a/src/chat/chat_loop/response_handler.py b/src/chat/chat_loop/response_handler.py index b88f39fe0..123032a78 100644 --- a/src/chat/chat_loop/response_handler.py +++ b/src/chat/chat_loop/response_handler.py @@ -1,18 +1,13 @@ import time import random -import traceback -from typing import Optional, Dict, Any, Tuple +from typing import Dict, Any, Tuple from src.common.logger import get_logger -from src.config.config import global_config -from src.plugin_system.apis import generator_api, send_api, message_api, database_api +from src.plugin_system.apis import send_api, message_api, database_api from src.person_info.person_info import get_person_info_manager from .hfc_context import HfcContext # 导入反注入系统 -from src.chat.antipromptinjector import get_anti_injector -from src.chat.antipromptinjector.types import ProcessResult -from src.chat.utils.prompt import Prompt logger = get_logger("hfc") anti_injector_logger = get_logger("anti_injector") diff --git a/src/chat/chat_loop/sleep_manager/sleep_state.py b/src/chat/chat_loop/sleep_manager/sleep_state.py index b8c994589..8aeb1d4d0 100644 --- a/src/chat/chat_loop/sleep_manager/sleep_state.py +++ b/src/chat/chat_loop/sleep_manager/sleep_state.py @@ -1,6 +1,5 @@ from enum import Enum, auto from datetime import datetime -from typing import Optional from src.common.logger import get_logger from src.manager.local_store_manager import local_storage diff --git a/src/chat/chat_loop/sleep_manager/time_checker.py b/src/chat/chat_loop/sleep_manager/time_checker.py index 7e1b80b49..342d0d62e 100644 --- a/src/chat/chat_loop/sleep_manager/time_checker.py +++ b/src/chat/chat_loop/sleep_manager/time_checker.py @@ -1,5 +1,5 @@ from datetime import datetime, time -from typing import Optional, List, Dict, Any +from typing import Optional from src.common.logger import get_logger from src.config.config import global_config diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index a1312dc06..f53b8ad99 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -37,7 +37,7 @@ from src.plugin_system.base.component_types import ActionInfo, EventType from src.plugin_system.apis import llm_api # 导入新的统一Prompt系统 -from src.chat.utils.prompt import Prompt, PromptParameters +from src.chat.utils.prompt import PromptParameters logger = get_logger("replyer") diff --git a/src/plugin_system/base/base_events_handler.py b/src/plugin_system/base/base_events_handler.py index 07dd9a7af..c56a84864 100644 --- a/src/plugin_system/base/base_events_handler.py +++ b/src/plugin_system/base/base_events_handler.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Tuple, Optional, Dict, List, Union +from typing import Tuple, Optional, List, Union from src.common.logger import get_logger from .component_types import EventType, EventHandlerInfo, ComponentType From 36b9eae6c8b25c78d7fe8d37d924573abdf36cb0 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 13:34:53 +0800 Subject: [PATCH 21/69] =?UTF-8?q?refactor(sleep):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=9D=A1=E7=9C=A0=E7=B3=BB=E7=BB=9F=E4=BB=A5=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E7=9D=A1=E5=90=8E=E5=A4=B1=E7=9C=A0=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构了原有的睡眠管理状态机,将睡前失眠逻辑调整为更真实的“睡后失眠”模式。现在系统会在角色入睡一段时间后,根据当前的睡眠压力判断是否触发失眠状态。 主要变更: - **状态机调整**: 移除了入睡前的失眠检查,改为在进入`SLEEPING`状态后,延迟一段时间再根据睡眠压力触发`INSOMNIA`状态。 - **通知系统重构**: `NotificationSender`被简化,现在通过触发主动思考事件 (`goodnight`, `post_sleep_insomnia`) 来发送通知,而不是直接调用生成器API。 - **配置更新**: 将固定的失眠持续时间改为一个随机范围,并增加了触发失眠判定的延迟时间配置。 - **代码解耦**: `EnergyManager`现在直接依赖新的`SleepManager`,不再通过旧的`schedule_manager`。 --- src/chat/chat_loop/energy_manager.py | 5 +- src/chat/chat_loop/hfc_context.py | 2 +- .../chat_loop/proactive/proactive_thinker.py | 2 + .../sleep_manager/notification_sender.py | 81 +++------ .../chat_loop/sleep_manager/sleep_manager.py | 172 ++++++++++-------- src/config/official_configs.py | 7 +- 6 files changed, 127 insertions(+), 142 deletions(-) diff --git a/src/chat/chat_loop/energy_manager.py b/src/chat/chat_loop/energy_manager.py index cc3cf8d0e..2eb7e7265 100644 --- a/src/chat/chat_loop/energy_manager.py +++ b/src/chat/chat_loop/energy_manager.py @@ -4,8 +4,7 @@ from typing import Optional from src.common.logger import get_logger from src.config.config import global_config from .hfc_context import HfcContext -from src.schedule.schedule_manager import schedule_manager - +from src.chat.chat_loop.sleep_manager import sleep_manager logger = get_logger("hfc") @@ -74,7 +73,7 @@ class EnergyManager: continue # 判断当前是否为睡眠时间 - is_sleeping = schedule_manager.is_sleeping() + is_sleeping = sleep_manager.SleepManager().is_sleeping() if is_sleeping: # 睡眠中:减少睡眠压力 diff --git a/src/chat/chat_loop/hfc_context.py b/src/chat/chat_loop/hfc_context.py index 4e7ec1e2f..e6a4b31f3 100644 --- a/src/chat/chat_loop/hfc_context.py +++ b/src/chat/chat_loop/hfc_context.py @@ -70,7 +70,7 @@ class HfcContext: # breaking形式下的累积兴趣值 self.breaking_accumulated_interest = 0.0 # 引用HeartFChatting实例,以便其他组件可以调用其方法 - self.chat_instance: Optional["HeartFChatting"] = None + self.chat_instance: "HeartFChatting" def save_context_state(self): """将当前状态保存到聊天流""" diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index e2be9fdc2..69c3fa96a 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -76,6 +76,8 @@ class ProactiveThinker: new_mood = "深夜emo,胡思乱想" elif trigger_event.reason == "goodnight": new_mood = "有点困了,准备睡觉了" + elif trigger_event.reason == "post_sleep_insomnia": + new_mood = "可恶,刚刚好像睡着了又醒了,现在睡不着了" if new_mood: mood_obj.mood_state = new_mood diff --git a/src/chat/chat_loop/sleep_manager/notification_sender.py b/src/chat/chat_loop/sleep_manager/notification_sender.py index 5230c4a11..9bf841810 100644 --- a/src/chat/chat_loop/sleep_manager/notification_sender.py +++ b/src/chat/chat_loop/sleep_manager/notification_sender.py @@ -1,68 +1,33 @@ import asyncio -import random -import hashlib from src.common.logger import get_logger -from src.config.config import global_config -from src.plugin_system.apis import send_api, generator_api +from ..hfc_context import HfcContext logger = get_logger("notification_sender") class NotificationSender: @staticmethod - async def send_pre_sleep_notification(): - """异步生成并发送睡前通知""" + async def send_goodnight_notification(context: HfcContext): + """发送晚安通知""" try: - groups = global_config.sleep_system.pre_sleep_notification_groups - prompt = global_config.sleep_system.pre_sleep_prompt - - if not groups: - logger.info("未配置睡前通知的群组,跳过发送。") - return - - if not prompt: - logger.warning("睡前通知的prompt为空,跳过发送。") - return - - # 为防止消息风暴,稍微延迟一下 - await asyncio.sleep(random.uniform(5, 15)) - - for group_id_str in groups: - try: - # 格式 "platform:group_id" - parts = group_id_str.split(":") - if len(parts) != 2: - logger.warning(f"无效的群组ID格式: {group_id_str}") - continue - - platform, group_id = parts - - # 使用与 ChatStream.get_stream_id 相同的逻辑生成 stream_id - key = "_".join([platform, group_id]) - stream_id = hashlib.md5(key.encode()).hexdigest() - - logger.info(f"正在为群组 {group_id_str} (Stream ID: {stream_id}) 生成睡前消息...") - - # 调用 generator_api 生成回复 - success, reply_set, _ = await generator_api.generate_reply( - chat_id=stream_id, extra_info=prompt, request_type="schedule.pre_sleep_notification" - ) - - if success and reply_set: - # 提取文本内容并发送 - reply_text = "".join([content for msg_type, content in reply_set if msg_type == "text"]) - if reply_text: - logger.info(f"向群组 {group_id_str} 发送睡前消息: {reply_text}") - await send_api.text_to_stream(text=reply_text, stream_id=stream_id) - else: - logger.warning(f"为群组 {group_id_str} 生成的回复内容为空。") - else: - logger.error(f"为群组 {group_id_str} 生成睡前消息失败。") - - await asyncio.sleep(random.uniform(2, 5)) # 避免发送过快 - - except Exception as e: - logger.error(f"向群组 {group_id_str} 发送睡前消息失败: {e}") - + from ..proactive.events import ProactiveTriggerEvent + from ..proactive.proactive_thinker import ProactiveThinker + + event = ProactiveTriggerEvent(source="sleep_manager", reason="goodnight") + proactive_thinker = ProactiveThinker(context, context.chat_instance.cycle_processor) + await proactive_thinker.think(event) except Exception as e: - logger.error(f"发送睡前通知任务失败: {e}") \ No newline at end of file + logger.error(f"发送晚安通知失败: {e}") + + @staticmethod + async def send_insomnia_notification(context: HfcContext): + """发送失眠通知""" + try: + from ..proactive.events import ProactiveTriggerEvent + from ..proactive.proactive_thinker import ProactiveThinker + + event = ProactiveTriggerEvent(source="sleep_manager", reason="post_sleep_insomnia") + proactive_thinker = ProactiveThinker(context, context.chat_instance.cycle_processor) + await proactive_thinker.think(event) + except Exception as e: + logger.error(f"发送失眠通知失败: {e}") \ No newline at end of file diff --git a/src/chat/chat_loop/sleep_manager/sleep_manager.py b/src/chat/chat_loop/sleep_manager/sleep_manager.py index 283466ec8..e9fbbf796 100644 --- a/src/chat/chat_loop/sleep_manager/sleep_manager.py +++ b/src/chat/chat_loop/sleep_manager/sleep_manager.py @@ -49,7 +49,7 @@ class SleepManager: today = now.date() if self._last_sleep_check_date != today: - logger.info(f"新的一天 ({today}),重置睡眠状态为 AWAKE。") + logger.info(f"新的一天 ({today}),重置睡眠状态。") self._total_delayed_minutes_today = 0 self._current_state = SleepState.AWAKE self._sleep_buffer_end_time = None @@ -58,93 +58,107 @@ class SleepManager: is_in_theoretical_sleep, activity = self.time_checker.is_in_theoretical_sleep_time(now.time()) + # 状态机处理 if self._current_state == SleepState.AWAKE: if is_in_theoretical_sleep: - logger.info(f"进入理论休眠时间 '{activity}',开始进行睡眠决策...") - sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999 - pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold - - if ( - sleep_pressure < pressure_threshold - and self._total_delayed_minutes_today < global_config.sleep_system.max_sleep_delay_minutes - ): - delay_minutes = 15 - self._total_delayed_minutes_today += delay_minutes - self._sleep_buffer_end_time = now + timedelta(minutes=delay_minutes) - self._current_state = SleepState.INSOMNIA - logger.info( - f"睡眠压力 ({sleep_pressure:.1f}) 低于阈值 ({pressure_threshold}),进入失眠状态,延迟入睡 {delay_minutes} 分钟。" - ) - if global_config.sleep_system.enable_pre_sleep_notification: - asyncio.create_task(NotificationSender.send_pre_sleep_notification()) - else: - buffer_seconds = random.randint(5 * 60, 10 * 60) - self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) - self._current_state = SleepState.PREPARING_SLEEP - logger.info( - f"睡眠压力正常或已达今日最大延迟,进入准备入睡状态,将在 {buffer_seconds / 60:.1f} 分钟内入睡。" - ) - if global_config.sleep_system.enable_pre_sleep_notification: - asyncio.create_task(NotificationSender.send_pre_sleep_notification()) - self._save_sleep_state() - - elif self._current_state == SleepState.INSOMNIA: - if not is_in_theoretical_sleep: - logger.info("已离开理论休眠时间,失眠结束,恢复清醒。") - self._current_state = SleepState.AWAKE - self._save_sleep_state() - elif self._sleep_buffer_end_time and now >= self._sleep_buffer_end_time: - logger.info("失眠状态下的延迟时间已过,重新评估是否入睡...") - sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999 - pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold - - if ( - sleep_pressure >= pressure_threshold - or self._total_delayed_minutes_today >= global_config.sleep_system.max_sleep_delay_minutes - ): - logger.info("睡眠压力足够或已达最大延迟,从失眠状态转换到准备入睡。") - buffer_seconds = random.randint(5 * 60, 10 * 60) - self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) - self._current_state = SleepState.PREPARING_SLEEP - else: - logger.info(f"睡眠压力({sleep_pressure:.1f})仍然较低,再延迟15分钟。") - delay_minutes = 15 - self._total_delayed_minutes_today += delay_minutes - self._sleep_buffer_end_time = now + timedelta(minutes=delay_minutes) - self._save_sleep_state() + self._handle_awake_to_sleep(now, activity, wakeup_manager) elif self._current_state == SleepState.PREPARING_SLEEP: - if not is_in_theoretical_sleep: - logger.info("准备入睡期间离开理论休眠时间,取消入睡,恢复清醒。") - self._current_state = SleepState.AWAKE - self._sleep_buffer_end_time = None - self._save_sleep_state() - elif self._sleep_buffer_end_time and now >= self._sleep_buffer_end_time: - logger.info("睡眠缓冲期结束,正式进入休眠状态。") - self._current_state = SleepState.SLEEPING - self._last_fully_slept_log_time = now.timestamp() - self._save_sleep_state() + self._handle_preparing_sleep(now, is_in_theoretical_sleep, wakeup_manager) elif self._current_state == SleepState.SLEEPING: - if not is_in_theoretical_sleep: - logger.info("理论休眠时间结束,自然醒来。") - self._current_state = SleepState.AWAKE - self._save_sleep_state() - else: - current_timestamp = now.timestamp() - if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval: - logger.info(f"当前处于休眠活动 '{activity}' 中。") - self.last_sleep_log_time = current_timestamp + self._handle_sleeping(now, is_in_theoretical_sleep, activity, wakeup_manager) + + elif self._current_state == SleepState.INSOMNIA: + self._handle_insomnia(now, is_in_theoretical_sleep) elif self._current_state == SleepState.WOKEN_UP: - if not is_in_theoretical_sleep: - logger.info("理论休眠时间结束,被吵醒的状态自动结束。") - self._current_state = SleepState.AWAKE - self._re_sleep_attempt_time = None + self._handle_woken_up(now, is_in_theoretical_sleep, wakeup_manager) + + def _handle_awake_to_sleep(self, now: datetime, activity: Optional[str], wakeup_manager: Optional["WakeUpManager"]): + if activity: + logger.info(f"进入理论休眠时间 '{activity}',开始进行睡眠决策...") + else: + logger.info("进入理论休眠时间,开始进行睡眠决策...") + + if wakeup_manager and global_config.sleep_system.enable_pre_sleep_notification: + asyncio.create_task(NotificationSender.send_goodnight_notification(wakeup_manager.context)) + + buffer_seconds = random.randint(1 * 60, 3 * 60) + self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) + self._current_state = SleepState.PREPARING_SLEEP + logger.info(f"进入准备入睡状态,将在 {buffer_seconds / 60:.1f} 分钟内入睡。") + self._save_sleep_state() + + def _handle_preparing_sleep(self, now: datetime, is_in_theoretical_sleep: bool, wakeup_manager: Optional["WakeUpManager"]): + if not is_in_theoretical_sleep: + logger.info("准备入睡期间离开理论休眠时间,取消入睡,恢复清醒。") + self._current_state = SleepState.AWAKE + self._sleep_buffer_end_time = None + self._save_sleep_state() + elif self._sleep_buffer_end_time and now >= self._sleep_buffer_end_time: + logger.info("睡眠缓冲期结束,正式进入休眠状态。") + self._current_state = SleepState.SLEEPING + self._last_fully_slept_log_time = now.timestamp() + + delay_minutes_range = global_config.sleep_system.insomnia_trigger_delay_minutes + delay_minutes = random.randint(delay_minutes_range[0], delay_minutes_range[1]) + self._sleep_buffer_end_time = now + timedelta(minutes=delay_minutes) + logger.info(f"已设置睡后失眠检查,将在 {delay_minutes} 分钟后触发。") + + self._save_sleep_state() + + def _handle_sleeping(self, now: datetime, is_in_theoretical_sleep: bool, activity: Optional[str], wakeup_manager: Optional["WakeUpManager"]): + if not is_in_theoretical_sleep: + logger.info("理论休眠时间结束,自然醒来。") + self._current_state = SleepState.AWAKE + self._save_sleep_state() + elif self._sleep_buffer_end_time and now >= self._sleep_buffer_end_time: + if wakeup_manager: + sleep_pressure = wakeup_manager.context.sleep_pressure + pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold + if sleep_pressure < pressure_threshold: + logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 低于阈值 ({pressure_threshold}),触发睡后失眠。") + self._current_state = SleepState.INSOMNIA + + duration_minutes_range = global_config.sleep_system.insomnia_duration_minutes + duration_minutes = random.randint(duration_minutes_range[0], duration_minutes_range[1]) + self._sleep_buffer_end_time = now + timedelta(minutes=duration_minutes) + + asyncio.create_task(NotificationSender.send_insomnia_notification(wakeup_manager.context)) + logger.info(f"进入失眠状态,将持续 {duration_minutes} 分钟。") + else: + logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 正常,未触发睡后失眠。") + self._sleep_buffer_end_time = None self._save_sleep_state() - elif self._re_sleep_attempt_time and now >= self._re_sleep_attempt_time: - logger.info("被吵醒后经过一段时间,尝试重新入睡...") - sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999 + else: + current_timestamp = now.timestamp() + if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval and activity: + logger.info(f"当前处于休眠活动 '{activity}' 中。") + self.last_sleep_log_time = current_timestamp + + def _handle_insomnia(self, now: datetime, is_in_theoretical_sleep: bool): + if not is_in_theoretical_sleep: + logger.info("已离开理论休眠时间,失眠结束,恢复清醒。") + self._current_state = SleepState.AWAKE + self._sleep_buffer_end_time = None + self._save_sleep_state() + elif self._sleep_buffer_end_time and now >= self._sleep_buffer_end_time: + logger.info("失眠状态持续时间已过,恢复睡眠。") + self._current_state = SleepState.SLEEPING + self._sleep_buffer_end_time = None + self._save_sleep_state() + + def _handle_woken_up(self, now: datetime, is_in_theoretical_sleep: bool, wakeup_manager: Optional["WakeUpManager"]): + if not is_in_theoretical_sleep: + logger.info("理论休眠时间结束,被吵醒的状态自动结束。") + self._current_state = SleepState.AWAKE + self._re_sleep_attempt_time = None + self._save_sleep_state() + elif self._re_sleep_attempt_time and now >= self._re_sleep_attempt_time: + logger.info("被吵醒后经过一段时间,尝试重新入睡...") + if wakeup_manager: + sleep_pressure = wakeup_manager.context.sleep_pressure pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold if sleep_pressure >= pressure_threshold: diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 3a7ab3fb7..3989e246c 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -625,7 +625,12 @@ class SleepSystemConfig(ValidatedConfigBase): # --- 失眠机制相关参数 --- enable_insomnia_system: bool = Field(default=True, description="是否启用失眠系统") - insomnia_duration_minutes: int = Field(default=30, ge=1, description="单次失眠状态的持续时间(分钟)") + insomnia_trigger_delay_minutes: List[int] = Field( + default_factory=lambda:[30, 60], description="入睡后触发失眠判定的延迟时间范围(分钟)" + ) + insomnia_duration_minutes: List[int] = Field( + default_factory=lambda:[15, 45], description="单次失眠状态的持续时间范围(分钟)" + ) sleep_pressure_threshold: float = Field(default=30.0, description="触发“压力不足型失眠”的睡眠压力阈值") deep_sleep_threshold: float = Field(default=80.0, description="进入“深度睡眠”的睡眠压力阈值") insomnia_chance_low_pressure: float = Field(default=0.6, ge=0.0, le=1.0, description="压力不足时的失眠基础概率") From 0b1d365fce97fc867b25963a6a8165b366fbfe76 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 13:44:38 +0800 Subject: [PATCH 22/69] =?UTF-8?q?docs(core):=20=E5=AE=8C=E5=96=84=E7=9D=A1?= =?UTF-8?q?=E7=9C=A0=E4=B8=8E=E4=B8=BB=E5=8A=A8=E6=80=9D=E8=80=83=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E7=9A=84=E6=96=87=E6=A1=A3=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 `ProactiveThinker`, `SleepManager`, 和 `SleepState` 核心类及其方法添加了详尽的中文文档字符串和内联注释。 本次更新旨在提高代码的可读性和可维护性,详细解释了以下关键组件的职责和工作流程: - **ProactiveThinker**: 阐明了其作为主动事件处理中心的角色,以及如何与规划器和生成器协作。 - **SleepManager**: 详细描述了其作为睡眠状态机的核心逻辑,包括状态转换的条件和处理流程。 - **SleepState**: 解释了各个睡眠状态的含义以及序列化器的作用,确保状态持久化。 此外,对配置文件 `bot_config_template.toml` 中的相关配置项也补充了更清晰的注释。 --- .../chat_loop/proactive/proactive_thinker.py | 59 ++++++++++----- .../chat_loop/sleep_manager/sleep_manager.py | 74 ++++++++++++++++--- .../chat_loop/sleep_manager/sleep_state.py | 50 +++++++++++-- template/bot_config_template.toml | 8 +- 4 files changed, 150 insertions(+), 41 deletions(-) diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index 69c3fa96a..8803efbaf 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -15,28 +15,35 @@ logger = get_logger("hfc") class ProactiveThinker: + """ + 主动思考器,负责处理和执行主动思考事件。 + 当接收到 ProactiveTriggerEvent 时,它会根据事件内容进行一系列决策和操作, + 例如调整情绪、调用规划器生成行动,并最终可能产生一个主动的回复。 + """ def __init__(self, context: HfcContext, cycle_processor: "CycleProcessor"): """ - 初始化主动思考器 + 初始化主动思考器。 Args: - context: HFC聊天上下文对象 - cycle_processor: 循环处理器,用于执行主动思考的结果 + context (HfcContext): HFC聊天上下文对象,提供了当前聊天会话的所有背景信息。 + cycle_processor (CycleProcessor): 循环处理器,用于执行主动思考后产生的动作。 功能说明: - - 接收主动思考事件并执行思考流程 - - 根据事件类型执行不同的前置操作(如修改情绪) - - 调用planner进行决策并由cycle_processor执行 + - 接收并处理主动思考事件 (ProactiveTriggerEvent)。 + - 在思考前根据事件类型执行预处理操作,如修改当前情绪状态。 + - 调用行动规划器 (Action Planner) 来决定下一步应该做什么。 + - 如果规划结果是发送消息,则调用生成器API生成回复并发送。 """ self.context = context self.cycle_processor = cycle_processor async def think(self, trigger_event: ProactiveTriggerEvent): """ - 统一的API入口,用于触发主动思考 + 主动思考的统一入口API。 + 这是外部触发主动思考时调用的主要方法。 Args: - trigger_event: 描述触发上下文的事件对象 + trigger_event (ProactiveTriggerEvent): 描述触发上下文的事件对象,包含了思考的来源和原因。 """ logger.info( f"{self.context.log_prefix} 接收到主动思考事件: " @@ -44,32 +51,38 @@ class ProactiveThinker: ) try: - # 1. 根据事件类型执行前置操作 + # 步骤 1: 根据事件类型执行思考前的准备工作,例如调整情绪。 await self._prepare_for_thinking(trigger_event) - # 2. 执行核心思考逻辑 + # 步骤 2: 执行核心的思考和决策逻辑。 await self._execute_proactive_thinking(trigger_event) except Exception as e: + # 捕获并记录在思考过程中发生的任何异常。 logger.error(f"{self.context.log_prefix} 主动思考 think 方法执行异常: {e}") logger.error(traceback.format_exc()) async def _prepare_for_thinking(self, trigger_event: ProactiveTriggerEvent): """ - 根据事件类型,执行思考前的准备工作,例如修改情绪 + 根据事件类型,在正式思考前执行准备工作。 + 目前主要是处理来自失眠管理器的事件,并据此调整情绪。 Args: - trigger_event: 触发事件 + trigger_event (ProactiveTriggerEvent): 触发事件。 """ + # 目前只处理来自失眠管理器(insomnia_manager)的事件 if trigger_event.source != "insomnia_manager": return try: + # 动态导入情绪管理器,避免循环依赖 from src.mood.mood_manager import mood_manager + # 获取当前聊天的情绪对象 mood_obj = mood_manager.get_mood_by_chat_id(self.context.stream_id) new_mood = None + # 根据失眠的不同原因设置对应的情绪 if trigger_event.reason == "low_pressure": new_mood = "精力过剩,毫无睡意" elif trigger_event.reason == "random": @@ -79,6 +92,7 @@ class ProactiveThinker: elif trigger_event.reason == "post_sleep_insomnia": new_mood = "可恶,刚刚好像睡着了又醒了,现在睡不着了" + # 如果成功匹配到了新的情绪,则更新情绪状态 if new_mood: mood_obj.mood_state = new_mood mood_obj.last_change_time = time.time() @@ -92,34 +106,39 @@ class ProactiveThinker: async def _execute_proactive_thinking(self, trigger_event: ProactiveTriggerEvent): """ - 执行主动思考的核心逻辑 + 执行主动思考的核心逻辑。 + 它会调用规划器来决定是否要采取行动,以及采取什么行动。 Args: - trigger_event: 触发事件 + trigger_event (ProactiveTriggerEvent): 触发事件。 """ try: - # 直接调用 planner 的 PROACTIVE 模式 + # 调用规划器的 PROACTIVE 模式,让其决定下一步的行动 actions, target_message = await self.cycle_processor.action_planner.plan(mode=ChatMode.PROACTIVE) - # 获取第一个规划出的动作作为主要决策 - action_result = actions[0] if actions else {} + # 通常只关心规划出的第一个动作 + action_result = actions if actions else {} - # 如果决策不是 do_nothing,则执行 + # 检查规划出的动作是否是“什么都不做” if action_result and action_result.get("action_type") != "do_nothing": + # 如果动作是“回复” if action_result.get("action_type") == "reply": + # 调用生成器API来创建回复内容 success, response_set, _ = await generator_api.generate_reply( chat_stream=self.context.chat_stream, reply_message=action_result["action_message"], - available_actions={}, + available_actions={}, # 主动回复不考虑工具使用 enable_tool=False, - request_type="chat.replyer.proactive", + request_type="chat.replyer.proactive", # 标记请求类型 from_plugin=False, ) + # 如果成功生成回复,则发送出去 if success and response_set: await self.cycle_processor.response_handler.send_response( response_set, time.time(), action_result["action_message"] ) else: + # 如果规划结果是“什么都不做”,则记录日志 logger.info(f"{self.context.log_prefix} 主动思考决策: 保持沉默") except Exception as e: diff --git a/src/chat/chat_loop/sleep_manager/sleep_manager.py b/src/chat/chat_loop/sleep_manager/sleep_manager.py index e9fbbf796..c64ab641e 100644 --- a/src/chat/chat_loop/sleep_manager/sleep_manager.py +++ b/src/chat/chat_loop/sleep_manager/sleep_manager.py @@ -16,29 +16,48 @@ logger = get_logger("sleep_manager") class SleepManager: + """ + 睡眠管理器,核心组件之一,负责管理角色的睡眠周期和状态转换。 + 它实现了一个状态机,根据预设的时间表、睡眠压力和随机因素, + 在不同的睡眠状态(如清醒、准备入睡、睡眠、失眠)之间进行切换。 + """ def __init__(self): - self.time_checker = TimeChecker(self) - self.today_schedule: Optional[List[Dict[str, Any]]] = None - self.last_sleep_log_time = 0 - self.sleep_log_interval = 35 + """ + 初始化睡眠管理器。 + """ + self.time_checker = TimeChecker(self) # 时间检查器,用于判断当前是否处于理论睡眠时间 + self.today_schedule: Optional[List[Dict[str, Any]]] = None # 当天的日程安排 + self.last_sleep_log_time = 0 # 上次记录睡眠日志的时间戳 + self.sleep_log_interval = 35 # 睡眠日志记录间隔(秒) # --- 统一睡眠状态管理 --- - self._current_state: SleepState = SleepState.AWAKE - self._sleep_buffer_end_time: Optional[datetime] = None - self._total_delayed_minutes_today: int = 0 - self._last_sleep_check_date: Optional[date] = None - self._last_fully_slept_log_time: float = 0 - self._re_sleep_attempt_time: Optional[datetime] = None + self._current_state: SleepState = SleepState.AWAKE # 当前睡眠状态 + self._sleep_buffer_end_time: Optional[datetime] = None # 睡眠缓冲结束时间,用于状态转换 + self._total_delayed_minutes_today: int = 0 # 今天总共延迟入睡的分钟数 + self._last_sleep_check_date: Optional[date] = None # 上次检查睡眠状态的日期 + self._last_fully_slept_log_time: float = 0 # 上次完全进入睡眠状态的时间戳 + self._re_sleep_attempt_time: Optional[datetime] = None # 被吵醒后,尝试重新入睡的时间点 + # 从本地存储加载上一次的睡眠状态 self._load_sleep_state() def get_current_sleep_state(self) -> SleepState: + """获取当前的睡眠状态。""" return self._current_state def is_sleeping(self) -> bool: + """判断当前是否处于正在睡觉的状态。""" return self._current_state == SleepState.SLEEPING async def update_sleep_state(self, wakeup_manager: Optional["WakeUpManager"] = None): + """ + 更新睡眠状态的核心方法,实现状态机的主要逻辑。 + 该方法会被周期性调用,以检查并更新当前的睡眠状态。 + + Args: + wakeup_manager (Optional["WakeUpManager"]): 唤醒管理器,用于获取睡眠压力等上下文信息。 + """ + # 如果全局禁用了睡眠系统,则强制设置为清醒状态并返回 if not global_config.sleep_system.enable: if self._current_state != SleepState.AWAKE: logger.debug("睡眠系统禁用,强制设为 AWAKE") @@ -48,6 +67,7 @@ class SleepManager: now = datetime.now() today = now.date() + # 跨天处理:如果日期变化,重置每日相关的睡眠状态 if self._last_sleep_check_date != today: logger.info(f"新的一天 ({today}),重置睡眠状态。") self._total_delayed_minutes_today = 0 @@ -56,9 +76,10 @@ class SleepManager: self._last_sleep_check_date = today self._save_sleep_state() + # 检查当前是否处于理论上的睡眠时间段 is_in_theoretical_sleep, activity = self.time_checker.is_in_theoretical_sleep_time(now.time()) - # 状态机处理 + # --- 状态机核心处理逻辑 --- if self._current_state == SleepState.AWAKE: if is_in_theoretical_sleep: self._handle_awake_to_sleep(now, activity, wakeup_manager) @@ -76,14 +97,17 @@ class SleepManager: self._handle_woken_up(now, is_in_theoretical_sleep, wakeup_manager) def _handle_awake_to_sleep(self, now: datetime, activity: Optional[str], wakeup_manager: Optional["WakeUpManager"]): + """处理从“清醒”到“准备入睡”的状态转换。""" if activity: logger.info(f"进入理论休眠时间 '{activity}',开始进行睡眠决策...") else: logger.info("进入理论休眠时间,开始进行睡眠决策...") + # 如果配置了睡前通知,则发送晚安通知 if wakeup_manager and global_config.sleep_system.enable_pre_sleep_notification: asyncio.create_task(NotificationSender.send_goodnight_notification(wakeup_manager.context)) + # 设置一个随机的缓冲时间,模拟入睡前的准备过程 buffer_seconds = random.randint(1 * 60, 3 * 60) self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) self._current_state = SleepState.PREPARING_SLEEP @@ -91,16 +115,20 @@ class SleepManager: self._save_sleep_state() def _handle_preparing_sleep(self, now: datetime, is_in_theoretical_sleep: bool, wakeup_manager: Optional["WakeUpManager"]): + """处理“准备入睡”状态下的逻辑。""" + # 如果在准备期间离开了理论睡眠时间,则取消入睡 if not is_in_theoretical_sleep: logger.info("准备入睡期间离开理论休眠时间,取消入睡,恢复清醒。") self._current_state = SleepState.AWAKE self._sleep_buffer_end_time = None self._save_sleep_state() + # 如果缓冲时间结束,则正式进入睡眠状态 elif self._sleep_buffer_end_time and now >= self._sleep_buffer_end_time: logger.info("睡眠缓冲期结束,正式进入休眠状态。") self._current_state = SleepState.SLEEPING self._last_fully_slept_log_time = now.timestamp() + # 设置一个随机的延迟,用于触发“睡后失眠”检查 delay_minutes_range = global_config.sleep_system.insomnia_trigger_delay_minutes delay_minutes = random.randint(delay_minutes_range[0], delay_minutes_range[1]) self._sleep_buffer_end_time = now + timedelta(minutes=delay_minutes) @@ -109,40 +137,51 @@ class SleepManager: self._save_sleep_state() def _handle_sleeping(self, now: datetime, is_in_theoretical_sleep: bool, activity: Optional[str], wakeup_manager: Optional["WakeUpManager"]): + """处理“正在睡觉”状态下的逻辑。""" + # 如果理论睡眠时间结束,则自然醒来 if not is_in_theoretical_sleep: logger.info("理论休眠时间结束,自然醒来。") self._current_state = SleepState.AWAKE self._save_sleep_state() + # 检查是否到了触发“睡后失眠”的时间点 elif self._sleep_buffer_end_time and now >= self._sleep_buffer_end_time: if wakeup_manager: sleep_pressure = wakeup_manager.context.sleep_pressure pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold + # 如果睡眠压力低于阈值,则触发失眠 if sleep_pressure < pressure_threshold: logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 低于阈值 ({pressure_threshold}),触发睡后失眠。") self._current_state = SleepState.INSOMNIA + # 设置失眠的持续时间 duration_minutes_range = global_config.sleep_system.insomnia_duration_minutes duration_minutes = random.randint(duration_minutes_range[0], duration_minutes_range[1]) self._sleep_buffer_end_time = now + timedelta(minutes=duration_minutes) + # 发送失眠通知 asyncio.create_task(NotificationSender.send_insomnia_notification(wakeup_manager.context)) logger.info(f"进入失眠状态,将持续 {duration_minutes} 分钟。") else: + # 睡眠压力正常,不触发失眠,清除检查时间点 logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 正常,未触发睡后失眠。") self._sleep_buffer_end_time = None self._save_sleep_state() else: + # 定期记录睡眠日志 current_timestamp = now.timestamp() if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval and activity: logger.info(f"当前处于休眠活动 '{activity}' 中。") self.last_sleep_log_time = current_timestamp def _handle_insomnia(self, now: datetime, is_in_theoretical_sleep: bool): + """处理“失眠”状态下的逻辑。""" + # 如果离开理论睡眠时间,则失眠结束 if not is_in_theoretical_sleep: logger.info("已离开理论休眠时间,失眠结束,恢复清醒。") self._current_state = SleepState.AWAKE self._sleep_buffer_end_time = None self._save_sleep_state() + # 如果失眠持续时间已过,则恢复睡眠 elif self._sleep_buffer_end_time and now >= self._sleep_buffer_end_time: logger.info("失眠状态持续时间已过,恢复睡眠。") self._current_state = SleepState.SLEEPING @@ -150,17 +189,21 @@ class SleepManager: self._save_sleep_state() def _handle_woken_up(self, now: datetime, is_in_theoretical_sleep: bool, wakeup_manager: Optional["WakeUpManager"]): + """处理“被吵醒”状态下的逻辑。""" + # 如果理论睡眠时间结束,则状态自动结束 if not is_in_theoretical_sleep: logger.info("理论休眠时间结束,被吵醒的状态自动结束。") self._current_state = SleepState.AWAKE self._re_sleep_attempt_time = None self._save_sleep_state() + # 到了尝试重新入睡的时间点 elif self._re_sleep_attempt_time and now >= self._re_sleep_attempt_time: logger.info("被吵醒后经过一段时间,尝试重新入睡...") if wakeup_manager: sleep_pressure = wakeup_manager.context.sleep_pressure pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold + # 如果睡眠压力足够,则尝试重新入睡 if sleep_pressure >= pressure_threshold: logger.info("睡眠压力足够,从被吵醒状态转换到准备入睡。") buffer_seconds = random.randint(3 * 60, 8 * 60) @@ -168,6 +211,7 @@ class SleepManager: self._current_state = SleepState.PREPARING_SLEEP self._re_sleep_attempt_time = None else: + # 睡眠压力不足,延迟一段时间后再次尝试 delay_minutes = 15 self._re_sleep_attempt_time = now + timedelta(minutes=delay_minutes) logger.info( @@ -176,6 +220,10 @@ class SleepManager: self._save_sleep_state() def reset_sleep_state_after_wakeup(self): + """ + 当角色被用户消息等外部因素唤醒时调用此方法。 + 将状态强制转换为 WOKEN_UP,并设置一个延迟,之后会尝试重新入睡。 + """ if self._current_state in [SleepState.PREPARING_SLEEP, SleepState.SLEEPING, SleepState.INSOMNIA]: logger.info("被唤醒,进入 WOKEN_UP 状态!") self._current_state = SleepState.WOKEN_UP @@ -186,12 +234,15 @@ class SleepManager: self._save_sleep_state() def get_today_schedule(self) -> Optional[List[Dict[str, Any]]]: + """获取今天的日程安排。""" return self.today_schedule def update_today_schedule(self, schedule: Optional[List[Dict[str, Any]]]): + """更新今天的日程安排。""" self.today_schedule = schedule def _save_sleep_state(self): + """将当前所有睡眠相关的状态打包并保存到本地存储。""" state_data = { "_current_state": self._current_state, "_sleep_buffer_end_time": self._sleep_buffer_end_time, @@ -202,6 +253,7 @@ class SleepManager: SleepStateSerializer.save(state_data) def _load_sleep_state(self): + """从本地存储加载并恢复所有睡眠相关的状态。""" state_data = SleepStateSerializer.load() self._current_state = state_data["_current_state"] self._sleep_buffer_end_time = state_data["_sleep_buffer_end_time"] diff --git a/src/chat/chat_loop/sleep_manager/sleep_state.py b/src/chat/chat_loop/sleep_manager/sleep_state.py index 8aeb1d4d0..624521ea0 100644 --- a/src/chat/chat_loop/sleep_manager/sleep_state.py +++ b/src/chat/chat_loop/sleep_manager/sleep_state.py @@ -7,26 +7,45 @@ logger = get_logger("sleep_state") class SleepState(Enum): - """睡眠状态枚举""" + """ + 定义了角色可能处于的几种睡眠状态。 + 这是一个状态机,用于管理角色的睡眠周期。 + """ - AWAKE = auto() - INSOMNIA = auto() - PREPARING_SLEEP = auto() - SLEEPING = auto() - WOKEN_UP = auto() + AWAKE = auto() # 清醒状态 + INSOMNIA = auto() # 失眠状态 + PREPARING_SLEEP = auto() # 准备入睡状态,一个短暂的过渡期 + SLEEPING = auto() # 正在睡觉状态 + WOKEN_UP = auto() # 被吵醒状态 class SleepStateSerializer: + """ + 睡眠状态序列化器。 + 负责将内存中的睡眠状态对象持久化到本地存储(如JSON文件), + 以及在程序启动时从本地存储中恢复状态。 + 这样可以确保即使程序重启,角色的睡眠状态也能得以保留。 + """ @staticmethod def save(state_data: dict): - """将当前睡眠状态保存到本地存储""" + """ + 将当前的睡眠状态数据保存到本地存储。 + + Args: + state_data (dict): 包含睡眠状态信息的字典。 + datetime对象会被转换为时间戳,Enum成员会被转换为其名称字符串。 + """ try: + # 准备要序列化的数据字典 state = { + # 保存当前状态的枚举名称 "current_state": state_data["_current_state"].name, + # 将datetime对象转换为Unix时间戳以便序列化 "sleep_buffer_end_time_ts": state_data["_sleep_buffer_end_time"].timestamp() if state_data["_sleep_buffer_end_time"] else None, "total_delayed_minutes_today": state_data["_total_delayed_minutes_today"], + # 将date对象转换为ISO格式的字符串 "last_sleep_check_date_str": state_data["_last_sleep_check_date"].isoformat() if state_data["_last_sleep_check_date"] else None, @@ -34,6 +53,7 @@ class SleepStateSerializer: if state_data["_re_sleep_attempt_time"] else None, } + # 写入本地存储 local_storage["schedule_sleep_state"] = state logger.debug(f"已保存睡眠状态: {state}") except Exception as e: @@ -41,7 +61,14 @@ class SleepStateSerializer: @staticmethod def load() -> dict: - """从本地存储加载睡眠状态""" + """ + 从本地存储加载并解析睡眠状态。 + + Returns: + dict: 包含恢复后睡眠状态信息的字典。 + 如果加载失败或没有找到数据,则返回一个默认的清醒状态。 + """ + # 定义一个默认的状态,以防加载失败 state_data = { "_current_state": SleepState.AWAKE, "_sleep_buffer_end_time": None, @@ -50,27 +77,34 @@ class SleepStateSerializer: "_re_sleep_attempt_time": None, } try: + # 从本地存储读取数据 state = local_storage["schedule_sleep_state"] if state and isinstance(state, dict): + # 恢复当前状态枚举 state_name = state.get("current_state") if state_name and hasattr(SleepState, state_name): state_data["_current_state"] = SleepState[state_name] + # 从时间戳恢复datetime对象 end_time_ts = state.get("sleep_buffer_end_time_ts") if end_time_ts: state_data["_sleep_buffer_end_time"] = datetime.fromtimestamp(end_time_ts) + # 恢复重新入睡尝试时间 re_sleep_ts = state.get("re_sleep_attempt_time_ts") if re_sleep_ts: state_data["_re_sleep_attempt_time"] = datetime.fromtimestamp(re_sleep_ts) + # 恢复今日延迟睡眠总分钟数 state_data["_total_delayed_minutes_today"] = state.get("total_delayed_minutes_today", 0) + # 从ISO格式字符串恢复date对象 date_str = state.get("last_sleep_check_date_str") if date_str: state_data["_last_sleep_check_date"] = datetime.fromisoformat(date_str).date() logger.info(f"成功从本地存储加载睡眠状态: {state}") except Exception as e: + # 如果加载过程中出现任何问题,记录警告并返回默认状态 logger.warning(f"加载睡眠状态失败,将使用默认值: {e}") return state_data \ No newline at end of file diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index f986a2cba..8b060c432 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.7.2" +version = "6.7.3" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -451,7 +451,6 @@ insomnia_chance_normal_pressure = 0.1 sleep_pressure_increment = 1.5 # 睡眠时,每分钟衰减的睡眠压力值 sleep_pressure_decay_rate = 1.5 -insomnia_duration_minutes = 30 # 单次失眠状态的持续时间(分钟) # --- 弹性睡眠与睡前消息 --- # 是否启用弹性睡眠。启用后,AI不会到点立刻入睡,而是会根据睡眠压力增加5-10分钟的缓冲,并可能因为压力不足而推迟睡眠。 @@ -467,6 +466,11 @@ enable_pre_sleep_notification = false pre_sleep_notification_groups = [] # 用于生成睡前消息的提示。AI会根据这个提示生成一句晚安问候。 pre_sleep_prompt = "我准备睡觉了,请生成一句简短自然的晚安问候。" +insomnia_duration_minutes = [30, 60] # 单次失眠状态的持续时间范围(分钟) +# --- 睡后失眠 --- +# 入睡后,经过一段延迟后触发失眠判定的延迟时间(分钟),设置为范围以增加随机性 +insomnia_trigger_delay_minutes = [15, 45] + [cross_context] # 跨群聊/私聊上下文共享配置 # 这是总开关,用于一键启用或禁用此功能 From d3f0b9d6007982bb19071e9789a3c379adc98e71 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sat, 6 Sep 2025 13:54:38 +0800 Subject: [PATCH 23/69] template --- template/bot_config_template.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index f986a2cba..4cf2b3f44 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.7.2" +version = "6.7.3" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -451,7 +451,8 @@ insomnia_chance_normal_pressure = 0.1 sleep_pressure_increment = 1.5 # 睡眠时,每分钟衰减的睡眠压力值 sleep_pressure_decay_rate = 1.5 -insomnia_duration_minutes = 30 # 单次失眠状态的持续时间(分钟) +insomnia_trigger_delay_minutes = [30, 60] # 入睡后触发失眠判定的延迟时间范围(分钟) +insomnia_duration_minutes = [15, 45] # 单次失眠状态的持续时间范围(分钟) # --- 弹性睡眠与睡前消息 --- # 是否启用弹性睡眠。启用后,AI不会到点立刻入睡,而是会根据睡眠压力增加5-10分钟的缓冲,并可能因为压力不足而推迟睡眠。 From 614f8064ba0ecb3c106bd2eb5cc5a26a20d1404e Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 13:55:57 +0800 Subject: [PATCH 24/69] =?UTF-8?q?feat(sleep):=20=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E5=A4=9A=E6=A0=B7=E5=8C=96=E7=9A=84=E5=A4=B1=E7=9C=A0=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构了失眠系统,引入了两种新的失眠触发机制: 1. **低睡眠压力失眠**:当睡后检查时睡眠压力低于设定阈值,将触发失眠。 2. **随机失眠**:在睡眠压力正常的情况下,仍有一定概率随机触发失眠。 同时,移除了原有的 `post_sleep_insomnia` 特定情绪,并调整了相关配置项,使失眠原因的传递和处理更加灵活。 --- src/chat/chat_loop/proactive/proactive_thinker.py | 4 +--- .../sleep_manager/notification_sender.py | 4 ++-- src/chat/chat_loop/sleep_manager/sleep_manager.py | 15 +++++++++++---- template/bot_config_template.toml | 7 ------- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index 8803efbaf..081941b18 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -89,8 +89,6 @@ class ProactiveThinker: new_mood = "深夜emo,胡思乱想" elif trigger_event.reason == "goodnight": new_mood = "有点困了,准备睡觉了" - elif trigger_event.reason == "post_sleep_insomnia": - new_mood = "可恶,刚刚好像睡着了又醒了,现在睡不着了" # 如果成功匹配到了新的情绪,则更新情绪状态 if new_mood: @@ -117,7 +115,7 @@ class ProactiveThinker: actions, target_message = await self.cycle_processor.action_planner.plan(mode=ChatMode.PROACTIVE) # 通常只关心规划出的第一个动作 - action_result = actions if actions else {} + action_result = actions[0] if actions else {} # 检查规划出的动作是否是“什么都不做” if action_result and action_result.get("action_type") != "do_nothing": diff --git a/src/chat/chat_loop/sleep_manager/notification_sender.py b/src/chat/chat_loop/sleep_manager/notification_sender.py index 9bf841810..55b32ec85 100644 --- a/src/chat/chat_loop/sleep_manager/notification_sender.py +++ b/src/chat/chat_loop/sleep_manager/notification_sender.py @@ -20,13 +20,13 @@ class NotificationSender: logger.error(f"发送晚安通知失败: {e}") @staticmethod - async def send_insomnia_notification(context: HfcContext): + async def send_insomnia_notification(context: HfcContext, reason: str): """发送失眠通知""" try: from ..proactive.events import ProactiveTriggerEvent from ..proactive.proactive_thinker import ProactiveThinker - event = ProactiveTriggerEvent(source="sleep_manager", reason="post_sleep_insomnia") + event = ProactiveTriggerEvent(source="sleep_manager", reason=reason) proactive_thinker = ProactiveThinker(context, context.chat_instance.cycle_processor) await proactive_thinker.think(event) except Exception as e: diff --git a/src/chat/chat_loop/sleep_manager/sleep_manager.py b/src/chat/chat_loop/sleep_manager/sleep_manager.py index c64ab641e..83ab4eee5 100644 --- a/src/chat/chat_loop/sleep_manager/sleep_manager.py +++ b/src/chat/chat_loop/sleep_manager/sleep_manager.py @@ -148,19 +148,26 @@ class SleepManager: if wakeup_manager: sleep_pressure = wakeup_manager.context.sleep_pressure pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold - # 如果睡眠压力低于阈值,则触发失眠 + # 检查是否触发失眠 + insomnia_reason = None if sleep_pressure < pressure_threshold: + insomnia_reason = "low_pressure" logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 低于阈值 ({pressure_threshold}),触发睡后失眠。") + elif random.random() < getattr(global_config.sleep_system, "random_insomnia_chance", 0.1): + insomnia_reason = "random" + logger.info("随机触发失眠。") + + if insomnia_reason: self._current_state = SleepState.INSOMNIA # 设置失眠的持续时间 duration_minutes_range = global_config.sleep_system.insomnia_duration_minutes - duration_minutes = random.randint(duration_minutes_range[0], duration_minutes_range[1]) + duration_minutes = random.randint(*duration_minutes_range) self._sleep_buffer_end_time = now + timedelta(minutes=duration_minutes) # 发送失眠通知 - asyncio.create_task(NotificationSender.send_insomnia_notification(wakeup_manager.context)) - logger.info(f"进入失眠状态,将持续 {duration_minutes} 分钟。") + asyncio.create_task(NotificationSender.send_insomnia_notification(wakeup_manager.context, insomnia_reason)) + logger.info(f"进入失眠状态 (原因: {insomnia_reason}),将持续 {duration_minutes} 分钟。") else: # 睡眠压力正常,不触发失眠,清除检查时间点 logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 正常,未触发睡后失眠。") diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 8b060c432..82e1b107a 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -222,11 +222,6 @@ willing_mode = "classical" # 回复意愿模式 —— 经典模式:classical [tool] enable_tool = true # 是否在普通聊天中启用工具 -[tool.history] -enable_history = true # 是否启用工具调用历史记录 -enable_prompt_history = true # 是否在提示词中加入工具历史记录 -max_history = 5 # 每个会话最多保留的历史记录数 - [mood] enable_mood = true # 是否启用情绪系统 mood_update_threshold = 1 # 情绪更新阈值,越高,更新越慢 @@ -443,8 +438,6 @@ enable_insomnia_system = false # 是否启用失眠系统 sleep_pressure_threshold = 30.0 # 进入“深度睡眠”的睡眠压力阈值 deep_sleep_threshold = 80.0 -# 压力不足时的失眠基础概率 (0.0 to 1.0) -insomnia_chance_low_pressure = 0.6 # 压力正常时的失眠基础概率 (0.0 to 1.0) insomnia_chance_normal_pressure = 0.1 # 每次AI执行动作后,增加的睡眠压力值 From 8f12dfd93c7de8ca665b7a60452670c594b43d5a Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 14:48:12 +0800 Subject: [PATCH 25/69] =?UTF-8?q?feat(sleep):=20=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E5=BC=B9=E6=80=A7=E7=9D=A1=E7=9C=A0=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根据睡眠压力动态调整入睡延迟时间。 - 新增`enable_flexible_sleep`配置项,用于启用或禁用此功能。 - 当启用时,系统会检查当前的睡眠压力值。 - 如果睡眠压力低于预设阈值,将根据压力差计算出一个延迟入睡的时间,压力越低,延迟越长。 - 增加了当日总延迟时间的上限,避免无限期推迟睡眠。 - 如果睡眠压力高于阈值,或当日延迟额度已用完,则会进入一个较短的准备阶段后入睡。 - 如果无法获取睡眠压力,系统将回退到旧的随机延迟逻辑。 - 将`_total_delayed_minutes_today`的数据类型从`int`修改为`float`以支持更精确的计算。 --- .../chat_loop/sleep_manager/sleep_manager.py | 64 ++++++++++++++++--- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/src/chat/chat_loop/sleep_manager/sleep_manager.py b/src/chat/chat_loop/sleep_manager/sleep_manager.py index 83ab4eee5..0ae9e39e0 100644 --- a/src/chat/chat_loop/sleep_manager/sleep_manager.py +++ b/src/chat/chat_loop/sleep_manager/sleep_manager.py @@ -33,7 +33,7 @@ class SleepManager: # --- 统一睡眠状态管理 --- self._current_state: SleepState = SleepState.AWAKE # 当前睡眠状态 self._sleep_buffer_end_time: Optional[datetime] = None # 睡眠缓冲结束时间,用于状态转换 - self._total_delayed_minutes_today: int = 0 # 今天总共延迟入睡的分钟数 + self._total_delayed_minutes_today: float = 0.0 # 今天总共延迟入睡的分钟数 self._last_sleep_check_date: Optional[date] = None # 上次检查睡眠状态的日期 self._last_fully_slept_log_time: float = 0 # 上次完全进入睡眠状态的时间戳 self._re_sleep_attempt_time: Optional[datetime] = None # 被吵醒后,尝试重新入睡的时间点 @@ -103,16 +103,60 @@ class SleepManager: else: logger.info("进入理论休眠时间,开始进行睡眠决策...") - # 如果配置了睡前通知,则发送晚安通知 - if wakeup_manager and global_config.sleep_system.enable_pre_sleep_notification: - asyncio.create_task(NotificationSender.send_goodnight_notification(wakeup_manager.context)) + if global_config.sleep_system.enable_flexible_sleep: + # --- 新的弹性睡眠逻辑 --- + if wakeup_manager: + sleep_pressure = wakeup_manager.context.sleep_pressure + pressure_threshold = global_config.sleep_system.flexible_sleep_pressure_threshold + max_delay_minutes = global_config.sleep_system.max_sleep_delay_minutes - # 设置一个随机的缓冲时间,模拟入睡前的准备过程 - buffer_seconds = random.randint(1 * 60, 3 * 60) - self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) - self._current_state = SleepState.PREPARING_SLEEP - logger.info(f"进入准备入睡状态,将在 {buffer_seconds / 60:.1f} 分钟内入睡。") - self._save_sleep_state() + buffer_seconds = 0 + # 如果睡眠压力低于阈值,则计算延迟时间 + if sleep_pressure <= pressure_threshold: + # 压力差,归一化到 (0, 1] + pressure_diff = (pressure_threshold - sleep_pressure) / pressure_threshold + # 延迟分钟数,压力越低,延迟越长 + delay_minutes = int(pressure_diff * max_delay_minutes) + + # 确保总延迟不超过当日最大值 + remaining_delay = max_delay_minutes - self._total_delayed_minutes_today + delay_minutes = min(delay_minutes, remaining_delay) + + if delay_minutes > 0: + # 增加一些随机性 + buffer_seconds = random.randint(int(delay_minutes * 0.8 * 60), int(delay_minutes * 1.2 * 60)) + self._total_delayed_minutes_today += buffer_seconds / 60.0 + logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 较低,延迟 {buffer_seconds / 60:.1f} 分钟入睡。") + else: + # 延迟额度已用完,设置一个较短的准备时间 + buffer_seconds = random.randint(1 * 60, 2 * 60) + logger.info("今日延迟入睡额度已用完,进入短暂准备后入睡。") + else: + # 睡眠压力较高,设置一个较短的准备时间 + buffer_seconds = random.randint(1 * 60, 2 * 60) + logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 较高,将在短暂准备后入睡。") + + # 发送睡前通知 + if global_config.sleep_system.enable_pre_sleep_notification: + asyncio.create_task(NotificationSender.send_goodnight_notification(wakeup_manager.context)) + + self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) + self._current_state = SleepState.PREPARING_SLEEP + logger.info(f"进入准备入睡状态,将在 {buffer_seconds / 60:.1f} 分钟内入睡。") + self._save_sleep_state() + else: + # 无法获取 wakeup_manager,退回旧逻辑 + buffer_seconds = random.randint(1 * 60, 3 * 60) + self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds) + self._current_state = SleepState.PREPARING_SLEEP + logger.warning("无法获取 WakeUpManager,弹性睡眠采用默认1-3分钟延迟。") + self._save_sleep_state() + else: + # 非弹性睡眠模式 + if wakeup_manager and global_config.sleep_system.enable_pre_sleep_notification: + asyncio.create_task(NotificationSender.send_goodnight_notification(wakeup_manager.context)) + self._current_state = SleepState.SLEEPING + def _handle_preparing_sleep(self, now: datetime, is_in_theoretical_sleep: bool, wakeup_manager: Optional["WakeUpManager"]): """处理“准备入睡”状态下的逻辑。""" From fd5d951501fff4e807465a42c7dfa65bc6e8fd7b Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 15:44:52 +0800 Subject: [PATCH 26/69] =?UTF-8?q?feat(chat):=20=E5=AE=9E=E7=8E=B0=E5=8F=91?= =?UTF-8?q?=E9=80=81=E9=94=99=E5=88=AB=E5=AD=97=E5=90=8E=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=92=A4=E5=9B=9E=E4=BF=AE=E6=AD=A3=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引入了一个新的聊天交互机制:当机器人发送了包含“错别字”的消息后,会在短暂延迟后自动撤回该消息,并发送正确的版本。此功能旨在模拟更真实的人类打字行为,增加交互的趣味性和拟人化程度。 主要变更: - **错别字处理流程**: - `ResponseHandler`现在会识别出带有错别字的消息,并在发送后创建一个异步任务来处理后续的修正。 - 新增`handle_typo_correction`方法,该方法会随机延迟2-4秒,然后调用新的`recall_message` API撤回原消息,并重新发送修正后的内容。 - **API扩展**: - `send_api`中增加了`recall_message`函数,用于调用适配器执行消息撤回操作。 - `send_response`的返回值从单个字符串`reply_text`变更为元组`(reply_text, sent_messages)`,以便将已发送的消息信息(包括ID和类型)传递给上层调用者。 - **数据结构调整**: - `process_llm_response`的返回类型从`list[str]`调整为`list[dict[str, str]]`,以支持更复杂的响应类型,如包含原文、错别字和修正建议的`typo`类型。 - **代码优化与重构**: - 对`ChineseTypoGenerator`进行了大量的代码清理、注释补充和逻辑优化,使其代码更清晰、更易于维护。 - 修复了多处代码中的类型注解和潜在的空指针问题,提高了代码的健壮性。 --- src/chat/chat_loop/cycle_processor.py | 12 +- src/chat/chat_loop/response_handler.py | 46 ++- src/chat/utils/typo_generator.py | 360 +++++++++++++----------- src/chat/utils/utils.py | 43 ++- src/plugin_system/apis/generator_api.py | 16 +- src/plugin_system/apis/send_api.py | 37 ++- 6 files changed, 313 insertions(+), 201 deletions(-) diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index 179089a7b..4a39ddd0e 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -52,7 +52,11 @@ class CycleProcessor: actions, ) -> Tuple[Dict[str, Any], str, Dict[str, float]]: with Timer("回复发送", cycle_timers): - reply_text = await self.response_handler.send_response(response_set, loop_start_time, action_message) + reply_text, sent_messages = await self.response_handler.send_response( + response_set, loop_start_time, action_message + ) + if sent_messages: + asyncio.create_task(self.response_handler.handle_typo_correction(sent_messages)) # 存储reply action信息 person_info_manager = get_person_info_manager() @@ -148,7 +152,7 @@ class CycleProcessor: cycle_timers, thinking_id = self.cycle_tracker.start_cycle() logger.info(f"{self.log_prefix} 开始第{self.context.cycle_counter}次思考") - if ENABLE_S4U: + if ENABLE_S4U and self.context.chat_stream and self.context.chat_stream.user_info: await send_typing(self.context.chat_stream.user_info.user_id) loop_start_time = time.time() @@ -175,7 +179,7 @@ class CycleProcessor: result = await event_manager.trigger_event( EventType.ON_PLAN, plugin_name="SYSTEM", stream_id=self.context.chat_stream ) - if not result.all_continue_process(): + if result and not result.all_continue_process(): raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于规划前中断了内容生成") with Timer("规划器", cycle_timers): @@ -380,7 +384,7 @@ class CycleProcessor: if fallback_action and fallback_action != action: logger.info(f"{self.context.log_prefix} 使用回退动作: {fallback_action}") action_handler = self.context.action_manager.create_action( - action_name=fallback_action if isinstance(fallback_action, list) else fallback_action, + action_name=str(fallback_action), action_data=action_data, reasoning=f"原动作'{action}'不可用,自动回退。{reasoning}", cycle_timers=cycle_timers, diff --git a/src/chat/chat_loop/response_handler.py b/src/chat/chat_loop/response_handler.py index 123032a78..982b7d54d 100644 --- a/src/chat/chat_loop/response_handler.py +++ b/src/chat/chat_loop/response_handler.py @@ -1,5 +1,6 @@ import time import random +import asyncio from typing import Dict, Any, Tuple from src.common.logger import get_logger @@ -59,7 +60,9 @@ class ResponseHandler: - 构建并返回完整的循环信息 - 用于上级方法的状态跟踪 """ - reply_text = await self.send_response(response_set, loop_start_time, action_message) + reply_text, sent_messages = await self.send_response(response_set, loop_start_time, action_message) + if sent_messages: + asyncio.create_task(self.handle_typo_correction(sent_messages)) person_info_manager = get_person_info_manager() @@ -100,18 +103,17 @@ class ResponseHandler: return loop_info, reply_text, cycle_timers - async def send_response(self, reply_set, thinking_start_time, message_data) -> str: + async def send_response(self, reply_set, thinking_start_time, message_data) -> tuple[str, list[dict[str, str]]]: """ 发送回复内容的具体实现 Args: reply_set: 回复内容集合,包含多个回复段 - reply_to: 回复目标 thinking_start_time: 思考开始时间 message_data: 消息数据 Returns: - str: 完整的回复文本 + tuple[str, list[dict[str, str]]]: (完整的回复文本, 已发送消息列表) 功能说明: - 检查是否有新消息需要回复 @@ -128,19 +130,17 @@ class ResponseHandler: need_reply = new_message_count >= random.randint(2, 4) reply_text = "" + sent_messages = [] is_proactive_thinking = message_data.get("message_type") == "proactive_thinking" first_replied = False for reply_seg in reply_set: - # 调试日志:验证reply_seg的格式 logger.debug(f"Processing reply_seg type: {type(reply_seg)}, content: {reply_seg}") - # 修正:正确处理元组格式 (格式为: (type, content)) - if isinstance(reply_seg, tuple) and len(reply_seg) >= 2: - _, data = reply_seg + if reply_seg["type"] == "typo": + data = reply_seg["typo"] else: - # 向下兼容:如果已经是字符串,则直接使用 - data = str(reply_seg) + data = reply_seg["content"] reply_text += data @@ -149,7 +149,7 @@ class ResponseHandler: continue if not first_replied: - await send_api.text_to_stream( + sent_message = await send_api.text_to_stream( text=data, stream_id=self.context.stream_id, reply_to_message=message_data, @@ -158,12 +158,32 @@ class ResponseHandler: ) first_replied = True else: - await send_api.text_to_stream( + sent_message = await send_api.text_to_stream( text=data, stream_id=self.context.stream_id, reply_to_message=None, set_reply=False, typing=True, ) + if sent_message and reply_seg["type"] == "typo": + sent_messages.append( + { + "type": "typo", + "message_id": sent_message, + "original_message": message_data, + "correction": reply_seg["correction"], + } + ) - return reply_text + return reply_text, sent_messages + + async def handle_typo_correction(self, sent_messages: list[dict[str, Any]]): + """处理错别字修正""" + for msg in sent_messages: + if msg["type"] == "typo": + await asyncio.sleep(random.uniform(2, 4)) + recalled = await send_api.recall_message(str(msg["message_id"]), self.context.stream_id) + if recalled: + await send_api.text_to_stream( + str(msg["correction"]), self.context.stream_id, reply_to_message=msg["original_message"] + ) diff --git a/src/chat/utils/typo_generator.py b/src/chat/utils/typo_generator.py index 9c3718b2b..c23c4c319 100644 --- a/src/chat/utils/typo_generator.py +++ b/src/chat/utils/typo_generator.py @@ -19,16 +19,22 @@ logger = get_logger("typo_gen") class ChineseTypoGenerator: + """ + 中文错别字生成器。 + 可以根据拼音、字频等信息,为给定的中文句子生成包含错别字的句子。 + 支持单字替换和整词替换。 + """ + def __init__(self, error_rate=0.3, min_freq=5, tone_error_rate=0.2, word_replace_rate=0.3, max_freq_diff=200): """ - 初始化错别字生成器 + 初始化错别字生成器。 - 参数: - error_rate: 单字替换概率 - min_freq: 最小字频阈值 - tone_error_rate: 声调错误概率 - word_replace_rate: 整词替换概率 - max_freq_diff: 最大允许的频率差异 + Args: + error_rate (float): 单个汉字被替换为同音字的概率。 + min_freq (int): 候选替换字的最小词频阈值,低于此阈值的字将被忽略。 + tone_error_rate (float): 在选择同音字时,使用错误声调的概率。 + word_replace_rate (float): 整个词语被替换为同音词的概率。 + max_freq_diff (int): 允许的原始字与替换字之间的最大频率差异。 """ self.error_rate = error_rate self.min_freq = min_freq @@ -36,42 +42,47 @@ class ChineseTypoGenerator: self.word_replace_rate = word_replace_rate self.max_freq_diff = max_freq_diff - # 加载数据 - # print("正在加载汉字数据库,请稍候...") - # logger.info("正在加载汉字数据库,请稍候...") - + # 加载核心数据 + logger.info("正在加载汉字数据库...") self.pinyin_dict = self._create_pinyin_dict() self.char_frequency = self._load_or_create_char_frequency() + logger.info("汉字数据库加载完成。") def _load_or_create_char_frequency(self): """ - 加载或创建汉字频率字典 + 加载或创建汉字频率字典。 + 如果存在缓存文件 `depends-data/char_frequency.json`,则直接加载。 + 否则,通过解析 `jieba` 的词典文件来创建,并保存为缓存。 + + Returns: + dict: 一个将汉字映射到其归一化频率的字典。 """ cache_file = Path("depends-data/char_frequency.json") - # 如果缓存文件存在,直接加载 + # 如果缓存文件存在,则直接从缓存加载,提高效率 if cache_file.exists(): with open(cache_file, "r", encoding="utf-8") as f: return orjson.loads(f.read()) - # 使用内置的词频文件 + # 如果没有缓存,则通过解析jieba词典来创建 char_freq = defaultdict(int) + # 定位jieba内置词典文件的路径 dict_path = os.path.join(os.path.dirname(jieba.__file__), "dict.txt") - # 读取jieba的词典文件 + # 读取jieba词典文件,统计每个汉字的频率 with open(dict_path, "r", encoding="utf-8") as f: for line in f: word, freq = line.strip().split()[:2] - # 对词中的每个字进行频率累加 + # 将词中每个汉字的频率进行累加 for char in word: if self._is_chinese_char(char): char_freq[char] += int(freq) - # 归一化频率值 + # 对频率值进行归一化处理,使其在0-1000的范围内 max_freq = max(char_freq.values()) normalized_freq = {char: freq / max_freq * 1000 for char, freq in char_freq.items()} - # 保存到缓存文件 + # 将计算出的频率数据保存到缓存文件,以便下次快速加载 with open(cache_file, "w", encoding="utf-8") as f: f.write(orjson.dumps(normalized_freq, option=orjson.OPT_INDENT_2).decode("utf-8")) @@ -80,18 +91,24 @@ class ChineseTypoGenerator: @staticmethod def _create_pinyin_dict(): """ - 创建拼音到汉字的映射字典 + 创建从拼音到汉字的映射字典。 + 遍历常用汉字范围,为每个汉字生成带声调的拼音,并构建映射。 + + Returns: + defaultdict: 一个将拼音映射到汉字列表的字典。 """ - # 常用汉字范围 + # 定义常用汉字的Unicode范围 chars = [chr(i) for i in range(0x4E00, 0x9FFF)] pinyin_dict = defaultdict(list) - # 为每个汉字建立拼音映射 + # 为范围内的每个汉字建立拼音到汉字的映射 for char in chars: try: - py = pinyin(char, style=Style.TONE3)[0][0] + # 获取带数字声调的拼音 (e.g., 'hao3') + py = pinyin(char, style=Style.TONE3) pinyin_dict[py].append(char) except Exception: + # 忽略无法转换拼音的字符 continue return pinyin_dict @@ -99,49 +116,62 @@ class ChineseTypoGenerator: @staticmethod def _is_chinese_char(char): """ - 判断是否为汉字 + 判断一个字符是否为中文字符。 + + Args: + char (str): 需要判断的字符。 + + Returns: + bool: 如果是中文字符,返回 True,否则返回 False。 """ try: + # 通过Unicode范围判断是否为中文字符 return "\u4e00" <= char <= "\u9fff" except Exception as e: - logger.debug(str(e)) + logger.debug(f"判断字符 '{char}' 时出错: {e}") return False def _get_pinyin(self, sentence): """ - 将中文句子拆分成单个汉字并获取其拼音 - """ - # 将句子拆分成单个字符 - characters = list(sentence) + 获取一个句子中每个汉字的拼音。 - # 获取每个字符的拼音 + Args: + sentence (str): 输入的中文句子。 + + Returns: + list: 一个元组列表,每个元组包含 (汉字, 拼音)。 + """ + characters = list(sentence) result = [] for char in characters: - # 跳过空格和非汉字字符 - if char.isspace() or not self._is_chinese_char(char): - continue - # 获取拼音(数字声调) - py = pinyin(char, style=Style.TONE3)[0][0] - result.append((char, py)) - + # 忽略所有非中文字符 + if self._is_chinese_char(char): + # 获取带数字声调的拼音 + py = pinyin(char, style=Style.TONE3) + result.append((char, py)) return result @staticmethod def _get_similar_tone_pinyin(py): """ - 获取相似声调的拼音 + 为一个给定的拼音生成一个声调错误的相似拼音。 + + Args: + py (str): 带数字声调的原始拼音 (e.g., 'hao3')。 + + Returns: + str: 一个声调被随机改变的拼音。 """ - # 检查拼音是否为空或无效 + # 检查拼音是否有效 if not py or len(py) < 1: return py - # 如果最后一个字符不是数字,说明可能是轻声或其他特殊情况 + # 如果拼音末尾不是数字(如轻声),则默认添加一声 if not py[-1].isdigit(): - # 为非数字结尾的拼音添加数字声调1 return f"{py}1" - base = py[:-1] # 去掉声调 - tone = int(py[-1]) # 获取声调 + base = py[:-1] # 拼音的基本部分 (e.g., 'hao') + tone = int(py[-1]) # 声调 (e.g., 3) # 处理轻声(通常用5表示)或无效声调 if tone not in [1, 2, 3, 4]: @@ -155,40 +185,56 @@ class ChineseTypoGenerator: def _calculate_replacement_probability(self, orig_freq, target_freq): """ - 根据频率差计算替换概率 + 根据原始字和目标替换字的频率差异,计算替换概率。 + 频率相近的字有更高的替换概率。 + + Args: + orig_freq (float): 原始字的频率。 + target_freq (float): 目标替换字的频率。 + + Returns: + float: 替换概率,介于 0.0 和 1.0 之间。 """ + # 如果目标字更常用,则替换概率为1 if target_freq > orig_freq: - return 1.0 # 如果替换字频率更高,保持原有概率 + return 1.0 freq_diff = orig_freq - target_freq + # 如果频率差异过大,则不进行替换 if freq_diff > self.max_freq_diff: - return 0.0 # 频率差太大,不替换 + return 0.0 - # 使用指数衰减函数计算概率 - # 频率差为0时概率为1,频率差为max_freq_diff时概率接近0 + # 使用指数衰减函数来计算概率,频率差异越大,概率越低 return math.exp(-3 * freq_diff / self.max_freq_diff) def _get_similar_frequency_chars(self, char, py, num_candidates=5): """ - 获取与给定字频率相近的同音字,可能包含声调错误 + 获取与给定汉字发音相似且频率相近的候选替换字。 + + Args: + char (str): 原始汉字。 + py (str): 原始汉字的拼音。 + num_candidates (int): 返回的候选字数量。 + + Returns: + list or None: 一个包含候选替换字的列表,如果没有找到则返回 None。 """ homophones = [] - # 有一定概率使用错误声调 + # 根据设定概率,可能使用声调错误的拼音来寻找候选字 if random.random() < self.tone_error_rate: wrong_tone_py = self._get_similar_tone_pinyin(py) - homophones.extend(self.pinyin_dict[wrong_tone_py]) + homophones.extend(self.pinyin_dict.get(wrong_tone_py, [])) - # 添加正确声调的同音字 - homophones.extend(self.pinyin_dict[py]) + # 添加声调正确的同音字 + homophones.extend(self.pinyin_dict.get(py, [])) if not homophones: return None - # 获取原字的频率 orig_freq = self.char_frequency.get(char, 0) - # 计算所有同音字与原字的频率差,并过滤掉低频字 + # 过滤掉低频字和原始字本身 freq_diff = [ (h, self.char_frequency.get(h, 0)) for h in homophones @@ -202,222 +248,215 @@ class ChineseTypoGenerator: candidates_with_prob = [] for h, freq in freq_diff: prob = self._calculate_replacement_probability(orig_freq, freq) - if prob > 0: # 只保留有效概率的候选字 + if prob > 0: candidates_with_prob.append((h, prob)) if not candidates_with_prob: return None - # 根据概率排序 - candidates_with_prob.sort(key=lambda x: x[1], reverse=True) + # 根据替换概率从高到低排序 + candidates_with_prob.sort(key=lambda x: x, reverse=True) - # 返回概率最高的几个字 - return [char for char, _ in candidates_with_prob[:num_candidates]] + # 返回概率最高的几个候选字 + return [c for c, _ in candidates_with_prob[:num_candidates]] @staticmethod def _get_word_pinyin(word): """ - 获取词语的拼音列表 + 获取一个词语中每个汉字的拼音列表。 + + Args: + word (str): 输入的词语。 + + Returns: + list: 包含每个汉字拼音的列表。 """ - return [py[0] for py in pinyin(word, style=Style.TONE3)] + return [py for py in pinyin(word, style=Style.TONE3)] @staticmethod def _segment_sentence(sentence): """ - 使用jieba分词,返回词语列表 + 使用 jieba 对句子进行分词。 + + Args: + sentence (str): 输入的句子。 + + Returns: + list: 分词后的词语列表。 """ return list(jieba.cut(sentence)) def _get_word_homophones(self, word): """ - 获取整个词的同音词,只返回高频的有意义词语 + 获取一个词语的同音词。 + 只返回在jieba词典中存在且频率较高的有意义词语。 + + Args: + word (str): 原始词语。 + + Returns: + list: 一个包含同音词的列表。 """ - if len(word) == 1: + if len(word) <= 1: return [] - # 获取词的拼音 word_pinyin = self._get_word_pinyin(word) - # 遍历所有可能的同音字组合 + # 为词语中的每个字找到所有同音字 candidates = [] for py in word_pinyin: chars = self.pinyin_dict.get(py, []) if not chars: - return [] + return [] # 如果某个字没有同音字,则无法构成同音词 candidates.append(chars) - # 生成所有可能的组合 + # 生成所有可能的同音字组合 import itertools all_combinations = itertools.product(*candidates) - # 获取jieba词典和词频信息 + # 加载jieba词典以验证组合出的词是否为有效词语 dict_path = os.path.join(os.path.dirname(jieba.__file__), "dict.txt") - valid_words = {} # 改用字典存储词语及其频率 + valid_words = {} with open(dict_path, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split() if len(parts) >= 2: - word_text = parts[0] - word_freq = float(parts[1]) # 获取词频 - valid_words[word_text] = word_freq + valid_words[parts] = float(parts[0][1]) - # 获取原词的词频作为参考 original_word_freq = valid_words.get(word, 0) - min_word_freq = original_word_freq * 0.1 # 设置最小词频为原词频的10% + # 设置一个最小词频阈值,过滤掉非常生僻的词 + min_word_freq = original_word_freq * 0.1 - # 过滤和计算频率 homophones = [] for combo in all_combinations: new_word = "".join(combo) + # 检查新词是否为有效词语且与原词不同 if new_word != word and new_word in valid_words: new_word_freq = valid_words[new_word] - # 只保留词频达到阈值的词 if new_word_freq >= min_word_freq: - # 计算词的平均字频(考虑字频和词频) + # 计算综合评分,结合词频和平均字频 char_avg_freq = sum(self.char_frequency.get(c, 0) for c in new_word) / len(new_word) - # 综合评分:结合词频和字频 combined_score = new_word_freq * 0.7 + char_avg_freq * 0.3 if combined_score >= self.min_freq: homophones.append((new_word, combined_score)) - # 按综合分数排序并限制返回数量 - sorted_homophones = sorted(homophones, key=lambda x: x[1], reverse=True) - return [word for word, _ in sorted_homophones[:5]] # 限制返回前5个结果 + # 按综合分数排序并返回前5个结果 + sorted_homophones = sorted(homophones, key=lambda x: x, reverse=True) + return [w for w, _ in sorted_homophones[:5]] def create_typo_sentence(self, sentence): """ - 创建包含同音字错误的句子,支持词语级别和字级别的替换 + 为输入句子生成一个包含错别字的版本。 + 该方法会先对句子进行分词,然后根据概率进行整词替换或单字替换。 - 参数: - sentence: 输入的中文句子 + Args: + sentence (str): 原始中文句子。 - 返回: - typo_sentence: 包含错别字的句子 - correction_suggestion: 随机选择的一个纠正建议,返回正确的字/词 + Returns: + tuple: 包含三个元素的元组: + - original_sentence (str): 原始句子。 + - typo_sentence (str): 包含错别字的句子。 + - correction_suggestion (str or None): 一个随机的修正建议(可能是正确的字或词),或 None。 """ result = [] - typo_info = [] - word_typos = [] # 记录词语错误对(错词,正确词) - char_typos = [] # 记录单字错误对(错字,正确字) - current_pos = 0 + typo_info = [] # 用于调试,记录详细的替换信息 + word_typos = [] # 记录 (错词, 正确词) + char_typos = [] # 记录 (错字, 正确字) - # 分词 + # 对句子进行分词 words = self._segment_sentence(sentence) for word in words: - # 如果是标点符号或空格,直接添加 + # 如果是标点符号或非中文字符,直接保留 if all(not self._is_chinese_char(c) for c in word): result.append(word) - current_pos += len(word) continue - # 获取词语的拼音 word_pinyin = self._get_word_pinyin(word) - # 尝试整词替换 + # 步骤1: 尝试进行整词替换 if len(word) > 1 and random.random() < self.word_replace_rate: word_homophones = self._get_word_homophones(word) if word_homophones: typo_word = random.choice(word_homophones) - # 计算词的平均频率 orig_freq = sum(self.char_frequency.get(c, 0) for c in word) / len(word) typo_freq = sum(self.char_frequency.get(c, 0) for c in typo_word) / len(typo_word) - # 添加到结果中 result.append(typo_word) typo_info.append( ( word, typo_word, - " ".join(word_pinyin), + " ".join(self._get_word_pinyin(word)), " ".join(self._get_word_pinyin(typo_word)), orig_freq, typo_freq, ) ) - word_typos.append((typo_word, word)) # 记录(错词,正确词)对 - current_pos += len(typo_word) + word_typos.append((typo_word, word)) continue - # 如果不进行整词替换,则进行单字替换 - if len(word) == 1: - char = word - py = word_pinyin[0] - if random.random() < self.error_rate: + # 步骤2: 如果不进行整词替换,则对词中的每个字进行单字替换 + new_word = [] + for char, py in zip(word, word_pinyin, strict=False): + # 词语越长,其中单个字被替换的概率越低 + char_error_rate = self.error_rate * (0.7 ** (len(word) - 1)) + if random.random() < char_error_rate: similar_chars = self._get_similar_frequency_chars(char, py) if similar_chars: typo_char = random.choice(similar_chars) - typo_freq = self.char_frequency.get(typo_char, 0) orig_freq = self.char_frequency.get(char, 0) - replace_prob = self._calculate_replacement_probability(orig_freq, typo_freq) - if random.random() < replace_prob: - result.append(typo_char) - typo_py = pinyin(typo_char, style=Style.TONE3)[0][0] + typo_freq = self.char_frequency.get(typo_char, 0) + # 根据频率计算最终是否替换 + if random.random() < self._calculate_replacement_probability(orig_freq, typo_freq): + new_word.append(typo_char) + typo_py = pinyin(typo_char, style=Style.TONE3) typo_info.append((char, typo_char, py, typo_py, orig_freq, typo_freq)) - char_typos.append((typo_char, char)) # 记录(错字,正确字)对 - current_pos += 1 + char_typos.append((typo_char, char)) continue - result.append(char) - current_pos += 1 - else: - # 处理多字词的单字替换 - word_result = [] - for _, (char, py) in enumerate(zip(word, word_pinyin, strict=False)): - # 词中的字替换概率降低 - word_error_rate = self.error_rate * (0.7 ** (len(word) - 1)) + # 如果不替换,则保留原字 + new_word.append(char) - if random.random() < word_error_rate: - similar_chars = self._get_similar_frequency_chars(char, py) - if similar_chars: - typo_char = random.choice(similar_chars) - typo_freq = self.char_frequency.get(typo_char, 0) - orig_freq = self.char_frequency.get(char, 0) - replace_prob = self._calculate_replacement_probability(orig_freq, typo_freq) - if random.random() < replace_prob: - word_result.append(typo_char) - typo_py = pinyin(typo_char, style=Style.TONE3)[0][0] - typo_info.append((char, typo_char, py, typo_py, orig_freq, typo_freq)) - char_typos.append((typo_char, char)) # 记录(错字,正确字)对 - continue - word_result.append(char) - result.append("".join(word_result)) - current_pos += len(word) + result.append("".join(new_word)) - # 优先从词语错误中选择,如果没有则从单字错误中选择 + # 步骤3: 生成修正建议 correction_suggestion = None - # 50%概率返回纠正建议 + # 有50%的概率提供一个修正建议 if random.random() < 0.5: + # 优先从整词错误中选择 if word_typos: - wrong_word, correct_word = random.choice(word_typos) + _, correct_word = random.choice(word_typos) correction_suggestion = correct_word + # 其次从单字错误中选择 elif char_typos: - wrong_char, correct_char = random.choice(char_typos) + _, correct_char = random.choice(char_typos) correction_suggestion = correct_char - return "".join(result), correction_suggestion + return sentence, "".join(result), correction_suggestion @staticmethod def format_typo_info(typo_info): """ - 格式化错别字信息 + 将错别字生成过程中的详细信息格式化为可读字符串。 - 参数: - typo_info: 错别字信息列表 + Args: + typo_info (list): `create_typo_sentence` 方法生成的详细信息列表。 - 返回: - 格式化后的错别字信息字符串 + Returns: + str: 格式化后的字符串,用于调试和分析。 """ if not typo_info: return "未生成错别字" result = [] for orig, typo, orig_py, typo_py, orig_freq, typo_freq in typo_info: - # 判断是否为词语替换 + # 判断是整词替换还是单字替换 is_word = " " in orig_py if is_word: error_type = "整词替换" else: + # 判断是声调错误还是同音字替换 tone_error = orig_py[:-1] == typo_py[:-1] and orig_py[-1] != typo_py[-1] error_type = "声调错误" if tone_error else "同音字替换" @@ -430,21 +469,22 @@ class ChineseTypoGenerator: def set_params(self, **kwargs): """ - 设置参数 + 动态设置生成器的参数。 - 可设置参数: - error_rate: 单字替换概率 - min_freq: 最小字频阈值 - tone_error_rate: 声调错误概率 - word_replace_rate: 整词替换概率 - max_freq_diff: 最大允许的频率差异 + Args: + **kwargs: 键值对参数,可设置的参数包括: + - error_rate (float) + - min_freq (int) + - tone_error_rate (float) + - word_replace_rate (float) + - max_freq_diff (int) """ for key, value in kwargs.items(): if hasattr(self, key): setattr(self, key, value) - print(f"参数 {key} 已设置为 {value}") + logger.info(f"参数 {key} 已更新为 {value}") else: - print(f"警告: 参数 {key} 不存在") + logger.warning(f"尝试设置不存在的参数: {key}") def main(): @@ -456,10 +496,10 @@ def main(): # 创建包含错别字的句子 start_time = time.time() - typo_sentence, correction_suggestion = typo_generator.create_typo_sentence(sentence) + original_sentence, typo_sentence, correction_suggestion = typo_generator.create_typo_sentence(sentence) # 打印结果 - print("\n原句:", sentence) + print("\n原句:", original_sentence) print("错字版:", typo_sentence) # 打印纠正建议 diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 501bf382d..dd60ef951 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -293,9 +293,11 @@ def random_remove_punctuation(text: str) -> str: return result -def process_llm_response(text: str, enable_splitter: bool = True, enable_chinese_typo: bool = True) -> list[str]: +def process_llm_response( + text: str, enable_splitter: bool = True, enable_chinese_typo: bool = True +) -> list[dict[str, str]]: if not global_config.response_post_process.enable_response_post_process: - return [text] + return [{"type": "text", "content": text}] # 先保护颜文字 if global_config.response_splitter.enable_kaomoji_protection: @@ -311,7 +313,7 @@ def process_llm_response(text: str, enable_splitter: bool = True, enable_chinese cleaned_text = pattern.sub("", protected_text) if cleaned_text == "": - return ["呃呃"] + return [{"type": "text", "content": "呃呃"}] logger.debug(f"{text}去除括号处理后的文本: {cleaned_text}") @@ -321,7 +323,7 @@ def process_llm_response(text: str, enable_splitter: bool = True, enable_chinese # 如果基本上是中文,则进行长度过滤 if get_western_ratio(cleaned_text) < 0.1 and len(cleaned_text) > max_length: logger.warning(f"回复过长 ({len(cleaned_text)} 字符),返回默认回复") - return ["懒得说"] + return [{"type": "text", "content": "懒得说"}] typo_generator = ChineseTypoGenerator( error_rate=global_config.chinese_typo.error_rate, @@ -338,16 +340,24 @@ def process_llm_response(text: str, enable_splitter: bool = True, enable_chinese sentences = [] for sentence in split_sentences: if global_config.chinese_typo.enable and enable_chinese_typo: - typoed_text, typo_corrections = typo_generator.create_typo_sentence(sentence) - sentences.append(typoed_text) + original_sentence, typo_sentence, typo_corrections = typo_generator.create_typo_sentence(sentence) if typo_corrections: - sentences.append(typo_corrections) + sentences.append( + { + "type": "typo", + "original": original_sentence, + "typo": typo_sentence, + "correction": typo_corrections, + } + ) + else: + sentences.append({"type": "text", "content": sentence}) else: - sentences.append(sentence) + sentences.append({"type": "text", "content": sentence}) if len(sentences) > max_sentence_num: logger.warning(f"分割后消息数量过多 ({len(sentences)} 条),返回默认回复") - return [f"{global_config.bot.nickname}不知道哦"] + return [{"type": "text", "content": f"{global_config.bot.nickname}不知道哦"}] # if extracted_contents: # for content in extracted_contents: @@ -355,7 +365,20 @@ def process_llm_response(text: str, enable_splitter: bool = True, enable_chinese # 在所有句子处理完毕后,对包含占位符的列表进行恢复 if global_config.response_splitter.enable_kaomoji_protection: - sentences = recover_kaomoji(sentences, kaomoji_mapping) + # sentences中的元素可能是dict,也可能是str,所以要分开处理 + recovered_sentences = [] + for s in sentences: + if isinstance(s, dict) and s.get("type") == "typo": + s["original"] = recover_kaomoji([s["original"]], kaomoji_mapping) + s["typo"] = recover_kaomoji([s["typo"]], kaomoji_mapping) + s["correction"] = recover_kaomoji([s["correction"]], kaomoji_mapping) + recovered_sentences.append(s) + elif isinstance(s, dict) and s.get("type") == "text": + s["content"] = recover_kaomoji([s["content"]], kaomoji_mapping) + recovered_sentences.append(s) + else: + recovered_sentences.append(recover_kaomoji([s], kaomoji_mapping)) + sentences = recovered_sentences return sentences diff --git a/src/plugin_system/apis/generator_api.py b/src/plugin_system/apis/generator_api.py index b20909e77..3ea507cc0 100644 --- a/src/plugin_system/apis/generator_api.py +++ b/src/plugin_system/apis/generator_api.py @@ -83,7 +83,7 @@ async def generate_reply( return_prompt: bool = False, request_type: str = "generator_api", from_plugin: bool = True, -) -> Tuple[bool, List[Tuple[str, Any]], Optional[str]]: +) -> Tuple[bool, List[Dict[str, Any]], Optional[str]]: """生成回复 Args: @@ -167,7 +167,7 @@ async def rewrite_reply( reply_to: str = "", return_prompt: bool = False, request_type: str = "generator_api", -) -> Tuple[bool, List[Tuple[str, Any]], Optional[str]]: +) -> Tuple[bool, List[Dict[str, Any]], Optional[str]]: """重写回复 Args: @@ -225,7 +225,9 @@ async def rewrite_reply( return False, [], None -def process_human_text(content: str, enable_splitter: bool, enable_chinese_typo: bool) -> List[Tuple[str, Any]]: +def process_human_text( + content: str, enable_splitter: bool, enable_chinese_typo: bool +) -> List[Dict[str, Any]]: """将文本处理为更拟人化的文本 Args: @@ -239,9 +241,11 @@ def process_human_text(content: str, enable_splitter: bool, enable_chinese_typo: processed_response = process_llm_response(content, enable_splitter, enable_chinese_typo) reply_set = [] - for text in processed_response: - reply_seg = ("text", text) - reply_set.append(reply_seg) + for item in processed_response: + if item["type"] == "typo": + reply_set.append(item) + else: + reply_set.append({"type": "text", "content": item["content"]}) return reply_set diff --git a/src/plugin_system/apis/send_api.py b/src/plugin_system/apis/send_api.py index 7a4d371a2..9c629fc3b 100644 --- a/src/plugin_system/apis/send_api.py +++ b/src/plugin_system/apis/send_api.py @@ -178,7 +178,7 @@ async def _send_to_target( # 构建机器人用户信息 bot_user_info = UserInfo( - user_id=global_config.bot.qq_account, + user_id=str(global_config.bot.qq_account), user_nickname=global_config.bot.nickname, platform=target_stream.platform, ) @@ -188,10 +188,13 @@ async def _send_to_target( if reply_to_message: anchor_message = message_dict_to_message_recv(message_dict=reply_to_message) - anchor_message.update_chat_stream(target_stream) - reply_to_platform_id = ( - f"{anchor_message.message_info.platform}:{anchor_message.message_info.user_info.user_id}" - ) + if anchor_message and anchor_message.message_info and anchor_message.message_info.user_info: + anchor_message.update_chat_stream(target_stream) + reply_to_platform_id = ( + f"{anchor_message.message_info.platform}:{anchor_message.message_info.user_info.user_id}" + ) + else: + reply_to_platform_id = None else: anchor_message = None reply_to_platform_id = None @@ -421,10 +424,10 @@ async def adapter_command_to_stream( # 创建临时的用户信息和聊天流 - temp_user_info = UserInfo(user_id="system", user_nickname="System", platform=platform) + temp_user_info = UserInfo(user_id="system", user_nickname="System", platform=platform or "qq") temp_chat_stream = ChatStream( - stream_id=stream_id, platform=platform, user_info=temp_user_info, group_info=None + stream_id=stream_id, platform=platform or "qq", user_info=temp_user_info, group_info=None ) target_stream = temp_chat_stream @@ -441,7 +444,7 @@ async def adapter_command_to_stream( # 构建机器人用户信息 bot_user_info = UserInfo( - user_id=global_config.bot.qq_account, + user_id=str(global_config.bot.qq_account), user_nickname=global_config.bot.nickname, platform=target_stream.platform, ) @@ -494,3 +497,21 @@ async def adapter_command_to_stream( logger.error(f"[SendAPI] 发送适配器命令时出错: {e}") traceback.print_exc() return {"status": "error", "message": f"发送适配器命令时出错: {str(e)}"} + + +async def recall_message(message_id: str, stream_id: str) -> bool: + """撤回消息 + + Args: + message_id: 消息ID + stream_id: 聊天流ID + + Returns: + bool: 是否成功 + """ + response = await adapter_command_to_stream( + action="delete_msg", + params={"message_id": message_id}, + stream_id=stream_id, + ) + return response.get("status") == "ok" From a3d0588e7efe37a5ebbd97a56dcfc385853f64b4 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 15:52:21 +0800 Subject: [PATCH 27/69] =?UTF-8?q?docs(core):=20=E4=B8=BA=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=B7=BB=E5=8A=A0=E6=97=A5=E5=BF=97=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=92=8C=E6=96=87=E6=A1=A3=E5=AD=97=E7=AC=A6=E4=B8=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 `cycle_processor`, `response_handler`, `generator_api`, 和 `send_api` 等核心代码文件补充了日志记录器和详细的文档字符串(docstrings)。 本次更新旨在提高代码的可读性和可维护性,通过清晰的文档注释和日志输出,使其他开发者能更容易地理解代码逻辑和功能,并为未来的调试和功能扩展提供便利。 --- src/chat/chat_loop/cycle_processor.py | 48 +++++++++++++++++++++++-- src/chat/chat_loop/response_handler.py | 20 +++++++++++ src/plugin_system/apis/generator_api.py | 17 +++++++++ src/plugin_system/apis/send_api.py | 2 ++ 4 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index 4a39ddd0e..a1862f515 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -19,10 +19,14 @@ from .hfc_context import HfcContext from .response_handler import ResponseHandler from .cycle_tracker import CycleTracker +# 日志记录器 logger = get_logger("hfc.processor") class CycleProcessor: + """ + 循环处理器类,负责处理单次思考循环的逻辑。 + """ def __init__(self, context: HfcContext, response_handler: ResponseHandler, cycle_tracker: CycleTracker): """ 初始化循环处理器 @@ -51,14 +55,30 @@ class CycleProcessor: thinking_id, actions, ) -> Tuple[Dict[str, Any], str, Dict[str, float]]: + """ + 发送并存储回复信息 + + Args: + response_set: 回复内容集合 + loop_start_time: 循环开始时间 + action_message: 动作消息 + cycle_timers: 循环计时器 + thinking_id: 思考ID + actions: 动作列表 + + Returns: + Tuple[Dict[str, Any], str, Dict[str, float]]: 循环信息, 回复文本, 循环计时器 + """ + # 发送回复 with Timer("回复发送", cycle_timers): reply_text, sent_messages = await self.response_handler.send_response( response_set, loop_start_time, action_message ) if sent_messages: + # 异步处理错别字修正 asyncio.create_task(self.response_handler.handle_typo_correction(sent_messages)) - # 存储reply action信息 + # 存储reply action信息 person_info_manager = get_person_info_manager() # 获取 platform,如果不存在则从 chat_stream 获取,如果还是 None 则使用默认值 @@ -66,6 +86,7 @@ class CycleProcessor: if platform is None: platform = getattr(self.context.chat_stream, "platform", "unknown") + # 获取用户信息并生成回复提示 person_id = person_info_manager.get_person_id( platform, action_message.get("user_id", ""), @@ -73,6 +94,7 @@ class CycleProcessor: person_name = await person_info_manager.get_value(person_id, "person_name") action_prompt_display = f"你对{person_name}进行了回复:{reply_text}" + # 存储动作信息到数据库 await database_api.store_action_info( chat_stream=self.context.chat_stream, action_build_into_prompt=False, @@ -106,7 +128,7 @@ class CycleProcessor: interest_value: 兴趣值 Returns: - bool: 处理是否成功 + str: 动作类型 功能说明: - 开始新的思考循环并记录计时 @@ -122,6 +144,15 @@ class CycleProcessor: # 当interest_value为0时,概率接近0(使用Focus模式) # 当interest_value很高时,概率接近1(使用Normal模式) def calculate_normal_mode_probability(interest_val: float) -> float: + """ + 计算普通模式的概率 + + Args: + interest_val: 兴趣值 + + Returns: + float: 概率 + """ # 使用sigmoid函数,调整参数使概率分布更合理 # 当interest_value = 0时,概率约为0.1 # 当interest_value = 1时,概率约为0.5 @@ -131,6 +162,7 @@ class CycleProcessor: x0 = 1.0 # 控制曲线中心点 return 1.0 / (1.0 + math.exp(-k * (interest_val - x0))) + # 计算普通模式概率 normal_mode_probability = ( calculate_normal_mode_probability(interest_value) * 0.5 @@ -149,9 +181,11 @@ class CycleProcessor: f"{self.log_prefix} 基于兴趣值 {interest_value:.2f},概率 {normal_mode_probability:.2f},选择Focus planner模式" ) + # 开始新的思考循环 cycle_timers, thinking_id = self.cycle_tracker.start_cycle() logger.info(f"{self.log_prefix} 开始第{self.context.cycle_counter}次思考") + # 发送正在输入状态 if ENABLE_S4U and self.context.chat_stream and self.context.chat_stream.user_info: await send_typing(self.context.chat_stream.user_info.user_id) @@ -176,12 +210,14 @@ class CycleProcessor: from src.plugin_system.core.event_manager import event_manager from src.plugin_system import EventType + # 触发规划前事件 result = await event_manager.trigger_event( EventType.ON_PLAN, plugin_name="SYSTEM", stream_id=self.context.chat_stream ) if result and not result.all_continue_process(): raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于规划前中断了内容生成") + # 规划动作 with Timer("规划器", cycle_timers): actions, _ = await self.action_planner.plan( mode=mode, @@ -227,6 +263,7 @@ class CycleProcessor: "command": command, } else: + # 生成回复 try: success, response_set, _ = await generator_api.generate_reply( chat_stream=self.context.chat_stream, @@ -245,6 +282,7 @@ class CycleProcessor: logger.debug(f"{self.log_prefix} 并行执行:回复生成任务已被取消") return {"action_type": "reply", "success": False, "reply_text": "", "loop_info": None} + # 发送并存储回复 loop_info, reply_text, cycle_timers_reply = await self._send_and_store_reply( response_set, loop_start_time, @@ -323,13 +361,15 @@ class CycleProcessor: } reply_text = action_reply_text + # 停止正在输入状态 if ENABLE_S4U: await stop_typing() + # 结束循环 self.context.chat_instance.cycle_tracker.end_cycle(loop_info, cycle_timers) self.context.chat_instance.cycle_tracker.print_cycle_info(cycle_timers) - action_type = actions[0]["action_type"] if actions else "no_action" + action_type = actions["action_type"] if actions else "no_action" return action_type async def _handle_action( @@ -357,6 +397,7 @@ class CycleProcessor: if not self.context.chat_stream: return False, "", "" try: + # 创建动作处理器 action_handler = self.context.action_manager.create_action( action_name=action, action_data=action_data, @@ -398,6 +439,7 @@ class CycleProcessor: logger.error(f"{self.context.log_prefix} 回退方案也失败,无法创建任何动作处理器") return False, "", "" + # 执行动作 success, reply_text = await action_handler.handle_action() return success, reply_text, "" except Exception as e: diff --git a/src/chat/chat_loop/response_handler.py b/src/chat/chat_loop/response_handler.py index 982b7d54d..8b56127e0 100644 --- a/src/chat/chat_loop/response_handler.py +++ b/src/chat/chat_loop/response_handler.py @@ -10,11 +10,15 @@ from .hfc_context import HfcContext # 导入反注入系统 +# 日志记录器 logger = get_logger("hfc") anti_injector_logger = get_logger("anti_injector") class ResponseHandler: + """ + 响应处理器类,负责生成和发送机器人的回复。 + """ def __init__(self, context: HfcContext): """ 初始化响应处理器 @@ -60,12 +64,15 @@ class ResponseHandler: - 构建并返回完整的循环信息 - 用于上级方法的状态跟踪 """ + # 发送回复 reply_text, sent_messages = await self.send_response(response_set, loop_start_time, action_message) if sent_messages: + # 异步处理错别字修正 asyncio.create_task(self.handle_typo_correction(sent_messages)) person_info_manager = get_person_info_manager() + # 获取平台信息 platform = "default" if self.context.chat_stream: platform = ( @@ -74,11 +81,13 @@ class ResponseHandler: or self.context.chat_stream.platform ) + # 获取用户信息并生成回复提示 user_id = action_message.get("user_id", "") person_id = person_info_manager.get_person_id(platform, user_id) person_name = await person_info_manager.get_value(person_id, "person_name") action_prompt_display = f"你对{person_name}进行了回复:{reply_text}" + # 存储动作信息到数据库 await database_api.store_action_info( chat_stream=self.context.chat_stream, action_build_into_prompt=False, @@ -89,6 +98,7 @@ class ResponseHandler: action_name="reply", ) + # 构建循环信息 loop_info: Dict[str, Any] = { "loop_plan_info": { "action_result": plan_result.get("action_result", {}), @@ -123,10 +133,12 @@ class ResponseHandler: - 正确处理元组格式的回复段 """ current_time = time.time() + # 计算新消息数量 new_message_count = message_api.count_new_messages( chat_id=self.context.stream_id, start_time=thinking_start_time, end_time=current_time ) + # 根据新消息数量决定是否需要引用回复 need_reply = new_message_count >= random.randint(2, 4) reply_text = "" @@ -137,6 +149,7 @@ class ResponseHandler: for reply_seg in reply_set: logger.debug(f"Processing reply_seg type: {type(reply_seg)}, content: {reply_seg}") + # 提取回复内容 if reply_seg["type"] == "typo": data = reply_seg["typo"] else: @@ -144,10 +157,12 @@ class ResponseHandler: reply_text += data + # 如果是主动思考且内容为“沉默”,则不发送 if is_proactive_thinking and data.strip() == "沉默": logger.info(f"{self.context.log_prefix} 主动思考决定保持沉默,不发送消息") continue + # 发送第一段回复 if not first_replied: sent_message = await send_api.text_to_stream( text=data, @@ -158,6 +173,7 @@ class ResponseHandler: ) first_replied = True else: + # 发送后续回复 sent_message = await send_api.text_to_stream( text=data, stream_id=self.context.stream_id, @@ -165,6 +181,7 @@ class ResponseHandler: set_reply=False, typing=True, ) + # 记录已发送的错别字消息 if sent_message and reply_seg["type"] == "typo": sent_messages.append( { @@ -181,9 +198,12 @@ class ResponseHandler: """处理错别字修正""" for msg in sent_messages: if msg["type"] == "typo": + # 随机等待一段时间 await asyncio.sleep(random.uniform(2, 4)) + # 撤回消息 recalled = await send_api.recall_message(str(msg["message_id"]), self.context.stream_id) if recalled: + # 发送修正后的消息 await send_api.text_to_stream( str(msg["correction"]), self.context.stream_id, reply_to_message=msg["original_message"] ) diff --git a/src/plugin_system/apis/generator_api.py b/src/plugin_system/apis/generator_api.py index 3ea507cc0..7f3074a81 100644 --- a/src/plugin_system/apis/generator_api.py +++ b/src/plugin_system/apis/generator_api.py @@ -20,6 +20,7 @@ from src.plugin_system.base.component_types import ActionInfo install(extra_lines=3) +# 日志记录器 logger = get_logger("generator_api") @@ -113,6 +114,7 @@ async def generate_reply( logger.debug("[GeneratorAPI] 开始生成回复") + # 向下兼容,从action_data中获取reply_to和extra_info if not reply_to and action_data: reply_to = action_data.get("reply_to", "") if not extra_info and action_data: @@ -133,6 +135,7 @@ async def generate_reply( return False, [], None assert llm_response_dict is not None, "llm_response_dict不应为None" # 虽然说不会出现llm_response为空的情况 if content := llm_response_dict.get("content", ""): + # 处理为拟人化文本 reply_set = process_human_text(content, enable_splitter, enable_chinese_typo) else: reply_set = [] @@ -208,6 +211,7 @@ async def rewrite_reply( ) reply_set = [] if content: + # 处理为拟人化文本 reply_set = process_human_text(content, enable_splitter, enable_chinese_typo) if success: @@ -238,6 +242,7 @@ def process_human_text( if not isinstance(content, str): raise ValueError("content 必须是字符串类型") try: + # 处理LLM响应 processed_response = process_llm_response(content, enable_splitter, enable_chinese_typo) reply_set = [] @@ -260,6 +265,18 @@ async def generate_response_custom( request_type: str = "generator_api", prompt: str = "", ) -> Optional[str]: + """ + 使用自定义提示生成回复 + + Args: + chat_stream: 聊天流对象 + chat_id: 聊天ID + request_type: 请求类型 + prompt: 自定义提示 + + Returns: + Optional[str]: 生成的回复内容 + """ replyer = get_replyer(chat_stream, chat_id, request_type=request_type) if not replyer: logger.error("[GeneratorAPI] 无法获取回复器") diff --git a/src/plugin_system/apis/send_api.py b/src/plugin_system/apis/send_api.py index 9c629fc3b..3aea3cde4 100644 --- a/src/plugin_system/apis/send_api.py +++ b/src/plugin_system/apis/send_api.py @@ -43,6 +43,7 @@ from src.chat.message_receive.message import MessageSending, MessageRecv from maim_message import Seg from src.config.config import global_config +# 日志记录器 logger = get_logger("send_api") # 适配器命令响应等待池 @@ -186,6 +187,7 @@ async def _send_to_target( # 创建消息段 message_segment = Seg(type=message_type, data=content) # type: ignore + # 处理回复消息 if reply_to_message: anchor_message = message_dict_to_message_recv(message_dict=reply_to_message) if anchor_message and anchor_message.message_info and anchor_message.message_info.user_info: From d16df808ef79cedc1928a03bf13c4e263f2bc7f3 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 16:03:52 +0800 Subject: [PATCH 28/69] =?UTF-8?q?fix(chat):=20=E5=A4=84=E7=90=86=E5=93=8D?= =?UTF-8?q?=E5=BA=94=E6=95=B0=E6=8D=AE=E4=B8=BA=E5=88=97=E8=A1=A8=E6=97=B6?= =?UTF-8?q?=E7=9A=84=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当响应内容`data`是一个列表时,将其所有元素连接成一个字符串,以避免在后续处理中因类型不匹配而引发的错误。 --- .gitignore | 1 + src/chat/chat_loop/response_handler.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 77cadf262..0d6bfa6f7 100644 --- a/.gitignore +++ b/.gitignore @@ -336,3 +336,4 @@ MaiBot.code-workspace /tests /tests .kilocode/rules/MoFox.md +src/chat/planner_actions/planner (2).py diff --git a/src/chat/chat_loop/response_handler.py b/src/chat/chat_loop/response_handler.py index 8b56127e0..61608ccb9 100644 --- a/src/chat/chat_loop/response_handler.py +++ b/src/chat/chat_loop/response_handler.py @@ -155,6 +155,8 @@ class ResponseHandler: else: data = reply_seg["content"] + if isinstance(data, list): + data = "".join(map(str, data)) reply_text += data # 如果是主动思考且内容为“沉默”,则不发送 From 76ad9279191bcadcaf3c6e09a92b06afebfc751c Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 16:10:45 +0800 Subject: [PATCH 29/69] =?UTF-8?q?fix(chat):=20=E4=BF=AE=E5=A4=8D=E5=8A=A8?= =?UTF-8?q?=E4=BD=9C=E5=88=97=E8=A1=A8=E4=B8=8D=E4=B8=BA=E7=A9=BA=E6=97=B6?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E5=8A=A8=E4=BD=9C=E7=B1=BB=E5=9E=8B=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98(=E5=BA=9F=E7=89=A9=E5=93=88=E5=90=89?= =?UTF-8?q?=E7=B1=B3=E6=9B=BF=E6=8D=A2=E4=BA=86=E5=8D=81=E6=AC=A1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat_loop/cycle_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index a1862f515..cc938164d 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -369,7 +369,7 @@ class CycleProcessor: self.context.chat_instance.cycle_tracker.end_cycle(loop_info, cycle_timers) self.context.chat_instance.cycle_tracker.print_cycle_info(cycle_timers) - action_type = actions["action_type"] if actions else "no_action" + action_type = actions[0]["action_type"] if actions else "no_action" return action_type async def _handle_action( From 4e639249c3e00d5eecbaca3fc4cd01f31930f3bd Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 16:23:07 +0800 Subject: [PATCH 30/69] =?UTF-8?q?fix(chat):=20=E4=BF=AE=E5=A4=8D=E9=94=99?= =?UTF-8?q?=E5=88=AB=E5=AD=97=E4=BF=AE=E6=AD=A3=E5=8A=9F=E8=83=BD=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=20kaomoji=20=E6=81=A2=E5=A4=8D=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在处理类型为 "typo" 的句子时,`recover_kaomoji` 函数的调用方式有误。原先代码将 `s["correction"]` 包装在列表中 (`[s["correction"]]`) 进行传递,这与函数的预期输入不符,可能导致在修正错别字时无法正确恢复 kaomoji 表情。 本次提交移除了不必要的列表包装,确保将 `s["correction"]` 直接传递给 `recover_kaomoji` 函数,从而修正了该问题。 --- src/chat/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index dd60ef951..9f85d0ff1 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -371,7 +371,7 @@ def process_llm_response( if isinstance(s, dict) and s.get("type") == "typo": s["original"] = recover_kaomoji([s["original"]], kaomoji_mapping) s["typo"] = recover_kaomoji([s["typo"]], kaomoji_mapping) - s["correction"] = recover_kaomoji([s["correction"]], kaomoji_mapping) + s["correction"] = recover_kaomoji(s["correction"], kaomoji_mapping) recovered_sentences.append(s) elif isinstance(s, dict) and s.get("type") == "text": s["content"] = recover_kaomoji([s["content"]], kaomoji_mapping) From bf294075387b0167837e2cd02d1bca809b6ed03b Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 16:24:31 +0800 Subject: [PATCH 31/69] =?UTF-8?q?refactor(chat):=20=E7=AE=80=E5=8C=96=20ka?= =?UTF-8?q?omoji=20=E6=81=A2=E5=A4=8D=E5=87=BD=E6=95=B0=E7=9A=84=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E4=BC=A0=E9=80=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 `recover_kaomoji` 函数的调用从传递列表 `[s["..."]]` 改为直接传递字符串 `s["..."]`。此更改简化了代码,并与函数预期的输入类型保持一致,提高了代码的可读性和健壮性。 --- src/chat/utils/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 9f85d0ff1..b9942816f 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -369,8 +369,8 @@ def process_llm_response( recovered_sentences = [] for s in sentences: if isinstance(s, dict) and s.get("type") == "typo": - s["original"] = recover_kaomoji([s["original"]], kaomoji_mapping) - s["typo"] = recover_kaomoji([s["typo"]], kaomoji_mapping) + s["original"] = recover_kaomoji(s["original"], kaomoji_mapping) + s["typo"] = recover_kaomoji(s["typo"], kaomoji_mapping) s["correction"] = recover_kaomoji(s["correction"], kaomoji_mapping) recovered_sentences.append(s) elif isinstance(s, dict) and s.get("type") == "text": From 59985cb905ecb49ded56b0a42401f7f3cdc7cb0c Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 16:29:00 +0800 Subject: [PATCH 32/69] =?UTF-8?q?fix(chat):=20=E4=BF=AE=E5=A4=8D=20kaomoji?= =?UTF-8?q?=20=E6=81=A2=E5=A4=8D=E5=87=BD=E6=95=B0=E7=9A=84=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在之前的重构中,`recover_kaomoji` 函数的调用被简化,但在此处调用时忘记移除数组包裹,导致传递了错误的参数类型。此提交删除了多余的方括号,确保将字符串直接传递给 `recover_kaomoji` 函数。 --- src/chat/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index b9942816f..d0aa0b6c5 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -374,7 +374,7 @@ def process_llm_response( s["correction"] = recover_kaomoji(s["correction"], kaomoji_mapping) recovered_sentences.append(s) elif isinstance(s, dict) and s.get("type") == "text": - s["content"] = recover_kaomoji([s["content"]], kaomoji_mapping) + s["content"] = recover_kaomoji(s["content"], kaomoji_mapping) recovered_sentences.append(s) else: recovered_sentences.append(recover_kaomoji([s], kaomoji_mapping)) From 9ec0b6977bf170bb47f10016e4deda5e3481a0b0 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 16:40:46 +0800 Subject: [PATCH 33/69] =?UTF-8?q?fix(api):=20=E5=A4=84=E7=90=86=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E7=B1=BB=E5=9E=8B=E7=9A=84=20content=20=E8=BE=93?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 `process_human_text` 函数中,增加了对 `content` 参数的类型检查。如果 `content` 是一个列表,它会被自动转换成字符串,以防止后续处理中出现 `ValueError`。这增强了函数的健壮性,使其能够处理更多样化的输入格式。 --- src/plugin_system/apis/generator_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugin_system/apis/generator_api.py b/src/plugin_system/apis/generator_api.py index 7f3074a81..8d8d04eb3 100644 --- a/src/plugin_system/apis/generator_api.py +++ b/src/plugin_system/apis/generator_api.py @@ -239,6 +239,8 @@ def process_human_text( enable_splitter: 是否启用消息分割器 enable_chinese_typo: 是否启用错字生成器 """ + if isinstance(content, list): + content = "".join(map(str, content)) if not isinstance(content, str): raise ValueError("content 必须是字符串类型") try: From 268dbcca5e2f95773a0b0c7fab8eaaf3b1a9a21a Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 16:55:24 +0800 Subject: [PATCH 34/69] =?UTF-8?q?refactor(api):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=92=A4=E5=9B=9E=E6=B6=88=E6=81=AF=E7=9A=84=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 `adapter_command_to_stream` 的直接调用重构为使用更通用的 `command_to_stream` 函数。 此更改统一了命令分发逻辑,提高了代码的可维护性。 --- src/plugin_system/apis/send_api.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/plugin_system/apis/send_api.py b/src/plugin_system/apis/send_api.py index 3aea3cde4..565bc9f25 100644 --- a/src/plugin_system/apis/send_api.py +++ b/src/plugin_system/apis/send_api.py @@ -511,9 +511,11 @@ async def recall_message(message_id: str, stream_id: str) -> bool: Returns: bool: 是否成功 """ - response = await adapter_command_to_stream( - action="delete_msg", - params={"message_id": message_id}, - stream_id=stream_id, - ) - return response.get("status") == "ok" + command_data = {"name": "delete_msg", "args": message_id} + + success = await command_to_stream( + command=command_data, + stream_id=stream_id, + storage_message=True, + ) + return success \ No newline at end of file From a0ddd525b315087b1c6eb798a28502d97b1952f8 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 17:13:58 +0800 Subject: [PATCH 35/69] =?UTF-8?q?refactor(chat):=20=E9=87=8D=E6=9E=84plann?= =?UTF-8?q?er=E4=B8=BA=E5=A4=A7=E8=84=91/=E5=B0=8F=E8=84=91=E5=B9=B6?= =?UTF-8?q?=E8=A1=8C=E6=9E=B6=E6=9E=84=E4=BB=A5=E6=8F=90=E5=8D=87=E6=80=A7?= =?UTF-8?q?=E8=83=BD=E5=92=8C=E5=8F=AF=E6=89=A9=E5=B1=95=E6=80=A7(?= =?UTF-8?q?=E5=88=AB=E7=AE=A1=E8=83=BD=E4=B8=8D=E8=83=BD=E7=94=A8=E5=85=88?= =?UTF-8?q?=E6=8F=92=E8=BF=9B=E6=9D=A5=E5=86=8D=E8=AF=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原有的单体`plan`方法重构为一个多智能体架构,包含一个"大脑"和多个并行的"小脑"。 大脑 (`plan`方法) 专注于决定是否进行聊天回复 (`reply`),并负责调度和整合所有决策。 小脑 (`sub_plan`方法) 并行处理具体的、独立的action判断。每个小脑接收一部分action,使用轻量级模型进行快速评估,从而实现并行化处理,减少了单一LLM调用的延迟。 这种新架构的主要优势包括: - **性能提升**:通过并行化action判断,显著减少了规划器的总响应时间。 - **可扩展性**:添加新的action变得更加容易,因为它们可以被分配到不同的小脑中,而不会增加主规划流程的复杂性。 - **鲁棒性**:将复杂的规划任务分解为更小的、独立的单元,降低了单个点失败导致整个规划失败的风险。 - **成本效益**:允许为小脑配置更轻量、更快速的模型,优化了资源使用。 --- src/chat/planner_actions/planner.py | 486 ++++++++++++++++++++-------- 1 file changed, 358 insertions(+), 128 deletions(-) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 80f974b94..deee8fdef 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -1,7 +1,11 @@ import orjson import time import traceback -from typing import Dict, Any, Optional, Tuple, List +import asyncio +import math +import random +import json +from typing import Dict, Any, Optional, Tuple, List, TYPE_CHECKING from rich.traceback import install from datetime import datetime from json_repair import repair_json @@ -19,12 +23,15 @@ from src.chat.utils.chat_message_builder import ( from src.chat.utils.utils import get_chat_type_and_target_info from src.chat.planner_actions.action_manager import ActionManager from src.chat.message_receive.chat_stream import get_chat_manager -from src.plugin_system.base.component_types import ActionInfo, ChatMode, ComponentType +from src.plugin_system.base.component_types import ActionInfo, ChatMode, ComponentType, ActionActivationType from src.plugin_system.core.component_registry import component_registry from src.schedule.schedule_manager import schedule_manager from src.mood.mood_manager import mood_manager from src.chat.memory_system.Hippocampus import hippocampus_manager +if TYPE_CHECKING: + pass + logger = get_logger("planner") install(extra_lines=3) @@ -110,6 +117,37 @@ 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: def __init__(self, chat_id: str, action_manager: ActionManager): @@ -117,14 +155,17 @@ class ActionPlanner: self.log_prefix = f"[{get_chat_manager().get_stream_name(chat_id) or chat_id}]" self.action_manager = action_manager # LLM规划器配置 + # --- 大脑 --- self.planner_llm = LLMRequest( model_set=model_config.model_task_config.planner, request_type="planner" - ) # 用于动作规划 + ) + # --- 小脑 (新增) --- + # TODO: 可以在 model_config.toml 中为 planner_small 单独配置一个轻量级模型 + self.planner_small_llm = LLMRequest( + model_set=model_config.model_task_config.planner, request_type="planner_small" + ) self.last_obs_time_mark = 0.0 - # 添加重试计数器 - self.plan_retry_count = 0 - self.max_plan_retries = 3 async def _get_long_term_memory_context(self) -> str: """ @@ -237,6 +278,168 @@ class ActionPlanner: # 假设消息列表是按时间顺序排列的,最后一个是最新的 return message_id_list[-1].get("message") + def _parse_single_action( + self, + action_json: dict, + message_id_list: list, # 使用 planner.py 的 list of dict + current_available_actions: list, # 使用 planner.py 的 list of tuple + ) -> List[Dict[str, Any]]: + """ + [注释] 解析单个小脑LLM返回的action JSON,并将其转换为标准化的字典。 + """ + parsed_actions = [] + try: + action = action_json.get("action", "no_action") + reasoning = action_json.get("reason", "未提供原因") + 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 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: + logger.warning(f"{self.log_prefix}无法找到target_message_id '{target_message_id}'") + target_message = self.get_latest_message(message_id_list) + else: + 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: + logger.warning( + f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{action}' (可用: {available_action_names}),将强制使用 'no_action'" + ) + reasoning = f"LLM 返回了当前不可用的动作 '{action}' (可用: {available_action_names})。原始理由: {reasoning}" + action = "no_action" + + # 将列表转换为字典格式以供将来使用 + available_actions_dict = dict(current_available_actions) + parsed_actions.append( + { + "action_type": action, + "reasoning": reasoning, + "action_data": action_data, + "action_message": target_message, + "available_actions": available_actions_dict, + } + ) + except Exception as e: + logger.error(f"{self.log_prefix}解析单个action时出错: {e}") + parsed_actions.append( + { + "action_type": "no_action", + "reasoning": f"解析action时出错: {e}", + "action_data": {}, + "action_message": None, + "available_actions": dict(current_available_actions), + } + ) + return parsed_actions + + def _filter_no_actions(self, action_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """ + [注释] 从一个action字典列表中过滤掉所有的 'no_action'。 + 如果过滤后列表为空, 则返回一个空的列表, 或者根据需要返回一个默认的no_action字典。 + """ + non_no_actions = [a for a in action_list if a.get("action_type") not in ["no_action", "no_reply"]] + if non_no_actions: + return non_no_actions + # 如果都是 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, @@ -244,153 +447,180 @@ class ActionPlanner: available_actions: Optional[Dict[str, ActionInfo]] = None, ) -> Tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]: """ - 规划器 (Planner): 使用LLM根据上下文决定做出什么动作。 + [注释] "大脑"规划器。 + 1. 启动多个并行的"小脑"(sub_plan)来决定是否执行具体的actions。 + 2. 自己(大脑)则专注于决定是否进行聊天回复(reply)。 + 3. 整合大脑和小脑的决策,返回最终要执行的动作列表。 """ + # --- 1. 准备上下文信息 --- + message_list_before_now = get_raw_msg_before_timestamp_with_chat( + chat_id=self.chat_id, + 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", + read_mark=self.last_obs_time_mark, + 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, + ) + self.last_obs_time_mark = time.time() - action = "no_reply" # 默认动作 - reasoning = "规划器初始化默认" - action_data = {} - current_available_actions: Dict[str, ActionInfo] = {} - target_message: Optional[Dict[str, Any]] = None # 初始化target_message变量 - prompt: str = "" - message_id_list: list = [] + 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]] = [] try: - is_group_chat, chat_target_info, current_available_actions = self.get_necessary_info() + 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 - # --- 构建提示词 (调用修改后的 PromptBuilder 方法) --- - prompt, message_id_list = await self.build_planner_prompt( - is_group_chat=is_group_chat, # <-- Pass HFC state - chat_target_info=chat_target_info, # <-- 传递获取到的聊天目标信息 - current_available_actions=current_available_actions, # <-- Pass determined actions + if sub_planner_actions: + sub_planner_actions_num = len(sub_planner_actions) + # TODO: 您可以在 config.toml 的 [chat] 部分添加 planner_size = 5.0 来自定义此值 + planner_size_config = getattr(global_config.chat, "planner_size", 5.0) + 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( + is_group_chat=is_group_chat, + chat_target_info=chat_target_info, + current_available_actions={}, # 大脑不考虑具体action mode=mode, + chat_content_block_override=chat_content_block, + message_id_list_override=message_id_list, ) - - # --- 调用 LLM (普通文本生成) --- - llm_content = None - try: - llm_content, (reasoning_content, _, _) = await self.planner_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}") - if reasoning_content: - logger.info(f"{self.log_prefix}规划器推理: {reasoning_content}") - else: - logger.debug(f"{self.log_prefix}规划器原始提示词: {prompt}") - logger.debug(f"{self.log_prefix}规划器原始响应: {llm_content}") - if reasoning_content: - logger.debug(f"{self.log_prefix}规划器推理: {reasoning_content}") - - except Exception as req_e: - logger.error(f"{self.log_prefix}LLM 请求执行失败: {req_e}") - reasoning = f"LLM 请求失败,模型出现问题: {req_e}" - action = "no_reply" + llm_content, _ = await self.planner_llm.generate_response_async(prompt=prompt) if llm_content: - try: - parsed_json = orjson.loads(repair_json(llm_content)) - - if isinstance(parsed_json, list): - if parsed_json: - parsed_json = parsed_json[-1] - logger.warning(f"{self.log_prefix}LLM返回了多个JSON对象,使用最后一个: {parsed_json}") - else: - parsed_json = {} - - if not isinstance(parsed_json, dict): - logger.error(f"{self.log_prefix}解析后的JSON不是字典类型: {type(parsed_json)}") - parsed_json = {} - + 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 - for key, value in parsed_json.items(): - if key not in ["action", "reason"]: - action_data[key] = value - - # 非no_reply动作需要target_message_id + action_data = {k: v for k, v in parsed_json.items() if k not in ["action", "reason"]} if action != "no_reply": - if target_message_id := parsed_json.get("target_message_id"): - # 根据target_message_id查找原始消息 - target_message = self.find_message_by_id(target_message_id, message_id_list) - # 如果获取的target_message为None,输出warning并重新plan - if target_message is None: - self.plan_retry_count += 1 - logger.warning( - f"{self.log_prefix}无法找到target_message_id '{target_message_id}' 对应的消息,重试次数: {self.plan_retry_count}/{self.max_plan_retries}" - ) + 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}]") - # 如果连续三次plan均为None,输出error并选取最新消息 - if self.plan_retry_count >= self.max_plan_retries: - logger.error( - f"{self.log_prefix}连续{self.max_plan_retries}次plan获取target_message失败,选择最新消息作为target_message" - ) - target_message = self.get_latest_message(message_id_list) - self.plan_retry_count = 0 # 重置计数器 - else: - # 递归重新plan - return await self.plan(mode, loop_start_time, available_actions) - else: - # 成功获取到target_message,重置计数器 - self.plan_retry_count = 0 - else: - logger.warning(f"{self.log_prefix}动作'{action}'缺少target_message_id") + except Exception as e: + logger.error(f"{self.log_prefix}大脑处理过程中发生意外错误: {e}\n{traceback.format_exc()}") + action, reasoning = "no_reply", f"大脑处理错误: {e}" - if action != "no_reply" and action != "reply" and action not in current_available_actions: - logger.warning( - f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{action}' (可用: {list(current_available_actions.keys())}),将强制使用 'no_reply'" - ) - reasoning = f"LLM 返回了当前不可用的动作 '{action}' (可用: {list(current_available_actions.keys())})。原始理由: {reasoning}" - action = "no_reply" - - except Exception as json_e: - logger.warning(f"{self.log_prefix}解析LLM响应JSON失败 {json_e}. LLM原始输出: '{llm_content}'") - traceback.print_exc() - reasoning = f"解析LLM响应JSON失败: {json_e}. 将使用默认动作 'no_reply'." - action = "no_reply" - - except Exception as outer_e: - logger.error(f"{self.log_prefix}Planner 处理过程中发生意外错误,规划失败,将执行 no_reply: {outer_e}") - traceback.print_exc() - action = "no_reply" - reasoning = f"Planner 内部处理错误: {outer_e}" - - is_parallel = False - if mode == ChatMode.NORMAL and action in current_available_actions: - is_parallel = current_available_actions[action].parallel_action + # --- 4. 整合大脑和小脑的决策 --- + 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]] = [] - actions = [] + 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) - # 1. 添加Planner取得的动作 - actions.append( - { - "action_type": action, - "reasoning": reasoning, - "action_data": action_data, - "action_message": target_message, - "available_actions": available_actions, # 添加这个字段 - } - ) + final_actions = self._filter_no_actions(final_actions) - if action != "reply" and is_parallel: - actions.append( - {"action_type": "reply", "action_message": target_message, "available_actions": available_actions} - ) + if not final_actions: + final_actions = [ + { + "action_type": "no_action", + "reasoning": "所有规划器都选择不执行动作", + "action_data": {}, "action_message": None, "available_actions": available_actions + } + ] - return actions, target_message + 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) + + 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}]") + + return final_actions, final_target_message async def build_planner_prompt( self, - is_group_chat: bool, # Now passed as argument - chat_target_info: Optional[dict], # Now passed as argument + is_group_chat: bool, + chat_target_info: Optional[dict], current_available_actions: Dict[str, ActionInfo], - refresh_time: bool = False, mode: ChatMode = ChatMode.FOCUS, - ) -> tuple[str, list]: # sourcery skip: use-join + chat_content_block_override: Optional[str] = None, + message_id_list_override: Optional[List] = None, + refresh_time: bool = False, # 添加缺失的参数 + ) -> tuple[str, list]: """构建 Planner LLM 的提示词 (获取模板并填充数据)""" try: # --- 通用信息获取 --- From e5247eba966e34da4f688dd957638a4d41b13c66 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 17:14:32 +0800 Subject: [PATCH 36/69] =?UTF-8?q?feat(config):=20=E4=B8=BA=E5=B0=8F?= =?UTF-8?q?=E8=84=91(sub-planner)=E6=96=B0=E5=A2=9E=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=92=8C=E5=B0=BA=E5=AF=B8=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为“小脑”(sub-planner)引入了独立的模型配置`planner_small`,使其可以与主planner(大脑)使用不同的、更轻量的模型,以提升并行处理性能。 同时,新增了`planner_size`配置项,用于控制每个小脑处理的action数量,允许用户根据需求调整并行度和单个小脑的上下文窗口。 - 在`model_config.toml`中添加`planner_small`任务配置 - 在`bot_config.toml`中添加`planner_size`参数 - 更新代码以使用新的配置项,并移除了相关的硬编码和TODO注释 --- src/chat/planner_actions/planner.py | 6 ++---- src/config/api_ada_configs.py | 5 +++-- src/config/official_configs.py | 1 + template/bot_config_template.toml | 3 +++ template/model_config_template.toml | 7 ++++++- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index deee8fdef..e2efa11aa 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -160,9 +160,8 @@ class ActionPlanner: model_set=model_config.model_task_config.planner, request_type="planner" ) # --- 小脑 (新增) --- - # TODO: 可以在 model_config.toml 中为 planner_small 单独配置一个轻量级模型 self.planner_small_llm = LLMRequest( - model_set=model_config.model_task_config.planner, request_type="planner_small" + model_set=model_config.model_task_config.planner_small, request_type="planner_small" ) self.last_obs_time_mark = 0.0 @@ -496,8 +495,7 @@ class ActionPlanner: if sub_planner_actions: sub_planner_actions_num = len(sub_planner_actions) - # TODO: 您可以在 config.toml 的 [chat] 部分添加 planner_size = 5.0 来自定义此值 - planner_size_config = getattr(global_config.chat, "planner_size", 5.0) + 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 ) diff --git a/src/config/api_ada_configs.py b/src/config/api_ada_configs.py index b74f1b558..5e53eec4b 100644 --- a/src/config/api_ada_configs.py +++ b/src/config/api_ada_configs.py @@ -113,6 +113,7 @@ 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构建模型配置") @@ -147,9 +148,9 @@ class ModelTaskConfig(ValidatedConfigBase): class APIAdapterConfig(ValidatedConfigBase): """API Adapter配置类""" - models: List[ModelInfo] = Field(..., min_items=1, description="模型列表") + models: List[ModelInfo] = Field(..., min_length=1, description="模型列表") model_task_config: ModelTaskConfig = Field(..., description="模型任务配置") - api_providers: List[APIProvider] = Field(..., min_items=1, description="API提供商列表") + api_providers: List[APIProvider] = Field(..., min_length=1, description="API提供商列表") def __init__(self, **data): super().__init__(**data) diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 3989e246c..3a8b46d03 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -92,6 +92,7 @@ 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/template/bot_config_template.toml b/template/bot_config_template.toml index 82e1b107a..60281dbb7 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -173,6 +173,9 @@ 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 # 是否启用关系系统 relation_frequency = 1 # 关系频率,MoFox-Bot构建关系的频率 diff --git a/template/model_config_template.toml b/template/model_config_template.toml index 0c1783143..fab3ee509 100644 --- a/template/model_config_template.toml +++ b/template/model_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.3.0" +version = "1.3.1" # 配置文件版本号迭代规则同bot_config.toml @@ -142,6 +142,11 @@ 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"] temperature = 0.3 From 657b12015b12ad936cb51987197eafa1f3f13f14 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 17:18:36 +0800 Subject: [PATCH 37/69] =?UTF-8?q?=E5=BF=98=E6=9B=B4=E6=96=B0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6=E7=89=88=E6=9C=AC=E5=8F=B7=E4=BA=86?= =?UTF-8?q?()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/bot_config_template.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 60281dbb7..0ee513b6e 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.7.3" +version = "6.7.4" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 From 8c446e54903ebaa5851b778a2ea12bac80a3c38f Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 19:42:48 +0800 Subject: [PATCH 38/69] =?UTF-8?q?refactor(chat):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=B8=BB=E5=8A=A8=E6=80=9D=E8=80=83=E6=A8=A1=E5=9D=97=E4=BB=A5?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E5=9B=9E=E5=A4=8D=E8=B4=A8=E9=87=8F=E5=92=8C?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E6=B8=85=E6=99=B0=E5=BA=A6=EF=BC=88=E5=93=AA?= =?UTF-8?q?=E4=B8=AA=E5=A4=A7=E8=81=AA=E6=98=8E=E6=8A=8A=E6=88=91=E8=81=94?= =?UTF-8?q?=E7=BD=91=E6=90=9C=E7=B4=A2=E7=83=A6=E4=BA=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将主动思考流程拆分为两个主要阶段:规划和内容生成。 在规划阶段(`ActionPlanner`),模型现在会结合最新的聊天上下文来决定是否发起主动对话,并确定一个合适的主题。这使得决策更加贴近当前对话氛围。 在内容生成阶段(`ProactiveThinker`),系统会围绕规划好的主题,主动搜集相关实时信息(如日程、网络资讯),并结合角色设定、心情和聊天历史,构建一个更丰富、更具上下文情境的提示词,从而生成更自然、更有趣的主动回复。 主要变更: - `ActionPlanner` 在主动模式下增加对近期聊天记录的分析,决策更精准。 - `ProactiveThinker` 新增 `_generate_proactive_content_and_send` 方法,负责整合多源信息(日程、搜索、上下文)生成最终回复。 - 简化了 `ProactiveThinker` 的主逻辑,使其专注于执行 `proactive_reply` 动作,而非处理多种动作类型。 - 优化了相关提示词,使其更专注于生成高质量的主动对话内容。 --- .../chat_loop/proactive/proactive_thinker.py | 134 +++++++-- src/chat/planner_actions/planner.py | 41 ++- .../built_in/web_search_tool/_manifest.json | 25 ++ .../web_search_tool/engines/__init__.py | 3 + .../built_in/web_search_tool/engines/base.py | 31 +++ .../web_search_tool/engines/bing_engine.py | 263 ++++++++++++++++++ .../web_search_tool/engines/ddg_engine.py | 42 +++ .../web_search_tool/engines/exa_engine.py | 79 ++++++ .../web_search_tool/engines/tavily_engine.py | 90 ++++++ .../built_in/web_search_tool/plugin.py | 160 +++++++++++ .../web_search_tool/tools/__init__.py | 3 + .../web_search_tool/tools/url_parser.py | 242 ++++++++++++++++ .../web_search_tool/tools/web_search.py | 164 +++++++++++ .../web_search_tool/utils/__init__.py | 3 + .../web_search_tool/utils/api_key_manager.py | 84 ++++++ .../web_search_tool/utils/formatters.py | 57 ++++ .../web_search_tool/utils/url_utils.py | 39 +++ 17 files changed, 1432 insertions(+), 28 deletions(-) create mode 100644 src/plugins/built_in/web_search_tool/_manifest.json create mode 100644 src/plugins/built_in/web_search_tool/engines/__init__.py create mode 100644 src/plugins/built_in/web_search_tool/engines/base.py create mode 100644 src/plugins/built_in/web_search_tool/engines/bing_engine.py create mode 100644 src/plugins/built_in/web_search_tool/engines/ddg_engine.py create mode 100644 src/plugins/built_in/web_search_tool/engines/exa_engine.py create mode 100644 src/plugins/built_in/web_search_tool/engines/tavily_engine.py create mode 100644 src/plugins/built_in/web_search_tool/plugin.py create mode 100644 src/plugins/built_in/web_search_tool/tools/__init__.py create mode 100644 src/plugins/built_in/web_search_tool/tools/url_parser.py create mode 100644 src/plugins/built_in/web_search_tool/tools/web_search.py create mode 100644 src/plugins/built_in/web_search_tool/utils/__init__.py create mode 100644 src/plugins/built_in/web_search_tool/utils/api_key_manager.py create mode 100644 src/plugins/built_in/web_search_tool/utils/formatters.py create mode 100644 src/plugins/built_in/web_search_tool/utils/url_utils.py diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index 081941b18..2ee715a4c 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -1,12 +1,19 @@ import time import traceback -from typing import TYPE_CHECKING +import orjson +from typing import TYPE_CHECKING, Dict, Any from src.common.logger import get_logger from src.plugin_system.base.component_types import ChatMode from ..hfc_context import HfcContext from .events import ProactiveTriggerEvent from src.plugin_system.apis import generator_api +from src.schedule.schedule_manager import schedule_manager +from src.plugin_system import tool_api +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 if TYPE_CHECKING: from ..cycle_processor import CycleProcessor @@ -20,6 +27,7 @@ class ProactiveThinker: 当接收到 ProactiveTriggerEvent 时,它会根据事件内容进行一系列决策和操作, 例如调整情绪、调用规划器生成行动,并最终可能产生一个主动的回复。 """ + def __init__(self, context: HfcContext, cycle_processor: "CycleProcessor"): """ 初始化主动思考器。 @@ -75,9 +83,6 @@ class ProactiveThinker: return try: - # 动态导入情绪管理器,避免循环依赖 - from src.mood.mood_manager import mood_manager - # 获取当前聊天的情绪对象 mood_obj = mood_manager.get_mood_by_chat_id(self.context.stream_id) new_mood = None @@ -112,29 +117,17 @@ class ProactiveThinker: """ try: # 调用规划器的 PROACTIVE 模式,让其决定下一步的行动 - actions, target_message = await self.cycle_processor.action_planner.plan(mode=ChatMode.PROACTIVE) + actions, _ = await self.cycle_processor.action_planner.plan(mode=ChatMode.PROACTIVE) # 通常只关心规划出的第一个动作 action_result = actions[0] if actions else {} - # 检查规划出的动作是否是“什么都不做” - if action_result and action_result.get("action_type") != "do_nothing": - # 如果动作是“回复” - if action_result.get("action_type") == "reply": - # 调用生成器API来创建回复内容 - success, response_set, _ = await generator_api.generate_reply( - chat_stream=self.context.chat_stream, - reply_message=action_result["action_message"], - available_actions={}, # 主动回复不考虑工具使用 - enable_tool=False, - request_type="chat.replyer.proactive", # 标记请求类型 - from_plugin=False, - ) - # 如果成功生成回复,则发送出去 - if success and response_set: - await self.cycle_processor.response_handler.send_response( - response_set, time.time(), action_result["action_message"] - ) + action_type = action_result.get("action_type") + + 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} 主动思考决策: 保持沉默") @@ -142,3 +135,98 @@ class ProactiveThinker: 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]): + """ + 获取实时信息,构建最终的生成提示词,并生成和发送主动回复。 + + Args: + action_result (Dict[str, Any]): 规划器返回的动作结果。 + """ + 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", "未能提取有效资讯。") + 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}") + + # 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), + ) + chat_context_block, _ = build_readable_messages_with_id(messages=message_list) + + # 4. 构建最终的生成提示词 + bot_name = global_config.bot.nickname + identity_block = f"你的名字是{bot_name},你{global_config.personality.personality_core}:" + mood_block = f"你现在的心情是:{mood_manager.get_mood_by_chat_id(self.context.stream_id).mood_state}" + + final_prompt = f""" +# 主动对话生成 + +## 你的角色 +{identity_block} + +## 你的心情 +{mood_block} + +## 你今天的日程安排 +{schedule_block} + +## 关于你准备讨论的话题“{topic}”的最新信息 +{news_block} + +## 最近的聊天内容 +{chat_context_block} + +## 任务 +你之前决定要发起一个关于“{topic}”的对话。现在,请结合以上所有信息,自然地开启这个话题。 + +## 要求 +- 你的发言要听起来像是自发的,而不是在念报告。 +- 巧妙地将日程安排或最新信息融入到你的开场白中。 +- 风格要符合你的角色设定。 +- 直接输出你想要说的内容,不要包含其他额外信息。 +""" + + # 5. 调用生成器API并发送 + response_text = await generator_api.generate_response_custom( + chat_stream=self.context.chat_stream, + prompt=final_prompt, + request_type="chat.replyer.proactive", + ) + + if response_text: + # 将纯文本包装成 ResponseSet 格式 + response_set = [{"type": "text", "data": {"text": response_text}}] + await self.cycle_processor.response_handler.send_response( + response_set, time.time(), action_result.get("action_message") + ) + else: + logger.error(f"{self.context.log_prefix} 主动思考生成回复失败。") + + except Exception as e: + logger.error(f"{self.context.log_prefix} 生成主动回复内容时异常: {e}") + logger.error(traceback.format_exc()) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index e2efa11aa..c353df3ef 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -90,12 +90,31 @@ def init_prompt(): ## 长期记忆摘要 {long_term_memory_block} +## 最近的聊天内容 +{chat_content_block} + ## 任务 -基于以上所有信息,分析当前情况,决定是否需要主动做些什么。 -如果你认为不需要,就选择 'do_nothing'。 +基于以上所有信息(特别是最近的聊天内容),分析当前情况,决定是否适合主动开启一个**新的、但又与当前氛围相关**的话题。 ## 可用动作 -{action_options_text} +动作:proactive_reply +动作描述:在当前对话的基础上,主动发起一个新的对话,分享一个有趣的想法、见闻或者对未来的计划。 +- 当你觉得可以说些什么来活跃气氛,并且内容与当前聊天氛围不冲突时 +- 当你有一些新的想法或计划想要分享,并且可以自然地衔接当前话题时 +{{ + "action": "proactive_reply", + "reason": "决定主动发起对话的具体原因", + "topic": "你想要发起对话的主题或内容(需要简洁)" +}} + +动作:do_nothing +动作描述:保持沉默,不主动发起任何动作或对话。 +- 当你分析了所有信息后,觉得当前不是一个发起互动的好时机时 +- 当最近的聊天内容很连贯,你的插入会打断别人时 +{{ + "action": "do_nothing", + "reason":"决定保持沉默的具体原因" +}} 你必须从上面列出的可用action中选择一个。 请以严格的 JSON 格式输出,且仅包含 JSON 内容: @@ -643,7 +662,19 @@ class ActionPlanner: # --- 根据模式构建不同的Prompt --- if mode == ChatMode.PROACTIVE: long_term_memory_block = await self._get_long_term_memory_context() - action_options_text = await self._build_action_options(current_available_actions, mode) + + # 获取最近的聊天记录用于主动思考决策 + message_list_short = get_raw_msg_before_timestamp_with_chat( + chat_id=self.chat_id, + timestamp=time.time(), + limit=int(global_config.chat.max_context_size * 0.2), # 主动思考时只看少量最近消息 + ) + chat_content_block, _ = build_readable_messages_with_id( + messages=message_list_short, + timestamp_mode="normal", + truncate=False, + show_actions=False, + ) prompt_template = await global_prompt_manager.get_prompt_async("proactive_planner_prompt") prompt = prompt_template.format( @@ -652,7 +683,7 @@ class ActionPlanner: schedule_block=schedule_block, mood_block=mood_block, long_term_memory_block=long_term_memory_block, - action_options_text=action_options_text, + chat_content_block=chat_content_block or "最近没有聊天内容。", ) return prompt, [] diff --git a/src/plugins/built_in/web_search_tool/_manifest.json b/src/plugins/built_in/web_search_tool/_manifest.json new file mode 100644 index 000000000..549781c2a --- /dev/null +++ b/src/plugins/built_in/web_search_tool/_manifest.json @@ -0,0 +1,25 @@ +{ + "manifest_version": 1, + "name": "web_search_tool", + "version": "1.0.0", + "description": "一个用于在互联网上搜索信息的工具", + "author": { + "name": "MoFox-Studio", + "url": "https://github.com/MoFox-Studio" + }, + "license": "GPL-v3.0-or-later", + + "host_application": { + "min_version": "0.10.0" + }, + "keywords": ["web_search", "url_parser"], + "categories": ["web_search", "url_parser"], + + "default_locale": "zh-CN", + "locales_path": "_locales", + + "plugin_info": { + "is_built_in": false, + "plugin_type": "web_search" + } +} \ No newline at end of file diff --git a/src/plugins/built_in/web_search_tool/engines/__init__.py b/src/plugins/built_in/web_search_tool/engines/__init__.py new file mode 100644 index 000000000..2f1c3492c --- /dev/null +++ b/src/plugins/built_in/web_search_tool/engines/__init__.py @@ -0,0 +1,3 @@ +""" +Search engines package +""" diff --git a/src/plugins/built_in/web_search_tool/engines/base.py b/src/plugins/built_in/web_search_tool/engines/base.py new file mode 100644 index 000000000..f7641aa2f --- /dev/null +++ b/src/plugins/built_in/web_search_tool/engines/base.py @@ -0,0 +1,31 @@ +""" +Base search engine interface +""" +from abc import ABC, abstractmethod +from typing import Dict, List, Any + + +class BaseSearchEngine(ABC): + """ + 搜索引擎基类 + """ + + @abstractmethod + async def search(self, args: Dict[str, Any]) -> List[Dict[str, Any]]: + """ + 执行搜索 + + Args: + args: 搜索参数,包含 query、num_results、time_range 等 + + Returns: + 搜索结果列表,每个结果包含 title、url、snippet、provider 字段 + """ + pass + + @abstractmethod + def is_available(self) -> bool: + """ + 检查搜索引擎是否可用 + """ + pass diff --git a/src/plugins/built_in/web_search_tool/engines/bing_engine.py b/src/plugins/built_in/web_search_tool/engines/bing_engine.py new file mode 100644 index 000000000..ac90956e0 --- /dev/null +++ b/src/plugins/built_in/web_search_tool/engines/bing_engine.py @@ -0,0 +1,263 @@ +""" +Bing search engine implementation +""" +import asyncio +import functools +import random +import traceback +from typing import Dict, List, Any +import requests +from bs4 import BeautifulSoup + +from src.common.logger import get_logger +from .base import BaseSearchEngine + +logger = get_logger("bing_engine") + +ABSTRACT_MAX_LENGTH = 300 # abstract max length + +user_agents = [ + # Edge浏览器 + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", + # Chrome浏览器 + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + # Firefox浏览器 + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0", +] + +# 请求头信息 +HEADERS = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + "Cache-Control": "max-age=0", + "Connection": "keep-alive", + "Host": "www.bing.com", + "Referer": "https://www.bing.com/", + "Sec-Ch-Ua": '"Chromium";v="122", "Microsoft Edge";v="122", "Not-A.Brand";v="99"', + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": '"Windows"', + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-User": "?1", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", +} + +bing_search_url = "https://www.bing.com/search?q=" + + +class BingSearchEngine(BaseSearchEngine): + """ + Bing搜索引擎实现 + """ + + def __init__(self): + self.session = requests.Session() + self.session.headers = HEADERS + + def is_available(self) -> bool: + """检查Bing搜索引擎是否可用""" + return True # Bing是免费搜索引擎,总是可用 + + async def search(self, args: Dict[str, Any]) -> List[Dict[str, Any]]: + """执行Bing搜索""" + query = args["query"] + num_results = args.get("num_results", 3) + time_range = args.get("time_range", "any") + + try: + loop = asyncio.get_running_loop() + func = functools.partial(self._search_sync, query, num_results, time_range) + search_response = await loop.run_in_executor(None, func) + return search_response + except Exception as e: + logger.error(f"Bing 搜索失败: {e}") + return [] + + def _search_sync(self, keyword: str, num_results: int, time_range: str) -> List[Dict[str, Any]]: + """同步执行Bing搜索""" + if not keyword: + return [] + + list_result = [] + + # 构建搜索URL + search_url = bing_search_url + keyword + + # 如果指定了时间范围,添加时间过滤参数 + if time_range == "week": + search_url += "&qft=+filterui:date-range-7" + elif time_range == "month": + search_url += "&qft=+filterui:date-range-30" + + try: + data = self._parse_html(search_url) + if data: + list_result.extend(data) + logger.debug(f"Bing搜索 [{keyword}] 找到 {len(data)} 个结果") + + except Exception as e: + logger.error(f"Bing搜索解析失败: {e}") + return [] + + logger.debug(f"Bing搜索 [{keyword}] 完成,总共 {len(list_result)} 个结果") + return list_result[:num_results] if len(list_result) > num_results else list_result + + def _parse_html(self, url: str) -> List[Dict[str, Any]]: + """解析处理结果""" + try: + logger.debug(f"访问Bing搜索URL: {url}") + + # 设置必要的Cookie + cookies = { + "SRCHHPGUSR": "SRCHLANG=zh-Hans", # 设置默认搜索语言为中文 + "SRCHD": "AF=NOFORM", + "SRCHUID": "V=2&GUID=1A4D4F1C8844493F9A2E3DB0D1BC806C", + "_SS": "SID=0D89D9A3C95C60B62E7AC80CC85461B3", + "_EDGE_S": "ui=zh-cn", # 设置界面语言为中文 + "_EDGE_V": "1", + } + + # 为每次请求随机选择不同的用户代理,降低被屏蔽风险 + headers = HEADERS.copy() + headers["User-Agent"] = random.choice(user_agents) + + # 创建新的session + session = requests.Session() + session.headers.update(headers) + session.cookies.update(cookies) + + # 发送请求 + try: + res = session.get(url=url, timeout=(3.05, 6), verify=True, allow_redirects=True) + except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: + logger.warning(f"第一次请求超时,正在重试: {str(e)}") + try: + res = session.get(url=url, timeout=(5, 10), verify=False) + except Exception as e2: + logger.error(f"第二次请求也失败: {str(e2)}") + return [] + + res.encoding = "utf-8" + + # 检查响应状态 + if res.status_code == 403: + logger.error("被禁止访问 (403 Forbidden),可能是IP被限制") + return [] + + if res.status_code != 200: + logger.error(f"必应搜索请求失败,状态码: {res.status_code}") + return [] + + # 检查是否被重定向到登录页面或验证页面 + if "login.live.com" in res.url or "login.microsoftonline.com" in res.url: + logger.error("被重定向到登录页面,可能需要登录") + return [] + + if "https://www.bing.com/ck/a" in res.url: + logger.error("被重定向到验证页面,可能被识别为机器人") + return [] + + # 解析HTML + try: + root = BeautifulSoup(res.text, "lxml") + except Exception: + try: + root = BeautifulSoup(res.text, "html.parser") + except Exception as e: + logger.error(f"HTML解析失败: {str(e)}") + return [] + + list_data = [] + + # 尝试提取搜索结果 + # 方法1: 查找标准的搜索结果容器 + results = root.select("ol#b_results li.b_algo") + + if results: + for _rank, result in enumerate(results, 1): + # 提取标题和链接 + title_link = result.select_one("h2 a") + if not title_link: + continue + + title = title_link.get_text().strip() + url = title_link.get("href", "") + + # 提取摘要 + abstract = "" + abstract_elem = result.select_one("div.b_caption p") + if abstract_elem: + abstract = abstract_elem.get_text().strip() + + # 限制摘要长度 + if ABSTRACT_MAX_LENGTH and len(abstract) > ABSTRACT_MAX_LENGTH: + abstract = abstract[:ABSTRACT_MAX_LENGTH] + "..." + + list_data.append({ + "title": title, + "url": url, + "snippet": abstract, + "provider": "Bing" + }) + + if len(list_data) >= 10: # 限制结果数量 + break + + # 方法2: 如果标准方法没找到结果,使用备用方法 + if not list_data: + # 查找所有可能的搜索结果链接 + all_links = root.find_all("a") + + for link in all_links: + href = link.get("href", "") + text = link.get_text().strip() + + # 过滤有效的搜索结果链接 + if (href and text and len(text) > 10 + and not href.startswith("javascript:") + and not href.startswith("#") + and "http" in href + and not any(x in href for x in [ + "bing.com/search", "bing.com/images", "bing.com/videos", + "bing.com/maps", "bing.com/news", "login", "account", + "microsoft", "javascript" + ])): + + # 尝试获取摘要 + abstract = "" + parent = link.parent + if parent and parent.get_text(): + full_text = parent.get_text().strip() + if len(full_text) > len(text): + abstract = full_text.replace(text, "", 1).strip() + + # 限制摘要长度 + if ABSTRACT_MAX_LENGTH and len(abstract) > ABSTRACT_MAX_LENGTH: + abstract = abstract[:ABSTRACT_MAX_LENGTH] + "..." + + list_data.append({ + "title": text, + "url": href, + "snippet": abstract, + "provider": "Bing" + }) + + if len(list_data) >= 10: + break + + logger.debug(f"从Bing解析到 {len(list_data)} 个搜索结果") + return list_data + + except Exception as e: + logger.error(f"解析Bing页面时出错: {str(e)}") + logger.debug(traceback.format_exc()) + return [] diff --git a/src/plugins/built_in/web_search_tool/engines/ddg_engine.py b/src/plugins/built_in/web_search_tool/engines/ddg_engine.py new file mode 100644 index 000000000..011935e27 --- /dev/null +++ b/src/plugins/built_in/web_search_tool/engines/ddg_engine.py @@ -0,0 +1,42 @@ +""" +DuckDuckGo search engine implementation +""" +from typing import Dict, List, Any +from asyncddgs import aDDGS + +from src.common.logger import get_logger +from .base import BaseSearchEngine + +logger = get_logger("ddg_engine") + + +class DDGSearchEngine(BaseSearchEngine): + """ + DuckDuckGo搜索引擎实现 + """ + + def is_available(self) -> bool: + """检查DuckDuckGo搜索引擎是否可用""" + return True # DuckDuckGo不需要API密钥,总是可用 + + async def search(self, args: Dict[str, Any]) -> List[Dict[str, Any]]: + """执行DuckDuckGo搜索""" + query = args["query"] + num_results = args.get("num_results", 3) + + try: + async with aDDGS() as ddgs: + search_response = await ddgs.text(query, max_results=num_results) + + return [ + { + "title": r.get("title"), + "url": r.get("href"), + "snippet": r.get("body"), + "provider": "DuckDuckGo" + } + for r in search_response + ] + except Exception as e: + logger.error(f"DuckDuckGo 搜索失败: {e}") + return [] diff --git a/src/plugins/built_in/web_search_tool/engines/exa_engine.py b/src/plugins/built_in/web_search_tool/engines/exa_engine.py new file mode 100644 index 000000000..2bb515e8e --- /dev/null +++ b/src/plugins/built_in/web_search_tool/engines/exa_engine.py @@ -0,0 +1,79 @@ +""" +Exa search engine implementation +""" +import asyncio +import functools +from datetime import datetime, timedelta +from typing import Dict, List, Any +from exa_py import Exa + +from src.common.logger import get_logger +from src.plugin_system.apis import config_api +from .base import BaseSearchEngine +from ..utils.api_key_manager import create_api_key_manager_from_config + +logger = get_logger("exa_engine") + + +class ExaSearchEngine(BaseSearchEngine): + """ + Exa搜索引擎实现 + """ + + def __init__(self): + self._initialize_clients() + + def _initialize_clients(self): + """初始化Exa客户端""" + # 从主配置文件读取API密钥 + exa_api_keys = config_api.get_global_config("exa.api_keys", None) + + # 创建API密钥管理器 + self.api_manager = create_api_key_manager_from_config( + exa_api_keys, + lambda key: Exa(api_key=key), + "Exa" + ) + + def is_available(self) -> bool: + """检查Exa搜索引擎是否可用""" + return self.api_manager.is_available() + + async def search(self, args: Dict[str, Any]) -> List[Dict[str, Any]]: + """执行Exa搜索""" + if not self.is_available(): + return [] + + query = args["query"] + num_results = args.get("num_results", 3) + time_range = args.get("time_range", "any") + + exa_args = {"num_results": num_results, "text": True, "highlights": True} + if time_range != "any": + today = datetime.now() + start_date = today - timedelta(days=7 if time_range == "week" else 30) + exa_args["start_published_date"] = start_date.strftime('%Y-%m-%d') + + try: + # 使用API密钥管理器获取下一个客户端 + exa_client = self.api_manager.get_next_client() + if not exa_client: + logger.error("无法获取Exa客户端") + return [] + + loop = asyncio.get_running_loop() + func = functools.partial(exa_client.search_and_contents, query, **exa_args) + search_response = await loop.run_in_executor(None, func) + + return [ + { + "title": res.title, + "url": res.url, + "snippet": " ".join(getattr(res, 'highlights', [])) or (getattr(res, 'text', '')[:250] + '...'), + "provider": "Exa" + } + for res in search_response.results + ] + except Exception as e: + logger.error(f"Exa 搜索失败: {e}") + return [] diff --git a/src/plugins/built_in/web_search_tool/engines/tavily_engine.py b/src/plugins/built_in/web_search_tool/engines/tavily_engine.py new file mode 100644 index 000000000..affb303fc --- /dev/null +++ b/src/plugins/built_in/web_search_tool/engines/tavily_engine.py @@ -0,0 +1,90 @@ +""" +Tavily search engine implementation +""" +import asyncio +import functools +from typing import Dict, List, Any +from tavily import TavilyClient + +from src.common.logger import get_logger +from src.plugin_system.apis import config_api +from .base import BaseSearchEngine +from ..utils.api_key_manager import create_api_key_manager_from_config + +logger = get_logger("tavily_engine") + + +class TavilySearchEngine(BaseSearchEngine): + """ + Tavily搜索引擎实现 + """ + + def __init__(self): + self._initialize_clients() + + def _initialize_clients(self): + """初始化Tavily客户端""" + # 从主配置文件读取API密钥 + tavily_api_keys = config_api.get_global_config("tavily.api_keys", None) + + # 创建API密钥管理器 + self.api_manager = create_api_key_manager_from_config( + tavily_api_keys, + lambda key: TavilyClient(api_key=key), + "Tavily" + ) + + def is_available(self) -> bool: + """检查Tavily搜索引擎是否可用""" + return self.api_manager.is_available() + + async def search(self, args: Dict[str, Any]) -> List[Dict[str, Any]]: + """执行Tavily搜索""" + if not self.is_available(): + return [] + + query = args["query"] + num_results = args.get("num_results", 3) + time_range = args.get("time_range", "any") + + try: + # 使用API密钥管理器获取下一个客户端 + tavily_client = self.api_manager.get_next_client() + if not tavily_client: + logger.error("无法获取Tavily客户端") + return [] + + # 构建Tavily搜索参数 + search_params = { + "query": query, + "max_results": num_results, + "search_depth": "basic", + "include_answer": False, + "include_raw_content": False + } + + # 根据时间范围调整搜索参数 + if time_range == "week": + search_params["days"] = 7 + elif time_range == "month": + search_params["days"] = 30 + + loop = asyncio.get_running_loop() + func = functools.partial(tavily_client.search, **search_params) + search_response = await loop.run_in_executor(None, func) + + results = [] + if search_response and "results" in search_response: + for res in search_response["results"]: + results.append({ + "title": res.get("title", "无标题"), + "url": res.get("url", ""), + "snippet": res.get("content", "")[:300] + "..." if res.get("content") else "无摘要", + "provider": "Tavily" + }) + + return results + + except Exception as e: + logger.error(f"Tavily 搜索失败: {e}") + return [] diff --git a/src/plugins/built_in/web_search_tool/plugin.py b/src/plugins/built_in/web_search_tool/plugin.py new file mode 100644 index 000000000..1789062ae --- /dev/null +++ b/src/plugins/built_in/web_search_tool/plugin.py @@ -0,0 +1,160 @@ +""" +Web Search Tool Plugin + +一个功能强大的网络搜索和URL解析插件,支持多种搜索引擎和解析策略。 +""" +from typing import List, Tuple, Type + +from src.plugin_system import ( + BasePlugin, + register_plugin, + ComponentInfo, + ConfigField, + PythonDependency +) +from src.plugin_system.apis import config_api +from src.common.logger import get_logger + +from .tools.web_search import WebSurfingTool +from .tools.url_parser import URLParserTool + +logger = get_logger("web_search_plugin") + + +@register_plugin +class WEBSEARCHPLUGIN(BasePlugin): + """ + 网络搜索工具插件 + + 提供网络搜索和URL解析功能,支持多种搜索引擎: + - Exa (需要API密钥) + - Tavily (需要API密钥) + - DuckDuckGo (免费) + - Bing (免费) + """ + + # 插件基本信息 + plugin_name: str = "web_search_tool" # 内部标识符 + enable_plugin: bool = True + dependencies: List[str] = [] # 插件依赖列表 + + def __init__(self, *args, **kwargs): + """初始化插件,立即加载所有搜索引擎""" + super().__init__(*args, **kwargs) + + # 立即初始化所有搜索引擎,触发API密钥管理器的日志输出 + logger.info("🚀 正在初始化所有搜索引擎...") + try: + from .engines.exa_engine import ExaSearchEngine + from .engines.tavily_engine import TavilySearchEngine + from .engines.ddg_engine import DDGSearchEngine + from .engines.bing_engine import BingSearchEngine + + # 实例化所有搜索引擎,这会触发API密钥管理器的初始化 + exa_engine = ExaSearchEngine() + tavily_engine = TavilySearchEngine() + ddg_engine = DDGSearchEngine() + bing_engine = BingSearchEngine() + + # 报告每个引擎的状态 + engines_status = { + "Exa": exa_engine.is_available(), + "Tavily": tavily_engine.is_available(), + "DuckDuckGo": ddg_engine.is_available(), + "Bing": bing_engine.is_available() + } + + available_engines = [name for name, available in engines_status.items() if available] + unavailable_engines = [name for name, available in engines_status.items() if not available] + + if available_engines: + logger.info(f"✅ 可用搜索引擎: {', '.join(available_engines)}") + if unavailable_engines: + logger.info(f"❌ 不可用搜索引擎: {', '.join(unavailable_engines)}") + + except Exception as e: + logger.error(f"❌ 搜索引擎初始化失败: {e}", exc_info=True) + + # Python包依赖列表 + python_dependencies: List[PythonDependency] = [ + PythonDependency( + package_name="asyncddgs", + description="异步DuckDuckGo搜索库", + optional=False + ), + PythonDependency( + package_name="exa_py", + description="Exa搜索API客户端库", + optional=True # 如果没有API密钥,这个是可选的 + ), + PythonDependency( + package_name="tavily", + install_name="tavily-python", # 安装时使用这个名称 + description="Tavily搜索API客户端库", + optional=True # 如果没有API密钥,这个是可选的 + ), + PythonDependency( + package_name="httpx", + version=">=0.20.0", + install_name="httpx[socks]", # 安装时使用这个名称(包含可选依赖) + description="支持SOCKS代理的HTTP客户端库", + optional=False + ) + ] + config_file_name: str = "config.toml" # 配置文件名 + + # 配置节描述 + config_section_descriptions = { + "plugin": "插件基本信息", + "proxy": "链接本地解析代理配置" + } + + # 配置Schema定义 + # 注意:EXA配置和组件设置已迁移到主配置文件(bot_config.toml)的[exa]和[web_search]部分 + config_schema: dict = { + "plugin": { + "name": ConfigField(type=str, default="WEB_SEARCH_PLUGIN", description="插件名称"), + "version": ConfigField(type=str, default="1.0.0", description="插件版本"), + "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), + }, + "proxy": { + "http_proxy": ConfigField( + type=str, + default=None, + description="HTTP代理地址,格式如: http://proxy.example.com:8080" + ), + "https_proxy": ConfigField( + type=str, + default=None, + description="HTTPS代理地址,格式如: http://proxy.example.com:8080" + ), + "socks5_proxy": ConfigField( + type=str, + default=None, + description="SOCKS5代理地址,格式如: socks5://proxy.example.com:1080" + ), + "enable_proxy": ConfigField( + type=bool, + default=False, + description="是否启用代理" + ) + }, + } + + def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: + """ + 获取插件组件列表 + + Returns: + 组件信息和类型的元组列表 + """ + enable_tool = [] + + # 从主配置文件读取组件启用配置 + if config_api.get_global_config("web_search.enable_web_search_tool", True): + enable_tool.append((WebSurfingTool.get_tool_info(), WebSurfingTool)) + + if config_api.get_global_config("web_search.enable_url_tool", True): + enable_tool.append((URLParserTool.get_tool_info(), URLParserTool)) + + return enable_tool diff --git a/src/plugins/built_in/web_search_tool/tools/__init__.py b/src/plugins/built_in/web_search_tool/tools/__init__.py new file mode 100644 index 000000000..480099acd --- /dev/null +++ b/src/plugins/built_in/web_search_tool/tools/__init__.py @@ -0,0 +1,3 @@ +""" +Tools package +""" diff --git a/src/plugins/built_in/web_search_tool/tools/url_parser.py b/src/plugins/built_in/web_search_tool/tools/url_parser.py new file mode 100644 index 000000000..315e06271 --- /dev/null +++ b/src/plugins/built_in/web_search_tool/tools/url_parser.py @@ -0,0 +1,242 @@ +""" +URL parser tool implementation +""" +import asyncio +import functools +from typing import Any, Dict +from exa_py import Exa +import httpx +from bs4 import BeautifulSoup + +from src.common.logger import get_logger +from src.plugin_system import BaseTool, ToolParamType, llm_api +from src.plugin_system.apis import config_api +from src.common.cache_manager import tool_cache + +from ..utils.formatters import format_url_parse_results +from ..utils.url_utils import parse_urls_from_input, validate_urls +from ..utils.api_key_manager import create_api_key_manager_from_config + +logger = get_logger("url_parser_tool") + + +class URLParserTool(BaseTool): + """ + 一个用于解析和总结一个或多个网页URL内容的工具。 + """ + name: str = "parse_url" + description: str = "当需要理解一个或多个特定网页链接的内容时,使用此工具。例如:'这些网页讲了什么?[https://example.com, https://example2.com]' 或 '帮我总结一下这些文章'" + available_for_llm: bool = True + parameters = [ + ("urls", ToolParamType.STRING, "要理解的网站", True, None), + ] + + def __init__(self, plugin_config=None): + super().__init__(plugin_config) + self._initialize_exa_clients() + + def _initialize_exa_clients(self): + """初始化Exa客户端""" + # 优先从主配置文件读取,如果没有则从插件配置文件读取 + exa_api_keys = config_api.get_global_config("exa.api_keys", None) + if exa_api_keys is None: + # 从插件配置文件读取 + exa_api_keys = self.get_config("exa.api_keys", []) + + # 创建API密钥管理器 + self.api_manager = create_api_key_manager_from_config( + exa_api_keys, + lambda key: Exa(api_key=key), + "Exa URL Parser" + ) + + async def _local_parse_and_summarize(self, url: str) -> Dict[str, Any]: + """ + 使用本地库(httpx, BeautifulSoup)解析URL,并调用LLM进行总结。 + """ + try: + # 读取代理配置 + enable_proxy = self.get_config("proxy.enable_proxy", False) + proxies = None + + if enable_proxy: + socks5_proxy = self.get_config("proxy.socks5_proxy", None) + http_proxy = self.get_config("proxy.http_proxy", None) + https_proxy = self.get_config("proxy.https_proxy", None) + + # 优先使用SOCKS5代理(全协议代理) + if socks5_proxy: + proxies = socks5_proxy + logger.info(f"使用SOCKS5代理: {socks5_proxy}") + elif http_proxy or https_proxy: + proxies = {} + if http_proxy: + proxies["http://"] = http_proxy + if https_proxy: + proxies["https://"] = https_proxy + logger.info(f"使用HTTP/HTTPS代理配置: {proxies}") + + client_kwargs = {"timeout": 15.0, "follow_redirects": True} + if proxies: + client_kwargs["proxies"] = proxies + + async with httpx.AsyncClient(**client_kwargs) as client: + response = await client.get(url) + response.raise_for_status() + + soup = BeautifulSoup(response.text, "html.parser") + + title = soup.title.string if soup.title else "无标题" + for script in soup(["script", "style"]): + script.extract() + text = soup.get_text(separator="\n", strip=True) + + if not text: + return {"error": "无法从页面提取有效文本内容。"} + + summary_prompt = f"请根据以下网页内容,生成一段不超过300字的中文摘要,保留核心信息和关键点:\n\n---\n\n标题: {title}\n\n内容:\n{text[:4000]}\n\n---\n\n摘要:" + + text_model = str(self.get_config("models.text_model", "replyer_1")) + models = llm_api.get_available_models() + model_config = models.get(text_model) + if not model_config: + logger.error("未配置LLM模型") + return {"error": "未配置LLM模型"} + + success, summary, reasoning, model_name = await llm_api.generate_with_model( + prompt=summary_prompt, + model_config=model_config, + request_type="story.generate", + temperature=0.3, + max_tokens=1000 + ) + + if not success: + logger.info(f"生成摘要失败: {summary}") + return {"error": "发生ai错误"} + + logger.info(f"成功生成摘要内容:'{summary}'") + + return { + "title": title, + "url": url, + "snippet": summary, + "source": "local" + } + + except httpx.HTTPStatusError as e: + logger.warning(f"本地解析URL '{url}' 失败 (HTTP {e.response.status_code})") + return {"error": f"请求失败,状态码: {e.response.status_code}"} + except Exception as e: + logger.error(f"本地解析或总结URL '{url}' 时发生未知异常: {e}", exc_info=True) + return {"error": f"发生未知错误: {str(e)}"} + + async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: + """ + 执行URL内容提取和总结。优先使用Exa,失败后尝试本地解析。 + """ + # 获取当前文件路径用于缓存键 + import os + current_file_path = os.path.abspath(__file__) + + # 检查缓存 + cached_result = await tool_cache.get(self.name, function_args, current_file_path) + if cached_result: + logger.info(f"缓存命中: {self.name} -> {function_args}") + return cached_result + + urls_input = function_args.get("urls") + if not urls_input: + return {"error": "URL列表不能为空。"} + + # 处理URL输入,确保是列表格式 + urls = parse_urls_from_input(urls_input) + if not urls: + return {"error": "提供的字符串中未找到有效的URL。"} + + # 验证URL格式 + valid_urls = validate_urls(urls) + if not valid_urls: + return {"error": "未找到有效的URL。"} + + urls = valid_urls + logger.info(f"准备解析 {len(urls)} 个URL: {urls}") + + successful_results = [] + error_messages = [] + urls_to_retry_locally = [] + + # 步骤 1: 尝试使用 Exa API 进行解析 + contents_response = None + if self.api_manager.is_available(): + logger.info(f"开始使用 Exa API 解析URL: {urls}") + try: + # 使用API密钥管理器获取下一个客户端 + exa_client = self.api_manager.get_next_client() + if not exa_client: + logger.error("无法获取Exa客户端") + else: + loop = asyncio.get_running_loop() + exa_params = {"text": True, "summary": True, "highlights": True} + func = functools.partial(exa_client.get_contents, urls, **exa_params) + contents_response = await loop.run_in_executor(None, func) + except Exception as e: + logger.error(f"执行 Exa URL解析时发生严重异常: {e}", exc_info=True) + contents_response = None # 确保异常后为None + + # 步骤 2: 处理Exa的响应 + if contents_response and hasattr(contents_response, 'statuses'): + results_map = {res.url: res for res in contents_response.results} if hasattr(contents_response, 'results') else {} + if contents_response.statuses: + for status in contents_response.statuses: + if status.status == 'success': + res = results_map.get(status.id) + if res: + summary = getattr(res, 'summary', '') + highlights = " ".join(getattr(res, 'highlights', [])) + text_snippet = (getattr(res, 'text', '')[:300] + '...') if getattr(res, 'text', '') else '' + snippet = summary or highlights or text_snippet or '无摘要' + + successful_results.append({ + "title": getattr(res, 'title', '无标题'), + "url": getattr(res, 'url', status.id), + "snippet": snippet, + "source": "exa" + }) + else: + error_tag = getattr(status, 'error', '未知错误') + logger.warning(f"Exa解析URL '{status.id}' 失败: {error_tag}。准备本地重试。") + urls_to_retry_locally.append(status.id) + else: + # 如果Exa未配置、API调用失败或返回无效响应,则所有URL都进入本地重试 + urls_to_retry_locally.extend(url for url in urls if url not in [res['url'] for res in successful_results]) + + # 步骤 3: 对失败的URL进行本地解析 + if urls_to_retry_locally: + logger.info(f"开始本地解析以下URL: {urls_to_retry_locally}") + local_tasks = [self._local_parse_and_summarize(url) for url in urls_to_retry_locally] + local_results = await asyncio.gather(*local_tasks) + + for i, res in enumerate(local_results): + url = urls_to_retry_locally[i] + if "error" in res: + error_messages.append(f"URL: {url} - 解析失败: {res['error']}") + else: + successful_results.append(res) + + if not successful_results: + return {"error": "无法从所有给定的URL获取内容。", "details": error_messages} + + formatted_content = format_url_parse_results(successful_results) + + result = { + "type": "url_parse_result", + "content": formatted_content, + "errors": error_messages + } + + # 保存到缓存 + if "error" not in result: + await tool_cache.set(self.name, function_args, current_file_path, result) + + return result diff --git a/src/plugins/built_in/web_search_tool/tools/web_search.py b/src/plugins/built_in/web_search_tool/tools/web_search.py new file mode 100644 index 000000000..c09ad5e92 --- /dev/null +++ b/src/plugins/built_in/web_search_tool/tools/web_search.py @@ -0,0 +1,164 @@ +""" +Web search tool implementation +""" +import asyncio +from typing import Any, Dict, List + +from src.common.logger import get_logger +from src.plugin_system import BaseTool, ToolParamType +from src.plugin_system.apis import config_api +from src.common.cache_manager import tool_cache + +from ..engines.exa_engine import ExaSearchEngine +from ..engines.tavily_engine import TavilySearchEngine +from ..engines.ddg_engine import DDGSearchEngine +from ..engines.bing_engine import BingSearchEngine +from ..utils.formatters import format_search_results, deduplicate_results + +logger = get_logger("web_search_tool") + + +class WebSurfingTool(BaseTool): + """ + 网络搜索工具 + """ + name: str = "web_search" + description: str = "用于执行网络搜索。当用户明确要求搜索,或者需要获取关于公司、产品、事件的最新信息、新闻或动态时,必须使用此工具" + available_for_llm: bool = True + parameters = [ + ("query", ToolParamType.STRING, "要搜索的关键词或问题。", True, None), + ("num_results", ToolParamType.INTEGER, "期望每个搜索引擎返回的搜索结果数量,默认为5。", False, None), + ("time_range", ToolParamType.STRING, "指定搜索的时间范围,可以是 'any', 'week', 'month'。默认为 'any'。", False, ["any", "week", "month"]) + ] # type: ignore + + def __init__(self, plugin_config=None): + super().__init__(plugin_config) + # 初始化搜索引擎 + self.engines = { + "exa": ExaSearchEngine(), + "tavily": TavilySearchEngine(), + "ddg": DDGSearchEngine(), + "bing": BingSearchEngine() + } + + async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: + query = function_args.get("query") + if not query: + return {"error": "搜索查询不能为空。"} + + # 获取当前文件路径用于缓存键 + import os + current_file_path = os.path.abspath(__file__) + + # 检查缓存 + cached_result = await tool_cache.get(self.name, function_args, current_file_path, semantic_query=query) + if cached_result: + logger.info(f"缓存命中: {self.name} -> {function_args}") + return cached_result + + # 读取搜索配置 + enabled_engines = config_api.get_global_config("web_search.enabled_engines", ["ddg"]) + search_strategy = config_api.get_global_config("web_search.search_strategy", "single") + + logger.info(f"开始搜索,策略: {search_strategy}, 启用引擎: {enabled_engines}, 参数: '{function_args}'") + + # 根据策略执行搜索 + if search_strategy == "parallel": + result = await self._execute_parallel_search(function_args, enabled_engines) + elif search_strategy == "fallback": + result = await self._execute_fallback_search(function_args, enabled_engines) + else: # single + result = await self._execute_single_search(function_args, enabled_engines) + + # 保存到缓存 + if "error" not in result: + await tool_cache.set(self.name, function_args, current_file_path, result, semantic_query=query) + + return result + + async def _execute_parallel_search(self, function_args: Dict[str, Any], enabled_engines: List[str]) -> Dict[str, Any]: + """并行搜索策略:同时使用所有启用的搜索引擎""" + search_tasks = [] + + for engine_name in enabled_engines: + engine = self.engines.get(engine_name) + if engine and engine.is_available(): + custom_args = function_args.copy() + custom_args["num_results"] = custom_args.get("num_results", 5) + search_tasks.append(engine.search(custom_args)) + + if not search_tasks: + return {"error": "没有可用的搜索引擎。"} + + try: + search_results_lists = await asyncio.gather(*search_tasks, return_exceptions=True) + + all_results = [] + for result in search_results_lists: + if isinstance(result, list): + all_results.extend(result) + elif isinstance(result, Exception): + logger.error(f"搜索时发生错误: {result}") + + # 去重并格式化 + unique_results = deduplicate_results(all_results) + formatted_content = format_search_results(unique_results) + + return { + "type": "web_search_result", + "content": formatted_content, + } + + except Exception as e: + logger.error(f"执行并行网络搜索时发生异常: {e}", exc_info=True) + return {"error": f"执行网络搜索时发生严重错误: {str(e)}"} + + async def _execute_fallback_search(self, function_args: Dict[str, Any], enabled_engines: List[str]) -> Dict[str, Any]: + """回退搜索策略:按顺序尝试搜索引擎,失败则尝试下一个""" + for engine_name in enabled_engines: + engine = self.engines.get(engine_name) + if not engine or not engine.is_available(): + continue + + try: + custom_args = function_args.copy() + custom_args["num_results"] = custom_args.get("num_results", 5) + + results = await engine.search(custom_args) + + if results: # 如果有结果,直接返回 + formatted_content = format_search_results(results) + return { + "type": "web_search_result", + "content": formatted_content, + } + + except Exception as e: + logger.warning(f"{engine_name} 搜索失败,尝试下一个引擎: {e}") + continue + + return {"error": "所有搜索引擎都失败了。"} + + async def _execute_single_search(self, function_args: Dict[str, Any], enabled_engines: List[str]) -> Dict[str, Any]: + """单一搜索策略:只使用第一个可用的搜索引擎""" + for engine_name in enabled_engines: + engine = self.engines.get(engine_name) + if not engine or not engine.is_available(): + continue + + try: + custom_args = function_args.copy() + custom_args["num_results"] = custom_args.get("num_results", 5) + + results = await engine.search(custom_args) + formatted_content = format_search_results(results) + return { + "type": "web_search_result", + "content": formatted_content, + } + + except Exception as e: + logger.error(f"{engine_name} 搜索失败: {e}") + return {"error": f"{engine_name} 搜索失败: {str(e)}"} + + return {"error": "没有可用的搜索引擎。"} diff --git a/src/plugins/built_in/web_search_tool/utils/__init__.py b/src/plugins/built_in/web_search_tool/utils/__init__.py new file mode 100644 index 000000000..8ebe2c35d --- /dev/null +++ b/src/plugins/built_in/web_search_tool/utils/__init__.py @@ -0,0 +1,3 @@ +""" +Web search tool utilities package +""" diff --git a/src/plugins/built_in/web_search_tool/utils/api_key_manager.py b/src/plugins/built_in/web_search_tool/utils/api_key_manager.py new file mode 100644 index 000000000..f8e0afa71 --- /dev/null +++ b/src/plugins/built_in/web_search_tool/utils/api_key_manager.py @@ -0,0 +1,84 @@ +""" +API密钥管理器,提供轮询机制 +""" +import itertools +from typing import List, Optional, TypeVar, Generic, Callable +from src.common.logger import get_logger + +logger = get_logger("api_key_manager") + +T = TypeVar('T') + + +class APIKeyManager(Generic[T]): + """ + API密钥管理器,支持轮询机制 + """ + + def __init__(self, api_keys: List[str], client_factory: Callable[[str], T], service_name: str = "Unknown"): + """ + 初始化API密钥管理器 + + Args: + api_keys: API密钥列表 + client_factory: 客户端工厂函数,接受API密钥参数并返回客户端实例 + service_name: 服务名称,用于日志记录 + """ + self.service_name = service_name + self.clients: List[T] = [] + self.client_cycle: Optional[itertools.cycle] = None + + if api_keys: + # 过滤有效的API密钥,排除None、空字符串、"None"字符串等 + valid_keys = [] + for key in api_keys: + if isinstance(key, str) and key.strip() and key.strip().lower() not in ("none", "null", ""): + valid_keys.append(key.strip()) + + if valid_keys: + try: + self.clients = [client_factory(key) for key in valid_keys] + self.client_cycle = itertools.cycle(self.clients) + logger.info(f"🔑 {service_name} 成功加载 {len(valid_keys)} 个 API 密钥") + except Exception as e: + logger.error(f"❌ 初始化 {service_name} 客户端失败: {e}") + self.clients = [] + self.client_cycle = None + else: + logger.warning(f"⚠️ {service_name} API Keys 配置无效(包含None或空值),{service_name} 功能将不可用") + else: + logger.warning(f"⚠️ {service_name} API Keys 未配置,{service_name} 功能将不可用") + + def is_available(self) -> bool: + """检查是否有可用的客户端""" + return bool(self.clients and self.client_cycle) + + def get_next_client(self) -> Optional[T]: + """获取下一个客户端(轮询)""" + if not self.is_available(): + return None + return next(self.client_cycle) + + def get_client_count(self) -> int: + """获取可用客户端数量""" + return len(self.clients) + + +def create_api_key_manager_from_config( + config_keys: Optional[List[str]], + client_factory: Callable[[str], T], + service_name: str +) -> APIKeyManager[T]: + """ + 从配置创建API密钥管理器的便捷函数 + + Args: + config_keys: 从配置读取的API密钥列表 + client_factory: 客户端工厂函数 + service_name: 服务名称 + + Returns: + API密钥管理器实例 + """ + api_keys = config_keys if isinstance(config_keys, list) else [] + return APIKeyManager(api_keys, client_factory, service_name) diff --git a/src/plugins/built_in/web_search_tool/utils/formatters.py b/src/plugins/built_in/web_search_tool/utils/formatters.py new file mode 100644 index 000000000..434f6f3c8 --- /dev/null +++ b/src/plugins/built_in/web_search_tool/utils/formatters.py @@ -0,0 +1,57 @@ +""" +Formatters for web search results +""" +from typing import List, Dict, Any + + +def format_search_results(results: List[Dict[str, Any]]) -> str: + """ + 格式化搜索结果为字符串 + """ + if not results: + return "没有找到相关的网络信息。" + + formatted_string = "根据网络搜索结果:\n\n" + for i, res in enumerate(results, 1): + title = res.get("title", '无标题') + url = res.get("url", '#') + snippet = res.get("snippet", '无摘要') + provider = res.get("provider", "未知来源") + + formatted_string += f"{i}. **{title}** (来自: {provider})\n" + formatted_string += f" - 摘要: {snippet}\n" + formatted_string += f" - 来源: {url}\n\n" + + return formatted_string + + +def format_url_parse_results(results: List[Dict[str, Any]]) -> str: + """ + 将成功解析的URL结果列表格式化为一段简洁的文本。 + """ + formatted_parts = [] + for res in results: + title = res.get('title', '无标题') + url = res.get('url', '#') + snippet = res.get('snippet', '无摘要') + source = res.get('source', '未知') + + formatted_string = f"**{title}**\n" + formatted_string += f"**内容摘要**:\n{snippet}\n" + formatted_string += f"**来源**: {url} (由 {source} 解析)\n" + formatted_parts.append(formatted_string) + + return "\n---\n".join(formatted_parts) + + +def deduplicate_results(results: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """ + 根据URL去重搜索结果 + """ + unique_urls = set() + unique_results = [] + for res in results: + if isinstance(res, dict) and res.get("url") and res["url"] not in unique_urls: + unique_urls.add(res["url"]) + unique_results.append(res) + return unique_results diff --git a/src/plugins/built_in/web_search_tool/utils/url_utils.py b/src/plugins/built_in/web_search_tool/utils/url_utils.py new file mode 100644 index 000000000..74afbc819 --- /dev/null +++ b/src/plugins/built_in/web_search_tool/utils/url_utils.py @@ -0,0 +1,39 @@ +""" +URL processing utilities +""" +import re +from typing import List + + +def parse_urls_from_input(urls_input) -> List[str]: + """ + 从输入中解析URL列表 + """ + if isinstance(urls_input, str): + # 如果是字符串,尝试解析为URL列表 + # 提取所有HTTP/HTTPS URL + url_pattern = r'https?://[^\s\],]+' + urls = re.findall(url_pattern, urls_input) + if not urls: + # 如果没有找到标准URL,将整个字符串作为单个URL + if urls_input.strip().startswith(('http://', 'https://')): + urls = [urls_input.strip()] + else: + return [] + elif isinstance(urls_input, list): + urls = [url.strip() for url in urls_input if isinstance(url, str) and url.strip()] + else: + return [] + + return urls + + +def validate_urls(urls: List[str]) -> List[str]: + """ + 验证URL格式,返回有效的URL列表 + """ + valid_urls = [] + for url in urls: + if url.startswith(('http://', 'https://')): + valid_urls.append(url) + return valid_urls From ceee3c1fbff075e093f1dd53c7b25df38dea0f78 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 20:09:17 +0800 Subject: [PATCH 39/69] =?UTF-8?q?refactor(chat):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=94=99=E5=88=AB=E5=AD=97=E7=94=9F=E6=88=90=E5=99=A8=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=B8=8E=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对中文错别字生成器(`ChineseTypoGenerator`)进行了大规模重构和改进,以提升代码的可读性、可维护性和生成质量。 主要变更包括: - **逻辑拆分**: 将核心的单字替换逻辑从主函数 `create_typo_sentence` 中提取到新的私有方法 `_char_replace`,使主流程更清晰。 - **文档增强**: 全面重写和丰富了所有主要方法的文档字符串(docstrings),详细解释了每个参数的用途、函数的内部工作原理和设计决策,显著提高了代码的可理解性。 - **代码简化**: 优化了同音词的查找逻辑(`_get_word_homophones`),移除了复杂的评分和文件读取过程,直接利用 `jieba.dt.FREQ` 进行有效性验证,使代码更简洁高效。 - **健壮性提升**: 在拼音转换和处理逻辑中增加了更具体的异常捕获(`IndexError`, `TypeError`),提高了代码的稳定性。 - **修正建议格式**: 将修正建议的返回格式从单个字符串更改为 `(错字/词, 正确字/词)` 的元组,提供了更完整的上下文信息。 此外,在 `generator_api.py` 中移除了一段冗余的内容类型检查代码。 --- src/chat/utils/typo_generator.py | 221 +++++++++++++----------- src/plugin_system/apis/generator_api.py | 2 - 2 files changed, 121 insertions(+), 102 deletions(-) diff --git a/src/chat/utils/typo_generator.py b/src/chat/utils/typo_generator.py index c23c4c319..1f2f9c346 100644 --- a/src/chat/utils/typo_generator.py +++ b/src/chat/utils/typo_generator.py @@ -2,12 +2,14 @@ 错别字生成器 - 基于拼音和字频的中文错别字生成工具 """ -import orjson +import itertools import math import os import random import time + import jieba +import orjson from collections import defaultdict from pathlib import Path @@ -30,11 +32,16 @@ class ChineseTypoGenerator: 初始化错别字生成器。 Args: - error_rate (float): 单个汉字被替换为同音字的概率。 - min_freq (int): 候选替换字的最小词频阈值,低于此阈值的字将被忽略。 - tone_error_rate (float): 在选择同音字时,使用错误声调的概率。 - word_replace_rate (float): 整个词语被替换为同音词的概率。 - max_freq_diff (int): 允许的原始字与替换字之间的最大频率差异。 + error_rate (float): 控制单个汉字被替换为错别字的基础概率。 + 这个概率会根据词语长度进行调整,词语越长,单个字出错的概率越低。 + min_freq (int): 候选替换字的最小词频阈值。一个汉字的频率低于此值时, + 它不会被选为替换候选字,以避免生成过于生僻的错别字。 + tone_error_rate (float): 在寻找同音字时,有多大的概率会故意使用一个错误的声调来寻找候选字。 + 这可以模拟常见的声调错误,如 "shí" -> "shì"。 + word_replace_rate (float): 控制一个多字词语被整体替换为同音词的概率。 + 例如,“天气” -> “天气”。 + max_freq_diff (int): 允许的原始字与替换字之间的最大归一化频率差异。 + 用于确保替换字与原字的常用程度相似,避免用非常常见的字替换罕见字,反之亦然。 """ self.error_rate = error_rate self.min_freq = min_freq @@ -51,11 +58,12 @@ class ChineseTypoGenerator: def _load_or_create_char_frequency(self): """ 加载或创建汉字频率字典。 - 如果存在缓存文件 `depends-data/char_frequency.json`,则直接加载。 - 否则,通过解析 `jieba` 的词典文件来创建,并保存为缓存。 + 如果存在缓存文件 `depends-data/char_frequency.json`,则直接加载以提高启动速度。 + 否则,通过解析 `jieba` 的内置词典文件 `dict.txt` 来创建。 + 创建过程中,会统计每个汉字的累计频率,并进行归一化处理,然后保存为缓存文件。 Returns: - dict: 一个将汉字映射到其归一化频率的字典。 + dict: 一个将汉字映射到其归一化频率(0-1000范围)的字典。 """ cache_file = Path("depends-data/char_frequency.json") @@ -92,10 +100,13 @@ class ChineseTypoGenerator: def _create_pinyin_dict(): """ 创建从拼音到汉字的映射字典。 - 遍历常用汉字范围,为每个汉字生成带声调的拼音,并构建映射。 + 该方法会遍历 Unicode 中定义的常用汉字范围 (U+4E00 至 U+9FFF), + 为每个汉字生成带数字声调的拼音(例如 'hao3'),并构建一个从拼音到包含该拼音的所有汉字列表的映射。 + 这个字典是生成同音字和同音词的基础。 Returns: - defaultdict: 一个将拼音映射到汉字列表的字典。 + defaultdict: 一个将拼音字符串映射到汉字字符列表的字典。 + 例如: {'hao3': ['好', '郝', ...]} """ # 定义常用汉字的Unicode范围 chars = [chr(i) for i in range(0x4E00, 0x9FFF)] @@ -104,11 +115,10 @@ class ChineseTypoGenerator: # 为范围内的每个汉字建立拼音到汉字的映射 for char in chars: try: - # 获取带数字声调的拼音 (e.g., 'hao3') py = pinyin(char, style=Style.TONE3) - pinyin_dict[py].append(char) - except Exception: - # 忽略无法转换拼音的字符 + if py: + pinyin_dict[py].append(char) + except (IndexError, TypeError): continue return pinyin_dict @@ -144,23 +154,28 @@ class ChineseTypoGenerator: characters = list(sentence) result = [] for char in characters: - # 忽略所有非中文字符 if self._is_chinese_char(char): - # 获取带数字声调的拼音 - py = pinyin(char, style=Style.TONE3) - result.append((char, py)) + try: + py = pinyin(char, style=Style.TONE3) + if py: + result.append((char, py)) + except (IndexError, TypeError): + continue return result @staticmethod def _get_similar_tone_pinyin(py): """ 为一个给定的拼音生成一个声调错误的相似拼音。 + 例如,输入 'hao3',可能返回 'hao1'、'hao2' 或 'hao4'。 + 此函数用于模拟中文输入时常见的声调错误。 + 对于轻声(拼音末尾无数字声调),会随机分配一个声调。 Args: py (str): 带数字声调的原始拼音 (e.g., 'hao3')。 Returns: - str: 一个声调被随机改变的拼音。 + str: 一个声调被随机改变的新拼音。 """ # 检查拼音是否有效 if not py or len(py) < 1: @@ -186,11 +201,15 @@ class ChineseTypoGenerator: def _calculate_replacement_probability(self, orig_freq, target_freq): """ 根据原始字和目标替换字的频率差异,计算替换概率。 - 频率相近的字有更高的替换概率。 + 这个概率模型遵循以下原则: + 1. 如果目标字比原始字更常用,替换概率为 1.0(倾向于换成更常见的字)。 + 2. 如果频率差异超过 `max_freq_diff` 阈值,替换概率为 0.0。 + 3. 否则,使用指数衰减函数计算概率,频率差异越大,替换概率越低。 + 这使得替换更倾向于选择频率相近的字。 Args: - orig_freq (float): 原始字的频率。 - target_freq (float): 目标替换字的频率。 + orig_freq (float): 原始字的归一化频率。 + target_freq (float): 目标替换字的归一化频率。 Returns: float: 替换概率,介于 0.0 和 1.0 之间。 @@ -210,14 +229,19 @@ class ChineseTypoGenerator: def _get_similar_frequency_chars(self, char, py, num_candidates=5): """ 获取与给定汉字发音相似且频率相近的候选替换字。 + 此方法首先根据 `tone_error_rate` 决定是否寻找声调错误的同音字, + 然后合并声调正确的同音字。接着,根据字频进行过滤和排序: + 1. 移除原始字本身和频率低于 `min_freq` 的字。 + 2. 计算每个候选字的替换概率。 + 3. 按替换概率降序排序,并返回前 `num_candidates` 个候选字。 Args: char (str): 原始汉字。 py (str): 原始汉字的拼音。 - num_candidates (int): 返回的候选字数量。 + num_candidates (int): 返回的候选字数量上限。 Returns: - list or None: 一个包含候选替换字的列表,如果没有找到则返回 None。 + list or None: 一个包含候选替换字的列表,如果没有找到合适的候选字则返回 None。 """ homophones = [] @@ -254,7 +278,6 @@ class ChineseTypoGenerator: if not candidates_with_prob: return None - # 根据替换概率从高到低排序 candidates_with_prob.sort(key=lambda x: x, reverse=True) # 返回概率最高的几个候选字 @@ -269,9 +292,9 @@ class ChineseTypoGenerator: word (str): 输入的词语。 Returns: - list: 包含每个汉字拼音的列表。 + List[str]: 包含每个汉字拼音的列表。 """ - return [py for py in pinyin(word, style=Style.TONE3)] + return ["".join(p) for p in pinyin(word, style=Style.TONE3)] @staticmethod def _segment_sentence(sentence): @@ -288,14 +311,17 @@ class ChineseTypoGenerator: def _get_word_homophones(self, word): """ - 获取一个词语的同音词。 - 只返回在jieba词典中存在且频率较高的有意义词语。 + 获取一个词语的所有同音词。 + 该方法首先获取词语中每个字的拼音,然后为每个字找到所有同音字。 + 接着,使用 `itertools.product` 生成所有可能的同音字组合。 + 最后,过滤掉原始词本身,并只保留在 `jieba` 词典中存在的、有意义的词语。 + 这可以有效避免生成无意义的同音词组合。 Args: word (str): 原始词语。 Returns: - list: 一个包含同音词的列表。 + List[str]: 一个包含所有有效同音词的列表。 """ if len(word) <= 1: return [] @@ -310,45 +336,28 @@ class ChineseTypoGenerator: return [] # 如果某个字没有同音字,则无法构成同音词 candidates.append(chars) - # 生成所有可能的同音字组合 - import itertools - all_combinations = itertools.product(*candidates) + homophones = [ + "".join(combo) + for combo in all_combinations + if ("".join(combo) != word and "".join(combo) in jieba.dt.FREQ) + ] - # 加载jieba词典以验证组合出的词是否为有效词语 - dict_path = os.path.join(os.path.dirname(jieba.__file__), "dict.txt") - valid_words = {} - with open(dict_path, "r", encoding="utf-8") as f: - for line in f: - parts = line.strip().split() - if len(parts) >= 2: - valid_words[parts] = float(parts[0][1]) - - original_word_freq = valid_words.get(word, 0) - # 设置一个最小词频阈值,过滤掉非常生僻的词 - min_word_freq = original_word_freq * 0.1 - - homophones = [] - for combo in all_combinations: - new_word = "".join(combo) - # 检查新词是否为有效词语且与原词不同 - if new_word != word and new_word in valid_words: - new_word_freq = valid_words[new_word] - if new_word_freq >= min_word_freq: - # 计算综合评分,结合词频和平均字频 - char_avg_freq = sum(self.char_frequency.get(c, 0) for c in new_word) / len(new_word) - combined_score = new_word_freq * 0.7 + char_avg_freq * 0.3 - if combined_score >= self.min_freq: - homophones.append((new_word, combined_score)) - - # 按综合分数排序并返回前5个结果 - sorted_homophones = sorted(homophones, key=lambda x: x, reverse=True) - return [w for w, _ in sorted_homophones[:5]] + return homophones def create_typo_sentence(self, sentence): """ 为输入句子生成一个包含错别字的版本。 - 该方法会先对句子进行分词,然后根据概率进行整词替换或单字替换。 + 这是核心的错别字生成方法,其流程如下: + 1. 使用 jieba 对输入句子进行分词。 + 2. 遍历每个词语: + a. 如果词语长度大于1,根据 `word_replace_rate` 概率尝试进行整词替换。 + 如果找到了合适的同音词,则替换并跳过后续步骤。 + b. 如果不进行整词替换,则遍历词语中的每个汉字。 + c. 对每个汉字,调用 `_char_replace` 方法,根据 `error_rate` 和词语长度调整后的概率, + 决定是否进行单字替换。 + 3. 将处理后的词语拼接成最终的错别字句子。 + 4. 从所有发生的替换中,随机选择一个作为修正建议返回。 Args: sentence (str): 原始中文句子。 @@ -357,7 +366,7 @@ class ChineseTypoGenerator: tuple: 包含三个元素的元组: - original_sentence (str): 原始句子。 - typo_sentence (str): 包含错别字的句子。 - - correction_suggestion (str or None): 一个随机的修正建议(可能是正确的字或词),或 None。 + - correction_suggestion (Optional[tuple(str, str)]): 一个随机的修正建议,格式为 (错字/词, 正确字/词),或 None。 """ result = [] typo_info = [] # 用于调试,记录详细的替换信息 @@ -397,44 +406,56 @@ class ChineseTypoGenerator: word_typos.append((typo_word, word)) continue - # 步骤2: 如果不进行整词替换,则对词中的每个字进行单字替换 - new_word = [] - for char, py in zip(word, word_pinyin, strict=False): - # 词语越长,其中单个字被替换的概率越低 - char_error_rate = self.error_rate * (0.7 ** (len(word) - 1)) - if random.random() < char_error_rate: - similar_chars = self._get_similar_frequency_chars(char, py) - if similar_chars: - typo_char = random.choice(similar_chars) - orig_freq = self.char_frequency.get(char, 0) - typo_freq = self.char_frequency.get(typo_char, 0) - # 根据频率计算最终是否替换 - if random.random() < self._calculate_replacement_probability(orig_freq, typo_freq): - new_word.append(typo_char) - typo_py = pinyin(typo_char, style=Style.TONE3) - typo_info.append((char, typo_char, py, typo_py, orig_freq, typo_freq)) - char_typos.append((typo_char, char)) - continue - # 如果不替换,则保留原字 - new_word.append(char) - - result.append("".join(new_word)) - - # 步骤3: 生成修正建议 - correction_suggestion = None - # 有50%的概率提供一个修正建议 - if random.random() < 0.5: - # 优先从整词错误中选择 - if word_typos: - _, correct_word = random.choice(word_typos) - correction_suggestion = correct_word - # 其次从单字错误中选择 - elif char_typos: - _, correct_char = random.choice(char_typos) - correction_suggestion = correct_char + new_word = "".join( + self._char_replace(char, py, len(word), typo_info, char_typos) for char, py in zip(word, word_pinyin) + ) + result.append(new_word) + all_typos = word_typos + char_typos + correction_suggestion = random.choice(all_typos) if all_typos and random.random() < 0.5 else None return sentence, "".join(result), correction_suggestion + def _char_replace(self, char, py, word_len, typo_info, char_typos): + """ + 根据概率替换单个汉字。 + 这个内部方法处理单个汉字的替换逻辑。 + + Args: + char (str): 要处理的原始汉字。 + py (str): 原始汉字的拼音。 + word_len (int): 原始汉字所在的词语的长度。 + typo_info (list): 用于记录详细调试信息的列表。 + char_typos (list): 用于记录 (错字, 正确字) 的列表。 + + Returns: + str: 替换后的汉字(可能是原汉字,也可能是错别字)。 + """ + # 根据词语长度调整错误率:词语越长,单个字出错的概率越低。 + # 这是一个启发式规则,模拟人们在输入长词时更不容易打错单个字。 + char_error_rate = self.error_rate * (0.7 ** (word_len - 1)) + if random.random() >= char_error_rate: + return char + + # 获取发音和频率都相似的候选错别字 + similar_chars = self._get_similar_frequency_chars(char, py) + if not similar_chars: + return char + + # 从候选列表中随机选择一个 + typo_char = random.choice(similar_chars) + orig_freq = self.char_frequency.get(char, 0) + typo_freq = self.char_frequency.get(typo_char, 0) + + # 根据频率差异再次进行概率判断,决定是否执行替换 + if random.random() >= self._calculate_replacement_probability(orig_freq, typo_freq): + return char + + # 执行替换,并记录相关信息 + typo_py = pinyin(typo_char, style=Style.TONE3) + typo_info.append((char, typo_char, py, typo_py, orig_freq, typo_freq)) + char_typos.append((typo_char, char)) + return typo_char + @staticmethod def format_typo_info(typo_info): """ diff --git a/src/plugin_system/apis/generator_api.py b/src/plugin_system/apis/generator_api.py index 8d8d04eb3..7f3074a81 100644 --- a/src/plugin_system/apis/generator_api.py +++ b/src/plugin_system/apis/generator_api.py @@ -239,8 +239,6 @@ def process_human_text( enable_splitter: 是否启用消息分割器 enable_chinese_typo: 是否启用错字生成器 """ - if isinstance(content, list): - content = "".join(map(str, content)) if not isinstance(content, str): raise ValueError("content 必须是字符串类型") try: From e848f89c59c9b285661cffd5fb1b775861691409 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 20:16:49 +0800 Subject: [PATCH 40/69] =?UTF-8?q?Revert=20"refactor(chat):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=94=99=E5=88=AB=E5=AD=97=E7=94=9F=E6=88=90=E5=99=A8?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E4=B8=8E=E6=96=87=E6=A1=A3"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit ceee3c1fbff075e093f1dd53c7b25df38dea0f78. --- src/chat/utils/typo_generator.py | 223 +++++++++++------------- src/plugin_system/apis/generator_api.py | 2 + 2 files changed, 103 insertions(+), 122 deletions(-) diff --git a/src/chat/utils/typo_generator.py b/src/chat/utils/typo_generator.py index 1f2f9c346..c23c4c319 100644 --- a/src/chat/utils/typo_generator.py +++ b/src/chat/utils/typo_generator.py @@ -2,14 +2,12 @@ 错别字生成器 - 基于拼音和字频的中文错别字生成工具 """ -import itertools +import orjson import math import os import random import time - import jieba -import orjson from collections import defaultdict from pathlib import Path @@ -32,16 +30,11 @@ class ChineseTypoGenerator: 初始化错别字生成器。 Args: - error_rate (float): 控制单个汉字被替换为错别字的基础概率。 - 这个概率会根据词语长度进行调整,词语越长,单个字出错的概率越低。 - min_freq (int): 候选替换字的最小词频阈值。一个汉字的频率低于此值时, - 它不会被选为替换候选字,以避免生成过于生僻的错别字。 - tone_error_rate (float): 在寻找同音字时,有多大的概率会故意使用一个错误的声调来寻找候选字。 - 这可以模拟常见的声调错误,如 "shí" -> "shì"。 - word_replace_rate (float): 控制一个多字词语被整体替换为同音词的概率。 - 例如,“天气” -> “天气”。 - max_freq_diff (int): 允许的原始字与替换字之间的最大归一化频率差异。 - 用于确保替换字与原字的常用程度相似,避免用非常常见的字替换罕见字,反之亦然。 + error_rate (float): 单个汉字被替换为同音字的概率。 + min_freq (int): 候选替换字的最小词频阈值,低于此阈值的字将被忽略。 + tone_error_rate (float): 在选择同音字时,使用错误声调的概率。 + word_replace_rate (float): 整个词语被替换为同音词的概率。 + max_freq_diff (int): 允许的原始字与替换字之间的最大频率差异。 """ self.error_rate = error_rate self.min_freq = min_freq @@ -58,12 +51,11 @@ class ChineseTypoGenerator: def _load_or_create_char_frequency(self): """ 加载或创建汉字频率字典。 - 如果存在缓存文件 `depends-data/char_frequency.json`,则直接加载以提高启动速度。 - 否则,通过解析 `jieba` 的内置词典文件 `dict.txt` 来创建。 - 创建过程中,会统计每个汉字的累计频率,并进行归一化处理,然后保存为缓存文件。 + 如果存在缓存文件 `depends-data/char_frequency.json`,则直接加载。 + 否则,通过解析 `jieba` 的词典文件来创建,并保存为缓存。 Returns: - dict: 一个将汉字映射到其归一化频率(0-1000范围)的字典。 + dict: 一个将汉字映射到其归一化频率的字典。 """ cache_file = Path("depends-data/char_frequency.json") @@ -100,13 +92,10 @@ class ChineseTypoGenerator: def _create_pinyin_dict(): """ 创建从拼音到汉字的映射字典。 - 该方法会遍历 Unicode 中定义的常用汉字范围 (U+4E00 至 U+9FFF), - 为每个汉字生成带数字声调的拼音(例如 'hao3'),并构建一个从拼音到包含该拼音的所有汉字列表的映射。 - 这个字典是生成同音字和同音词的基础。 + 遍历常用汉字范围,为每个汉字生成带声调的拼音,并构建映射。 Returns: - defaultdict: 一个将拼音字符串映射到汉字字符列表的字典。 - 例如: {'hao3': ['好', '郝', ...]} + defaultdict: 一个将拼音映射到汉字列表的字典。 """ # 定义常用汉字的Unicode范围 chars = [chr(i) for i in range(0x4E00, 0x9FFF)] @@ -115,10 +104,11 @@ class ChineseTypoGenerator: # 为范围内的每个汉字建立拼音到汉字的映射 for char in chars: try: + # 获取带数字声调的拼音 (e.g., 'hao3') py = pinyin(char, style=Style.TONE3) - if py: - pinyin_dict[py].append(char) - except (IndexError, TypeError): + pinyin_dict[py].append(char) + except Exception: + # 忽略无法转换拼音的字符 continue return pinyin_dict @@ -154,28 +144,23 @@ class ChineseTypoGenerator: characters = list(sentence) result = [] for char in characters: + # 忽略所有非中文字符 if self._is_chinese_char(char): - try: - py = pinyin(char, style=Style.TONE3) - if py: - result.append((char, py)) - except (IndexError, TypeError): - continue + # 获取带数字声调的拼音 + py = pinyin(char, style=Style.TONE3) + result.append((char, py)) return result @staticmethod def _get_similar_tone_pinyin(py): """ 为一个给定的拼音生成一个声调错误的相似拼音。 - 例如,输入 'hao3',可能返回 'hao1'、'hao2' 或 'hao4'。 - 此函数用于模拟中文输入时常见的声调错误。 - 对于轻声(拼音末尾无数字声调),会随机分配一个声调。 Args: py (str): 带数字声调的原始拼音 (e.g., 'hao3')。 Returns: - str: 一个声调被随机改变的新拼音。 + str: 一个声调被随机改变的拼音。 """ # 检查拼音是否有效 if not py or len(py) < 1: @@ -201,15 +186,11 @@ class ChineseTypoGenerator: def _calculate_replacement_probability(self, orig_freq, target_freq): """ 根据原始字和目标替换字的频率差异,计算替换概率。 - 这个概率模型遵循以下原则: - 1. 如果目标字比原始字更常用,替换概率为 1.0(倾向于换成更常见的字)。 - 2. 如果频率差异超过 `max_freq_diff` 阈值,替换概率为 0.0。 - 3. 否则,使用指数衰减函数计算概率,频率差异越大,替换概率越低。 - 这使得替换更倾向于选择频率相近的字。 + 频率相近的字有更高的替换概率。 Args: - orig_freq (float): 原始字的归一化频率。 - target_freq (float): 目标替换字的归一化频率。 + orig_freq (float): 原始字的频率。 + target_freq (float): 目标替换字的频率。 Returns: float: 替换概率,介于 0.0 和 1.0 之间。 @@ -229,19 +210,14 @@ class ChineseTypoGenerator: def _get_similar_frequency_chars(self, char, py, num_candidates=5): """ 获取与给定汉字发音相似且频率相近的候选替换字。 - 此方法首先根据 `tone_error_rate` 决定是否寻找声调错误的同音字, - 然后合并声调正确的同音字。接着,根据字频进行过滤和排序: - 1. 移除原始字本身和频率低于 `min_freq` 的字。 - 2. 计算每个候选字的替换概率。 - 3. 按替换概率降序排序,并返回前 `num_candidates` 个候选字。 Args: char (str): 原始汉字。 py (str): 原始汉字的拼音。 - num_candidates (int): 返回的候选字数量上限。 + num_candidates (int): 返回的候选字数量。 Returns: - list or None: 一个包含候选替换字的列表,如果没有找到合适的候选字则返回 None。 + list or None: 一个包含候选替换字的列表,如果没有找到则返回 None。 """ homophones = [] @@ -278,6 +254,7 @@ class ChineseTypoGenerator: if not candidates_with_prob: return None + # 根据替换概率从高到低排序 candidates_with_prob.sort(key=lambda x: x, reverse=True) # 返回概率最高的几个候选字 @@ -292,9 +269,9 @@ class ChineseTypoGenerator: word (str): 输入的词语。 Returns: - List[str]: 包含每个汉字拼音的列表。 + list: 包含每个汉字拼音的列表。 """ - return ["".join(p) for p in pinyin(word, style=Style.TONE3)] + return [py for py in pinyin(word, style=Style.TONE3)] @staticmethod def _segment_sentence(sentence): @@ -311,17 +288,14 @@ class ChineseTypoGenerator: def _get_word_homophones(self, word): """ - 获取一个词语的所有同音词。 - 该方法首先获取词语中每个字的拼音,然后为每个字找到所有同音字。 - 接着,使用 `itertools.product` 生成所有可能的同音字组合。 - 最后,过滤掉原始词本身,并只保留在 `jieba` 词典中存在的、有意义的词语。 - 这可以有效避免生成无意义的同音词组合。 + 获取一个词语的同音词。 + 只返回在jieba词典中存在且频率较高的有意义词语。 Args: word (str): 原始词语。 Returns: - List[str]: 一个包含所有有效同音词的列表。 + list: 一个包含同音词的列表。 """ if len(word) <= 1: return [] @@ -336,28 +310,45 @@ class ChineseTypoGenerator: return [] # 如果某个字没有同音字,则无法构成同音词 candidates.append(chars) - all_combinations = itertools.product(*candidates) - homophones = [ - "".join(combo) - for combo in all_combinations - if ("".join(combo) != word and "".join(combo) in jieba.dt.FREQ) - ] + # 生成所有可能的同音字组合 + import itertools - return homophones + all_combinations = itertools.product(*candidates) + + # 加载jieba词典以验证组合出的词是否为有效词语 + dict_path = os.path.join(os.path.dirname(jieba.__file__), "dict.txt") + valid_words = {} + with open(dict_path, "r", encoding="utf-8") as f: + for line in f: + parts = line.strip().split() + if len(parts) >= 2: + valid_words[parts] = float(parts[0][1]) + + original_word_freq = valid_words.get(word, 0) + # 设置一个最小词频阈值,过滤掉非常生僻的词 + min_word_freq = original_word_freq * 0.1 + + homophones = [] + for combo in all_combinations: + new_word = "".join(combo) + # 检查新词是否为有效词语且与原词不同 + if new_word != word and new_word in valid_words: + new_word_freq = valid_words[new_word] + if new_word_freq >= min_word_freq: + # 计算综合评分,结合词频和平均字频 + char_avg_freq = sum(self.char_frequency.get(c, 0) for c in new_word) / len(new_word) + combined_score = new_word_freq * 0.7 + char_avg_freq * 0.3 + if combined_score >= self.min_freq: + homophones.append((new_word, combined_score)) + + # 按综合分数排序并返回前5个结果 + sorted_homophones = sorted(homophones, key=lambda x: x, reverse=True) + return [w for w, _ in sorted_homophones[:5]] def create_typo_sentence(self, sentence): """ 为输入句子生成一个包含错别字的版本。 - 这是核心的错别字生成方法,其流程如下: - 1. 使用 jieba 对输入句子进行分词。 - 2. 遍历每个词语: - a. 如果词语长度大于1,根据 `word_replace_rate` 概率尝试进行整词替换。 - 如果找到了合适的同音词,则替换并跳过后续步骤。 - b. 如果不进行整词替换,则遍历词语中的每个汉字。 - c. 对每个汉字,调用 `_char_replace` 方法,根据 `error_rate` 和词语长度调整后的概率, - 决定是否进行单字替换。 - 3. 将处理后的词语拼接成最终的错别字句子。 - 4. 从所有发生的替换中,随机选择一个作为修正建议返回。 + 该方法会先对句子进行分词,然后根据概率进行整词替换或单字替换。 Args: sentence (str): 原始中文句子。 @@ -366,7 +357,7 @@ class ChineseTypoGenerator: tuple: 包含三个元素的元组: - original_sentence (str): 原始句子。 - typo_sentence (str): 包含错别字的句子。 - - correction_suggestion (Optional[tuple(str, str)]): 一个随机的修正建议,格式为 (错字/词, 正确字/词),或 None。 + - correction_suggestion (str or None): 一个随机的修正建议(可能是正确的字或词),或 None。 """ result = [] typo_info = [] # 用于调试,记录详细的替换信息 @@ -406,56 +397,44 @@ class ChineseTypoGenerator: word_typos.append((typo_word, word)) continue - new_word = "".join( - self._char_replace(char, py, len(word), typo_info, char_typos) for char, py in zip(word, word_pinyin) - ) - result.append(new_word) + # 步骤2: 如果不进行整词替换,则对词中的每个字进行单字替换 + new_word = [] + for char, py in zip(word, word_pinyin, strict=False): + # 词语越长,其中单个字被替换的概率越低 + char_error_rate = self.error_rate * (0.7 ** (len(word) - 1)) + if random.random() < char_error_rate: + similar_chars = self._get_similar_frequency_chars(char, py) + if similar_chars: + typo_char = random.choice(similar_chars) + orig_freq = self.char_frequency.get(char, 0) + typo_freq = self.char_frequency.get(typo_char, 0) + # 根据频率计算最终是否替换 + if random.random() < self._calculate_replacement_probability(orig_freq, typo_freq): + new_word.append(typo_char) + typo_py = pinyin(typo_char, style=Style.TONE3) + typo_info.append((char, typo_char, py, typo_py, orig_freq, typo_freq)) + char_typos.append((typo_char, char)) + continue + # 如果不替换,则保留原字 + new_word.append(char) + + result.append("".join(new_word)) + + # 步骤3: 生成修正建议 + correction_suggestion = None + # 有50%的概率提供一个修正建议 + if random.random() < 0.5: + # 优先从整词错误中选择 + if word_typos: + _, correct_word = random.choice(word_typos) + correction_suggestion = correct_word + # 其次从单字错误中选择 + elif char_typos: + _, correct_char = random.choice(char_typos) + correction_suggestion = correct_char - all_typos = word_typos + char_typos - correction_suggestion = random.choice(all_typos) if all_typos and random.random() < 0.5 else None return sentence, "".join(result), correction_suggestion - def _char_replace(self, char, py, word_len, typo_info, char_typos): - """ - 根据概率替换单个汉字。 - 这个内部方法处理单个汉字的替换逻辑。 - - Args: - char (str): 要处理的原始汉字。 - py (str): 原始汉字的拼音。 - word_len (int): 原始汉字所在的词语的长度。 - typo_info (list): 用于记录详细调试信息的列表。 - char_typos (list): 用于记录 (错字, 正确字) 的列表。 - - Returns: - str: 替换后的汉字(可能是原汉字,也可能是错别字)。 - """ - # 根据词语长度调整错误率:词语越长,单个字出错的概率越低。 - # 这是一个启发式规则,模拟人们在输入长词时更不容易打错单个字。 - char_error_rate = self.error_rate * (0.7 ** (word_len - 1)) - if random.random() >= char_error_rate: - return char - - # 获取发音和频率都相似的候选错别字 - similar_chars = self._get_similar_frequency_chars(char, py) - if not similar_chars: - return char - - # 从候选列表中随机选择一个 - typo_char = random.choice(similar_chars) - orig_freq = self.char_frequency.get(char, 0) - typo_freq = self.char_frequency.get(typo_char, 0) - - # 根据频率差异再次进行概率判断,决定是否执行替换 - if random.random() >= self._calculate_replacement_probability(orig_freq, typo_freq): - return char - - # 执行替换,并记录相关信息 - typo_py = pinyin(typo_char, style=Style.TONE3) - typo_info.append((char, typo_char, py, typo_py, orig_freq, typo_freq)) - char_typos.append((typo_char, char)) - return typo_char - @staticmethod def format_typo_info(typo_info): """ diff --git a/src/plugin_system/apis/generator_api.py b/src/plugin_system/apis/generator_api.py index 7f3074a81..8d8d04eb3 100644 --- a/src/plugin_system/apis/generator_api.py +++ b/src/plugin_system/apis/generator_api.py @@ -239,6 +239,8 @@ def process_human_text( enable_splitter: 是否启用消息分割器 enable_chinese_typo: 是否启用错字生成器 """ + if isinstance(content, list): + content = "".join(map(str, content)) if not isinstance(content, str): raise ValueError("content 必须是字符串类型") try: From c097b5d00b27fb7f6ae2be76e7c5a79019259454 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 20:23:24 +0800 Subject: [PATCH 41/69] =?UTF-8?q?feat(config):=20=E6=94=AF=E6=8C=81API?= =?UTF-8?q?=E5=AF=86=E9=92=A5=E5=88=97=E8=A1=A8=E8=BD=AE=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将`api_key`类型从`str`扩展为`Union[str, List[str]]`,允许用户配置多个API密钥。 - 新增`get_api_key`方法,通过线程安全的方式实现API密钥的轮询使用,提高API请求的稳定性和可用性。 - 更新了`api_key`的验证逻辑,以同时支持字符串和字符串列表两种格式。 - 相应地更新了配置文件模板,以示例新的密钥列表配置方式。 --- src/config/api_ada_configs.py | 32 ++++++++++++++++++++++++----- template/model_config_template.toml | 4 ++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/config/api_ada_configs.py b/src/config/api_ada_configs.py index 5e53eec4b..cc25d0646 100644 --- a/src/config/api_ada_configs.py +++ b/src/config/api_ada_configs.py @@ -1,5 +1,6 @@ -from typing import List, Dict, Any, Literal +from typing import List, Dict, Any, Literal, Union from pydantic import Field, field_validator +from threading import Lock from src.config.config_base import ValidatedConfigBase @@ -9,7 +10,7 @@ class APIProvider(ValidatedConfigBase): name: str = Field(..., min_length=1, description="API提供商名称") base_url: str = Field(..., description="API基础URL") - api_key: str = Field(..., min_length=1, description="API密钥") + api_key: Union[str, List[str]] = Field(..., min_length=1, description="API密钥,支持单个密钥或密钥列表轮询") client_type: Literal["openai", "gemini", "aiohttp_gemini"] = Field( default="openai", description="客户端类型(如openai/google等,默认为openai)" ) @@ -33,12 +34,33 @@ class APIProvider(ValidatedConfigBase): @classmethod def validate_api_key(cls, v): """验证API密钥不能为空""" - if not v or not v.strip(): - raise ValueError("API密钥不能为空") + if isinstance(v, str): + if not v.strip(): + raise ValueError("API密钥不能为空") + elif isinstance(v, list): + if not v: + raise ValueError("API密钥列表不能为空") + for key in v: + if not isinstance(key, str) or not key.strip(): + raise ValueError("API密钥列表中的密钥不能为空") + else: + raise ValueError("API密钥必须是字符串或字符串列表") return v + def __init__(self, **data): + super().__init__(**data) + self._api_key_lock = Lock() + self._api_key_index = 0 + def get_api_key(self) -> str: - return self.api_key + with self._api_key_lock: + if isinstance(self.api_key, str): + return self.api_key + if not self.api_key: + raise ValueError("API密钥列表为空") + key = self.api_key[self._api_key_index] + self._api_key_index = (self._api_key_index + 1) % len(self.api_key) + return key class ModelInfo(ValidatedConfigBase): diff --git a/template/model_config_template.toml b/template/model_config_template.toml index fab3ee509..c5f2a2947 100644 --- a/template/model_config_template.toml +++ b/template/model_config_template.toml @@ -6,7 +6,7 @@ version = "1.3.1" [[api_providers]] # API服务提供商(可以配置多个) name = "DeepSeek" # API服务商名称(可随意命名,在models的api-provider中需使用这个命名) base_url = "https://api.deepseek.com/v1" # API服务商的BaseURL -api_key = "your-api-key-here" # API密钥(请替换为实际的API密钥) +api_key = ["your-api-key-here-1", "your-api-key-here-2"] # API密钥(支持单个密钥或密钥列表轮询) client_type = "openai" # 请求客户端(可选,默认值为"openai",使用gimini等Google系模型时请配置为"gemini") max_retry = 2 # 最大重试次数(单个模型API调用失败,最多重试的次数) timeout = 30 # API请求超时时间(单位:秒) @@ -24,7 +24,7 @@ retry_interval = 10 [[api_providers]] # 特殊:Google的Gimini使用特殊API,与OpenAI格式不兼容,需要配置client为"aiohttp_gemini" name = "Google" base_url = "https://api.google.com/v1" -api_key = "your-google-api-key-1" +api_key = ["your-google-api-key-1", "your-google-api-key-2"] client_type = "aiohttp_gemini" # 官方的gemini客户端现在已经死了 max_retry = 2 timeout = 30 From 742b47c099a310bf83e5f1778afb01a327839159 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sat, 6 Sep 2025 20:39:59 +0800 Subject: [PATCH 42/69] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E6=97=A0=E6=B3=95=E6=AD=A3=E7=A1=AE=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E6=8F=92=E4=BB=B6=E9=85=8D=E7=BD=AE=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 1 - src/main.py | 3 ++ src/plugin_system/base/base_events_handler.py | 13 ++++- src/plugin_system/core/component_registry.py | 34 ++++++++++-- src/plugin_system/core/event_manager.py | 1 + .../built_in/napcat_adapter_plugin/plugin.py | 33 ++++++------ test_plugin_config_fix.py | 53 +++++++++++++++++++ 7 files changed, 116 insertions(+), 22 deletions(-) create mode 100644 test_plugin_config_fix.py diff --git a/bot.py b/bot.py index 9f90832a7..51d76e642 100644 --- a/bot.py +++ b/bot.py @@ -82,7 +82,6 @@ def easter_egg(): async def graceful_shutdown(): try: logger.info("正在优雅关闭麦麦...") - # 停止所有异步任务 await async_task_manager.stop_and_wait_all_tasks() diff --git a/src/main.py b/src/main.py index 832f70be9..d76dfd0d3 100644 --- a/src/main.py +++ b/src/main.py @@ -113,6 +113,9 @@ class MainSystem: """清理资源""" try: # 停止消息重组器 + 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")) from src.utils.message_chunker import reassembler import asyncio diff --git a/src/plugin_system/base/base_events_handler.py b/src/plugin_system/base/base_events_handler.py index c56a84864..6b8ed1d73 100644 --- a/src/plugin_system/base/base_events_handler.py +++ b/src/plugin_system/base/base_events_handler.py @@ -34,8 +34,14 @@ class BaseEventHandler(ABC): if EventType.UNKNOWN in self.init_subscribe: raise NotImplementedError("事件处理器必须指定 event_type") - from src.plugin_system.core.component_registry import component_registry - self.plugin_config = component_registry.get_plugin_config(self.plugin_name) + # 优先使用实例级别的 plugin_config,如果没有则使用类级别的配置 + # 事件管理器会在注册时通过 set_plugin_config 设置实例级别的配置 + instance_config = getattr(self, "plugin_config", None) + if instance_config is not None: + self.plugin_config = instance_config + else: + # 如果实例级别没有配置,则使用类级别的配置(向后兼容) + self.plugin_config = getattr(self.__class__, "plugin_config", {}) @abstractmethod async def execute(self, kwargs: dict | None) -> Tuple[bool, bool, Optional[str]]: @@ -101,6 +107,9 @@ class BaseEventHandler(ABC): """ self.plugin_name = plugin_name + def set_plugin_config(self,plugin_config) -> None: + self.plugin_config = plugin_config + def get_config(self, key: str, default=None): """获取插件配置值,支持嵌套键访问 diff --git a/src/plugin_system/core/component_registry.py b/src/plugin_system/core/component_registry.py index 7dfba5bd3..529f327a3 100644 --- a/src/plugin_system/core/component_registry.py +++ b/src/plugin_system/core/component_registry.py @@ -1,3 +1,4 @@ +from pathlib import Path import re from typing import Dict, List, Optional, Any, Pattern, Tuple, Union, Type @@ -170,6 +171,8 @@ class ComponentRegistry: return False action_class.plugin_name = action_info.plugin_name + # 设置插件配置 + action_class.plugin_config = self.get_plugin_config(action_info.plugin_name) or {} self._action_registry[action_name] = action_class # 如果启用,添加到默认动作集 @@ -188,6 +191,8 @@ class ComponentRegistry: return False command_class.plugin_name = command_info.plugin_name + # 设置插件配置 + command_class.plugin_config = self.get_plugin_config(command_info.plugin_name) or {} self._command_registry[command_name] = command_class # 如果启用了且有匹配模式 @@ -220,6 +225,8 @@ class ComponentRegistry: self._plus_command_registry: Dict[str, Type[PlusCommand]] = {} plus_command_class.plugin_name = plus_command_info.plugin_name + # 设置插件配置 + plus_command_class.plugin_config = self.get_plugin_config(plus_command_info.plugin_name) or {} self._plus_command_registry[plus_command_name] = plus_command_class logger.debug(f"已注册PlusCommand组件: {plus_command_name}") @@ -230,6 +237,8 @@ class ComponentRegistry: tool_name = tool_info.name tool_class.plugin_name = tool_info.plugin_name + # 设置插件配置 + tool_class.plugin_config = self.get_plugin_config(tool_info.plugin_name) or {} self._tool_registry[tool_name] = tool_class # 如果是llm可用的且启用的工具,添加到 llm可用工具列表 @@ -249,6 +258,8 @@ class ComponentRegistry: return False handler_class.plugin_name = handler_info.plugin_name + # 设置插件配置 + handler_class.plugin_config = self.get_plugin_config(handler_info.plugin_name) or {} self._event_handler_registry[handler_name] = handler_class if not handler_info.enabled: @@ -259,7 +270,7 @@ class ComponentRegistry: # 使用EventManager进行事件处理器注册 from src.plugin_system.core.event_manager import event_manager - return event_manager.register_event_handler(handler_class) + return event_manager.register_event_handler(handler_class,self.get_plugin_config(handler_info.plugin_name) or {}) # === 组件移除相关 === @@ -656,20 +667,35 @@ class ComponentRegistry: plugin_info = self.get_plugin_info(plugin_name) return plugin_info.components if plugin_info else [] - def get_plugin_config(self, plugin_name: str) -> Optional[dict]: + def get_plugin_config(self, plugin_name: str) -> dict: """获取插件配置 Args: plugin_name: 插件名称 Returns: - Optional[dict]: 插件配置字典或None + dict: 插件配置字典,如果插件实例不存在或配置为空,返回空字典 """ # 从插件管理器获取插件实例的配置 from src.plugin_system.core.plugin_manager import plugin_manager plugin_instance = plugin_manager.get_plugin_instance(plugin_name) - return plugin_instance.config if plugin_instance else None + if plugin_instance and plugin_instance.config: + return plugin_instance.config + + # 如果插件实例不存在,尝试从配置文件读取 + try: + import toml + config_path = Path("config") / "plugins" / plugin_name / "config.toml" + if config_path.exists(): + with open(config_path, 'r', encoding='utf-8') as f: + config_data = toml.load(f) + logger.debug(f"从配置文件读取插件 {plugin_name} 的配置") + return config_data + except Exception as e: + logger.debug(f"读取插件 {plugin_name} 配置文件失败: {e}") + + return {} def get_registry_stats(self) -> Dict[str, Any]: """获取注册中心统计信息""" diff --git a/src/plugin_system/core/event_manager.py b/src/plugin_system/core/event_manager.py index 4e950fd76..a69fb01c0 100644 --- a/src/plugin_system/core/event_manager.py +++ b/src/plugin_system/core/event_manager.py @@ -166,6 +166,7 @@ class EventManager: # 创建事件处理器实例,传递插件配置 handler_instance = handler_class() + handler_instance.plugin_config = plugin_config if plugin_config is not None and hasattr(handler_instance, 'set_plugin_config'): handler_instance.set_plugin_config(plugin_config) diff --git a/src/plugins/built_in/napcat_adapter_plugin/plugin.py b/src/plugins/built_in/napcat_adapter_plugin/plugin.py index c3dc3b23b..4f7c1ee8e 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/plugin.py +++ b/src/plugins/built_in/napcat_adapter_plugin/plugin.py @@ -233,21 +233,6 @@ class LauchNapcatAdapterHandler(BaseEventHandler): logger.info("功能配置已迁移到插件系统") logger.info("开始启动Napcat Adapter") message_send_instance.maibot_router = router - # 设置插件配置 - message_send_instance.set_plugin_config(self.plugin_config) - # 设置chunker的插件配置 - chunker.set_plugin_config(self.plugin_config) - # 设置response_pool的插件配置 - from .src.response_pool import set_plugin_config as set_response_pool_config - set_response_pool_config(self.plugin_config) - # 设置send_handler的插件配置 - send_handler.set_plugin_config(self.plugin_config) - # 设置message_handler的插件配置 - message_handler.set_plugin_config(self.plugin_config) - # 设置notice_handler的插件配置 - notice_handler.set_plugin_config(self.plugin_config) - # 设置meta_event_handler的插件配置 - meta_event_handler.set_plugin_config(self.plugin_config) # 创建单独的异步任务,防止阻塞主线程 asyncio.create_task(napcat_server(self.plugin_config)) asyncio.create_task(mmc_start_com(self.plugin_config)) @@ -355,3 +340,21 @@ class NapcatAdapterPlugin(BasePlugin): if issubclass(handler, BaseEventHandler): components.append((handler.get_handler_info(), handler)) return components + + async def on_plugin_loaded(self): + # 设置插件配置 + message_send_instance.set_plugin_config(self.config) + # 设置chunker的插件配置 + chunker.set_plugin_config(self.config) + # 设置response_pool的插件配置 + from .src.response_pool import set_plugin_config as set_response_pool_config + set_response_pool_config(self.config) + # 设置send_handler的插件配置 + send_handler.set_plugin_config(self.config) + # 设置message_handler的插件配置 + message_handler.set_plugin_config(self.config) + # 设置notice_handler的插件配置 + notice_handler.set_plugin_config(self.config) + # 设置meta_event_handler的插件配置 + meta_event_handler.set_plugin_config(self.config) + # 设置其他handler的插件配置(现在由component_registry在注册时自动设置) \ No newline at end of file diff --git a/test_plugin_config_fix.py b/test_plugin_config_fix.py new file mode 100644 index 000000000..a5e6c77b0 --- /dev/null +++ b/test_plugin_config_fix.py @@ -0,0 +1,53 @@ +#!/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 From 132354804cc4b4a6e848bb35fc9f0433ffc02ca3 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 20:49:56 +0800 Subject: [PATCH 43/69] =?UTF-8?q?feat(planner):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=A4=A7=E5=B0=8F=E8=84=91=E8=A7=84=E5=88=92=E5=99=A8=E5=88=86?= =?UTF-8?q?=E7=A6=BB=E4=BB=A5=E4=BC=98=E5=8C=96=E5=86=B3=E7=AD=96=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将规划器(Planner)拆分为“大脑”和“小脑”两个部分,以实现更精细化的决策控制。 - **大脑(BIG_BRAIN)**: 负责宏观决策,如是否回复、是否需要@人等高层级意图。 - **小脑(SMALL_BRAIN)**: 负责具体的功能性动作执行。 此重构引入了 `PlannerType` 枚举,并更新了动作(Action)定义,允许将动作明确分配给大脑或小脑,从而提升了AI回复的逻辑性和条理性。同时,新增了 `no_action` 类型,用于在规划阶段明确表示“无动作”,提高了处理流程的清晰度。 --- src/chat/chat_loop/cycle_processor.py | 6 ++-- src/chat/planner_actions/planner.py | 18 ++++++++-- src/plugin_system/base/base_action.py | 4 ++- src/plugin_system/base/component_types.py | 34 +++++++------------ src/plugins/built_in/at_user_plugin/plugin.py | 3 +- 5 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index cc938164d..bb58e6884 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -228,6 +228,8 @@ class CycleProcessor: async def execute_action(action_info): """执行单个动作的通用函数""" try: + if action_info["action_type"] == "no_action": + return {"action_type": "no_action", "success": True, "reply_text": "", "command": ""} if action_info["action_type"] == "no_reply": # 直接处理no_reply逻辑,不再通过动作系统 reason = action_info.get("reasoning", "选择不回复") @@ -245,7 +247,7 @@ class CycleProcessor: ) return {"action_type": "no_reply", "success": True, "reply_text": "", "command": ""} - elif action_info["action_type"] != "reply": + elif action_info["action_type"] != "reply" and action_info["action_type"] != "no_action": # 执行普通动作 with Timer("动作执行", cycle_timers): success, reply_text, command = await self._handle_action( @@ -420,7 +422,7 @@ class CycleProcessor: if "reply" in available_actions: fallback_action = "reply" elif available_actions: - fallback_action = list(available_actions.keys()) + fallback_action = list(available_actions.keys())[0] if fallback_action and fallback_action != action: logger.info(f"{self.context.log_prefix} 使用回退动作: {fallback_action}") diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index c353df3ef..a1efd8d57 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -23,7 +23,13 @@ from src.chat.utils.chat_message_builder import ( from src.chat.utils.utils import get_chat_type_and_target_info from src.chat.planner_actions.action_manager import ActionManager from src.chat.message_receive.chat_stream import get_chat_manager -from src.plugin_system.base.component_types import ActionInfo, ChatMode, ComponentType, ActionActivationType +from src.plugin_system.base.component_types import ( + ActionInfo, + ChatMode, + ComponentType, + ActionActivationType, + PlannerType, +) from src.plugin_system.core.component_registry import component_registry from src.schedule.schedule_manager import schedule_manager from src.mood.mood_manager import mood_manager @@ -503,6 +509,9 @@ class ActionPlanner: try: sub_planner_actions: Dict[str, ActionInfo] = {} for action_name, action_info in available_actions.items(): + if action_info.planner_type not in [PlannerType.SMALL_BRAIN, PlannerType.ALL]: + continue + 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: @@ -550,10 +559,15 @@ class ActionPlanner: # --- 3. 大脑独立思考是否回复 --- action, reasoning, action_data, target_message = "no_reply", "大脑初始化默认", {}, None try: + big_brain_actions = { + name: info + for name, info in available_actions.items() + if info.planner_type in [PlannerType.BIG_BRAIN, PlannerType.ALL] + } prompt, _ = await self.build_planner_prompt( is_group_chat=is_group_chat, chat_target_info=chat_target_info, - current_available_actions={}, # 大脑不考虑具体action + current_available_actions=big_brain_actions, mode=mode, chat_content_block_override=chat_content_block, message_id_list_override=message_id_list, diff --git a/src/plugin_system/base/base_action.py b/src/plugin_system/base/base_action.py index 4a2d16aa1..38d7f5a5b 100644 --- a/src/plugin_system/base/base_action.py +++ b/src/plugin_system/base/base_action.py @@ -6,7 +6,7 @@ from typing import Tuple, Optional from src.common.logger import get_logger from src.chat.message_receive.chat_stream import ChatStream -from src.plugin_system.base.component_types import ActionActivationType, ChatMode, ActionInfo, ComponentType, ChatType +from src.plugin_system.base.component_types import ActionActivationType, ChatMode, ActionInfo, ComponentType, ChatType,PlannerType from src.plugin_system.apis import send_api, database_api, message_api @@ -92,6 +92,8 @@ class BaseAction(ABC): self.parallel_action: bool = getattr(self.__class__, "parallel_action", True) self.associated_types: list[str] = getattr(self.__class__, "associated_types", []).copy() self.chat_type_allow: ChatType = getattr(self.__class__, "chat_type_allow", ChatType.ALL) + self.planner_type: PlannerType = getattr(self.__class__, "planner_type", ChatType.ALL) + # ============================================================================= # 便捷属性 - 直接在初始化时获取常用聊天信息(带类型注解) diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index ec88ff3ae..f740d0c96 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -51,6 +51,17 @@ class ChatMode(Enum): # 聊天类型枚举 +class PlannerType(Enum): + """规划器类型枚举""" + + BIG_BRAIN = "big_brain" # 大脑,负责宏观决策 + SMALL_BRAIN = "small_brain" # 小脑,负责具体动作 + ALL = "all" # 通用 + + def __str__(self): + return self.value + + class ChatType(Enum): """聊天类型枚举,用于限制插件在不同聊天环境中的使用""" @@ -140,6 +151,7 @@ class ActionInfo(ComponentInfo): mode_enable: ChatMode = ChatMode.ALL parallel_action: bool = False chat_type_allow: ChatType = ChatType.ALL # 允许的聊天类型 + planner_type: PlannerType = PlannerType.ALL def __post_init__(self): super().__post_init__() @@ -215,27 +227,7 @@ class EventInfo(ComponentInfo): def __post_init__(self): super().__post_init__() - self.component_type = ComponentType.EVENT - - -# 事件类型枚举 -class EventType(Enum): - """ - 事件类型枚举类 - """ - - ON_START = "on_start" # 启动事件,用于调用按时任务 - ON_STOP = "on_stop" # 停止事件,用于调用按时任务 - ON_MESSAGE = "on_message" - ON_PLAN = "on_plan" - POST_LLM = "post_llm" - AFTER_LLM = "after_llm" - POST_SEND = "post_send" - AFTER_SEND = "after_send" - UNKNOWN = "unknown" # 未知事件类型 - - def __str__(self) -> str: - return self.value + self.component_type = ComponentType.EVENT_HANDLER @dataclass diff --git a/src/plugins/built_in/at_user_plugin/plugin.py b/src/plugins/built_in/at_user_plugin/plugin.py index c39bb8971..4b422a525 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -10,7 +10,7 @@ from src.plugin_system import ( ) from src.person_info.person_info import get_person_info_manager from src.common.logger import get_logger -from src.plugin_system.base.component_types import ChatType +from src.plugin_system.base.component_types import ChatType,PlannerType logger = get_logger(__name__) @@ -24,6 +24,7 @@ class AtAction(BaseAction): activation_type = ActionActivationType.LLM_JUDGE # 消息接收时激活(?) parallel_action = False chat_type_allow = ChatType.GROUP + planner_type = PlannerType.BIG_BRAIN # === 功能描述(必须填写)=== action_parameters = {"user_name": "需要艾特用户的名字", "at_message": "艾特用户时要发送的消,注意消息里不要有@"} From 2241db3ebffe74bf14a3d5517c38e88c81ec6459 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 20:56:13 +0800 Subject: [PATCH 44/69] =?UTF-8?q?Revert=20"feat(chat):=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E5=8F=91=E9=80=81=E9=94=99=E5=88=AB=E5=AD=97=E5=90=8E?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=92=A4=E5=9B=9E=E4=BF=AE=E6=AD=A3=E7=9A=84?= =?UTF-8?q?=E5=8A=9F=E8=83=BD"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit fd5d951501fff4e807465a42c7dfa65bc6e8fd7b. --- src/chat/chat_loop/cycle_processor.py | 14 +- src/chat/chat_loop/response_handler.py | 51 +--- src/chat/utils/typo_generator.py | 358 +++++++++++------------- src/chat/utils/utils.py | 43 +-- src/plugin_system/apis/generator_api.py | 16 +- src/plugin_system/apis/send_api.py | 39 +-- 6 files changed, 199 insertions(+), 322 deletions(-) diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index bb58e6884..913111bdc 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -71,12 +71,7 @@ class CycleProcessor: """ # 发送回复 with Timer("回复发送", cycle_timers): - reply_text, sent_messages = await self.response_handler.send_response( - response_set, loop_start_time, action_message - ) - if sent_messages: - # 异步处理错别字修正 - asyncio.create_task(self.response_handler.handle_typo_correction(sent_messages)) + reply_text = await self.response_handler.send_response(response_set, loop_start_time, action_message) # 存储reply action信息 person_info_manager = get_person_info_manager() @@ -185,8 +180,7 @@ class CycleProcessor: cycle_timers, thinking_id = self.cycle_tracker.start_cycle() logger.info(f"{self.log_prefix} 开始第{self.context.cycle_counter}次思考") - # 发送正在输入状态 - if ENABLE_S4U and self.context.chat_stream and self.context.chat_stream.user_info: + if ENABLE_S4U: await send_typing(self.context.chat_stream.user_info.user_id) loop_start_time = time.time() @@ -214,7 +208,7 @@ class CycleProcessor: result = await event_manager.trigger_event( EventType.ON_PLAN, plugin_name="SYSTEM", stream_id=self.context.chat_stream ) - if result and not result.all_continue_process(): + if not result.all_continue_process(): raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于规划前中断了内容生成") # 规划动作 @@ -427,7 +421,7 @@ class CycleProcessor: if fallback_action and fallback_action != action: logger.info(f"{self.context.log_prefix} 使用回退动作: {fallback_action}") action_handler = self.context.action_manager.create_action( - action_name=str(fallback_action), + action_name=fallback_action if isinstance(fallback_action, list) else fallback_action, action_data=action_data, reasoning=f"原动作'{action}'不可用,自动回退。{reasoning}", cycle_timers=cycle_timers, diff --git a/src/chat/chat_loop/response_handler.py b/src/chat/chat_loop/response_handler.py index 61608ccb9..f21a33afd 100644 --- a/src/chat/chat_loop/response_handler.py +++ b/src/chat/chat_loop/response_handler.py @@ -1,6 +1,5 @@ import time import random -import asyncio from typing import Dict, Any, Tuple from src.common.logger import get_logger @@ -64,11 +63,7 @@ class ResponseHandler: - 构建并返回完整的循环信息 - 用于上级方法的状态跟踪 """ - # 发送回复 - reply_text, sent_messages = await self.send_response(response_set, loop_start_time, action_message) - if sent_messages: - # 异步处理错别字修正 - asyncio.create_task(self.handle_typo_correction(sent_messages)) + reply_text = await self.send_response(response_set, loop_start_time, action_message) person_info_manager = get_person_info_manager() @@ -113,17 +108,18 @@ class ResponseHandler: return loop_info, reply_text, cycle_timers - async def send_response(self, reply_set, thinking_start_time, message_data) -> tuple[str, list[dict[str, str]]]: + async def send_response(self, reply_set, thinking_start_time, message_data) -> str: """ 发送回复内容的具体实现 Args: reply_set: 回复内容集合,包含多个回复段 + reply_to: 回复目标 thinking_start_time: 思考开始时间 message_data: 消息数据 Returns: - tuple[str, list[dict[str, str]]]: (完整的回复文本, 已发送消息列表) + str: 完整的回复文本 功能说明: - 检查是否有新消息需要回复 @@ -142,18 +138,19 @@ class ResponseHandler: need_reply = new_message_count >= random.randint(2, 4) reply_text = "" - sent_messages = [] is_proactive_thinking = message_data.get("message_type") == "proactive_thinking" first_replied = False for reply_seg in reply_set: + # 调试日志:验证reply_seg的格式 logger.debug(f"Processing reply_seg type: {type(reply_seg)}, content: {reply_seg}") - # 提取回复内容 - if reply_seg["type"] == "typo": - data = reply_seg["typo"] + # 修正:正确处理元组格式 (格式为: (type, content)) + if isinstance(reply_seg, tuple) and len(reply_seg) >= 2: + _, data = reply_seg else: - data = reply_seg["content"] + # 向下兼容:如果已经是字符串,则直接使用 + data = str(reply_seg) if isinstance(data, list): data = "".join(map(str, data)) @@ -166,7 +163,7 @@ class ResponseHandler: # 发送第一段回复 if not first_replied: - sent_message = await send_api.text_to_stream( + await send_api.text_to_stream( text=data, stream_id=self.context.stream_id, reply_to_message=message_data, @@ -183,29 +180,5 @@ class ResponseHandler: set_reply=False, typing=True, ) - # 记录已发送的错别字消息 - if sent_message and reply_seg["type"] == "typo": - sent_messages.append( - { - "type": "typo", - "message_id": sent_message, - "original_message": message_data, - "correction": reply_seg["correction"], - } - ) - return reply_text, sent_messages - - async def handle_typo_correction(self, sent_messages: list[dict[str, Any]]): - """处理错别字修正""" - for msg in sent_messages: - if msg["type"] == "typo": - # 随机等待一段时间 - await asyncio.sleep(random.uniform(2, 4)) - # 撤回消息 - recalled = await send_api.recall_message(str(msg["message_id"]), self.context.stream_id) - if recalled: - # 发送修正后的消息 - await send_api.text_to_stream( - str(msg["correction"]), self.context.stream_id, reply_to_message=msg["original_message"] - ) + return reply_text diff --git a/src/chat/utils/typo_generator.py b/src/chat/utils/typo_generator.py index c23c4c319..9c3718b2b 100644 --- a/src/chat/utils/typo_generator.py +++ b/src/chat/utils/typo_generator.py @@ -19,22 +19,16 @@ logger = get_logger("typo_gen") class ChineseTypoGenerator: - """ - 中文错别字生成器。 - 可以根据拼音、字频等信息,为给定的中文句子生成包含错别字的句子。 - 支持单字替换和整词替换。 - """ - def __init__(self, error_rate=0.3, min_freq=5, tone_error_rate=0.2, word_replace_rate=0.3, max_freq_diff=200): """ - 初始化错别字生成器。 + 初始化错别字生成器 - Args: - error_rate (float): 单个汉字被替换为同音字的概率。 - min_freq (int): 候选替换字的最小词频阈值,低于此阈值的字将被忽略。 - tone_error_rate (float): 在选择同音字时,使用错误声调的概率。 - word_replace_rate (float): 整个词语被替换为同音词的概率。 - max_freq_diff (int): 允许的原始字与替换字之间的最大频率差异。 + 参数: + error_rate: 单字替换概率 + min_freq: 最小字频阈值 + tone_error_rate: 声调错误概率 + word_replace_rate: 整词替换概率 + max_freq_diff: 最大允许的频率差异 """ self.error_rate = error_rate self.min_freq = min_freq @@ -42,47 +36,42 @@ class ChineseTypoGenerator: self.word_replace_rate = word_replace_rate self.max_freq_diff = max_freq_diff - # 加载核心数据 - logger.info("正在加载汉字数据库...") + # 加载数据 + # print("正在加载汉字数据库,请稍候...") + # logger.info("正在加载汉字数据库,请稍候...") + self.pinyin_dict = self._create_pinyin_dict() self.char_frequency = self._load_or_create_char_frequency() - logger.info("汉字数据库加载完成。") def _load_or_create_char_frequency(self): """ - 加载或创建汉字频率字典。 - 如果存在缓存文件 `depends-data/char_frequency.json`,则直接加载。 - 否则,通过解析 `jieba` 的词典文件来创建,并保存为缓存。 - - Returns: - dict: 一个将汉字映射到其归一化频率的字典。 + 加载或创建汉字频率字典 """ cache_file = Path("depends-data/char_frequency.json") - # 如果缓存文件存在,则直接从缓存加载,提高效率 + # 如果缓存文件存在,直接加载 if cache_file.exists(): with open(cache_file, "r", encoding="utf-8") as f: return orjson.loads(f.read()) - # 如果没有缓存,则通过解析jieba词典来创建 + # 使用内置的词频文件 char_freq = defaultdict(int) - # 定位jieba内置词典文件的路径 dict_path = os.path.join(os.path.dirname(jieba.__file__), "dict.txt") - # 读取jieba词典文件,统计每个汉字的频率 + # 读取jieba的词典文件 with open(dict_path, "r", encoding="utf-8") as f: for line in f: word, freq = line.strip().split()[:2] - # 将词中每个汉字的频率进行累加 + # 对词中的每个字进行频率累加 for char in word: if self._is_chinese_char(char): char_freq[char] += int(freq) - # 对频率值进行归一化处理,使其在0-1000的范围内 + # 归一化频率值 max_freq = max(char_freq.values()) normalized_freq = {char: freq / max_freq * 1000 for char, freq in char_freq.items()} - # 将计算出的频率数据保存到缓存文件,以便下次快速加载 + # 保存到缓存文件 with open(cache_file, "w", encoding="utf-8") as f: f.write(orjson.dumps(normalized_freq, option=orjson.OPT_INDENT_2).decode("utf-8")) @@ -91,24 +80,18 @@ class ChineseTypoGenerator: @staticmethod def _create_pinyin_dict(): """ - 创建从拼音到汉字的映射字典。 - 遍历常用汉字范围,为每个汉字生成带声调的拼音,并构建映射。 - - Returns: - defaultdict: 一个将拼音映射到汉字列表的字典。 + 创建拼音到汉字的映射字典 """ - # 定义常用汉字的Unicode范围 + # 常用汉字范围 chars = [chr(i) for i in range(0x4E00, 0x9FFF)] pinyin_dict = defaultdict(list) - # 为范围内的每个汉字建立拼音到汉字的映射 + # 为每个汉字建立拼音映射 for char in chars: try: - # 获取带数字声调的拼音 (e.g., 'hao3') - py = pinyin(char, style=Style.TONE3) + py = pinyin(char, style=Style.TONE3)[0][0] pinyin_dict[py].append(char) except Exception: - # 忽略无法转换拼音的字符 continue return pinyin_dict @@ -116,62 +99,49 @@ class ChineseTypoGenerator: @staticmethod def _is_chinese_char(char): """ - 判断一个字符是否为中文字符。 - - Args: - char (str): 需要判断的字符。 - - Returns: - bool: 如果是中文字符,返回 True,否则返回 False。 + 判断是否为汉字 """ try: - # 通过Unicode范围判断是否为中文字符 return "\u4e00" <= char <= "\u9fff" except Exception as e: - logger.debug(f"判断字符 '{char}' 时出错: {e}") + logger.debug(str(e)) return False def _get_pinyin(self, sentence): """ - 获取一个句子中每个汉字的拼音。 - - Args: - sentence (str): 输入的中文句子。 - - Returns: - list: 一个元组列表,每个元组包含 (汉字, 拼音)。 + 将中文句子拆分成单个汉字并获取其拼音 """ + # 将句子拆分成单个字符 characters = list(sentence) + + # 获取每个字符的拼音 result = [] for char in characters: - # 忽略所有非中文字符 - if self._is_chinese_char(char): - # 获取带数字声调的拼音 - py = pinyin(char, style=Style.TONE3) - result.append((char, py)) + # 跳过空格和非汉字字符 + if char.isspace() or not self._is_chinese_char(char): + continue + # 获取拼音(数字声调) + py = pinyin(char, style=Style.TONE3)[0][0] + result.append((char, py)) + return result @staticmethod def _get_similar_tone_pinyin(py): """ - 为一个给定的拼音生成一个声调错误的相似拼音。 - - Args: - py (str): 带数字声调的原始拼音 (e.g., 'hao3')。 - - Returns: - str: 一个声调被随机改变的拼音。 + 获取相似声调的拼音 """ - # 检查拼音是否有效 + # 检查拼音是否为空或无效 if not py or len(py) < 1: return py - # 如果拼音末尾不是数字(如轻声),则默认添加一声 + # 如果最后一个字符不是数字,说明可能是轻声或其他特殊情况 if not py[-1].isdigit(): + # 为非数字结尾的拼音添加数字声调1 return f"{py}1" - base = py[:-1] # 拼音的基本部分 (e.g., 'hao') - tone = int(py[-1]) # 声调 (e.g., 3) + base = py[:-1] # 去掉声调 + tone = int(py[-1]) # 获取声调 # 处理轻声(通常用5表示)或无效声调 if tone not in [1, 2, 3, 4]: @@ -185,56 +155,40 @@ class ChineseTypoGenerator: def _calculate_replacement_probability(self, orig_freq, target_freq): """ - 根据原始字和目标替换字的频率差异,计算替换概率。 - 频率相近的字有更高的替换概率。 - - Args: - orig_freq (float): 原始字的频率。 - target_freq (float): 目标替换字的频率。 - - Returns: - float: 替换概率,介于 0.0 和 1.0 之间。 + 根据频率差计算替换概率 """ - # 如果目标字更常用,则替换概率为1 if target_freq > orig_freq: - return 1.0 + return 1.0 # 如果替换字频率更高,保持原有概率 freq_diff = orig_freq - target_freq - # 如果频率差异过大,则不进行替换 if freq_diff > self.max_freq_diff: - return 0.0 + return 0.0 # 频率差太大,不替换 - # 使用指数衰减函数来计算概率,频率差异越大,概率越低 + # 使用指数衰减函数计算概率 + # 频率差为0时概率为1,频率差为max_freq_diff时概率接近0 return math.exp(-3 * freq_diff / self.max_freq_diff) def _get_similar_frequency_chars(self, char, py, num_candidates=5): """ - 获取与给定汉字发音相似且频率相近的候选替换字。 - - Args: - char (str): 原始汉字。 - py (str): 原始汉字的拼音。 - num_candidates (int): 返回的候选字数量。 - - Returns: - list or None: 一个包含候选替换字的列表,如果没有找到则返回 None。 + 获取与给定字频率相近的同音字,可能包含声调错误 """ homophones = [] - # 根据设定概率,可能使用声调错误的拼音来寻找候选字 + # 有一定概率使用错误声调 if random.random() < self.tone_error_rate: wrong_tone_py = self._get_similar_tone_pinyin(py) - homophones.extend(self.pinyin_dict.get(wrong_tone_py, [])) + homophones.extend(self.pinyin_dict[wrong_tone_py]) - # 添加声调正确的同音字 - homophones.extend(self.pinyin_dict.get(py, [])) + # 添加正确声调的同音字 + homophones.extend(self.pinyin_dict[py]) if not homophones: return None + # 获取原字的频率 orig_freq = self.char_frequency.get(char, 0) - # 过滤掉低频字和原始字本身 + # 计算所有同音字与原字的频率差,并过滤掉低频字 freq_diff = [ (h, self.char_frequency.get(h, 0)) for h in homophones @@ -248,215 +202,222 @@ class ChineseTypoGenerator: candidates_with_prob = [] for h, freq in freq_diff: prob = self._calculate_replacement_probability(orig_freq, freq) - if prob > 0: + if prob > 0: # 只保留有效概率的候选字 candidates_with_prob.append((h, prob)) if not candidates_with_prob: return None - # 根据替换概率从高到低排序 - candidates_with_prob.sort(key=lambda x: x, reverse=True) + # 根据概率排序 + candidates_with_prob.sort(key=lambda x: x[1], reverse=True) - # 返回概率最高的几个候选字 - return [c for c, _ in candidates_with_prob[:num_candidates]] + # 返回概率最高的几个字 + return [char for char, _ in candidates_with_prob[:num_candidates]] @staticmethod def _get_word_pinyin(word): """ - 获取一个词语中每个汉字的拼音列表。 - - Args: - word (str): 输入的词语。 - - Returns: - list: 包含每个汉字拼音的列表。 + 获取词语的拼音列表 """ - return [py for py in pinyin(word, style=Style.TONE3)] + return [py[0] for py in pinyin(word, style=Style.TONE3)] @staticmethod def _segment_sentence(sentence): """ - 使用 jieba 对句子进行分词。 - - Args: - sentence (str): 输入的句子。 - - Returns: - list: 分词后的词语列表。 + 使用jieba分词,返回词语列表 """ return list(jieba.cut(sentence)) def _get_word_homophones(self, word): """ - 获取一个词语的同音词。 - 只返回在jieba词典中存在且频率较高的有意义词语。 - - Args: - word (str): 原始词语。 - - Returns: - list: 一个包含同音词的列表。 + 获取整个词的同音词,只返回高频的有意义词语 """ - if len(word) <= 1: + if len(word) == 1: return [] + # 获取词的拼音 word_pinyin = self._get_word_pinyin(word) - # 为词语中的每个字找到所有同音字 + # 遍历所有可能的同音字组合 candidates = [] for py in word_pinyin: chars = self.pinyin_dict.get(py, []) if not chars: - return [] # 如果某个字没有同音字,则无法构成同音词 + return [] candidates.append(chars) - # 生成所有可能的同音字组合 + # 生成所有可能的组合 import itertools all_combinations = itertools.product(*candidates) - # 加载jieba词典以验证组合出的词是否为有效词语 + # 获取jieba词典和词频信息 dict_path = os.path.join(os.path.dirname(jieba.__file__), "dict.txt") - valid_words = {} + valid_words = {} # 改用字典存储词语及其频率 with open(dict_path, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split() if len(parts) >= 2: - valid_words[parts] = float(parts[0][1]) + word_text = parts[0] + word_freq = float(parts[1]) # 获取词频 + valid_words[word_text] = word_freq + # 获取原词的词频作为参考 original_word_freq = valid_words.get(word, 0) - # 设置一个最小词频阈值,过滤掉非常生僻的词 - min_word_freq = original_word_freq * 0.1 + min_word_freq = original_word_freq * 0.1 # 设置最小词频为原词频的10% + # 过滤和计算频率 homophones = [] for combo in all_combinations: new_word = "".join(combo) - # 检查新词是否为有效词语且与原词不同 if new_word != word and new_word in valid_words: new_word_freq = valid_words[new_word] + # 只保留词频达到阈值的词 if new_word_freq >= min_word_freq: - # 计算综合评分,结合词频和平均字频 + # 计算词的平均字频(考虑字频和词频) char_avg_freq = sum(self.char_frequency.get(c, 0) for c in new_word) / len(new_word) + # 综合评分:结合词频和字频 combined_score = new_word_freq * 0.7 + char_avg_freq * 0.3 if combined_score >= self.min_freq: homophones.append((new_word, combined_score)) - # 按综合分数排序并返回前5个结果 - sorted_homophones = sorted(homophones, key=lambda x: x, reverse=True) - return [w for w, _ in sorted_homophones[:5]] + # 按综合分数排序并限制返回数量 + sorted_homophones = sorted(homophones, key=lambda x: x[1], reverse=True) + return [word for word, _ in sorted_homophones[:5]] # 限制返回前5个结果 def create_typo_sentence(self, sentence): """ - 为输入句子生成一个包含错别字的版本。 - 该方法会先对句子进行分词,然后根据概率进行整词替换或单字替换。 + 创建包含同音字错误的句子,支持词语级别和字级别的替换 - Args: - sentence (str): 原始中文句子。 + 参数: + sentence: 输入的中文句子 - Returns: - tuple: 包含三个元素的元组: - - original_sentence (str): 原始句子。 - - typo_sentence (str): 包含错别字的句子。 - - correction_suggestion (str or None): 一个随机的修正建议(可能是正确的字或词),或 None。 + 返回: + typo_sentence: 包含错别字的句子 + correction_suggestion: 随机选择的一个纠正建议,返回正确的字/词 """ result = [] - typo_info = [] # 用于调试,记录详细的替换信息 - word_typos = [] # 记录 (错词, 正确词) - char_typos = [] # 记录 (错字, 正确字) + typo_info = [] + word_typos = [] # 记录词语错误对(错词,正确词) + char_typos = [] # 记录单字错误对(错字,正确字) + current_pos = 0 - # 对句子进行分词 + # 分词 words = self._segment_sentence(sentence) for word in words: - # 如果是标点符号或非中文字符,直接保留 + # 如果是标点符号或空格,直接添加 if all(not self._is_chinese_char(c) for c in word): result.append(word) + current_pos += len(word) continue + # 获取词语的拼音 word_pinyin = self._get_word_pinyin(word) - # 步骤1: 尝试进行整词替换 + # 尝试整词替换 if len(word) > 1 and random.random() < self.word_replace_rate: word_homophones = self._get_word_homophones(word) if word_homophones: typo_word = random.choice(word_homophones) + # 计算词的平均频率 orig_freq = sum(self.char_frequency.get(c, 0) for c in word) / len(word) typo_freq = sum(self.char_frequency.get(c, 0) for c in typo_word) / len(typo_word) + # 添加到结果中 result.append(typo_word) typo_info.append( ( word, typo_word, - " ".join(self._get_word_pinyin(word)), + " ".join(word_pinyin), " ".join(self._get_word_pinyin(typo_word)), orig_freq, typo_freq, ) ) - word_typos.append((typo_word, word)) + word_typos.append((typo_word, word)) # 记录(错词,正确词)对 + current_pos += len(typo_word) continue - # 步骤2: 如果不进行整词替换,则对词中的每个字进行单字替换 - new_word = [] - for char, py in zip(word, word_pinyin, strict=False): - # 词语越长,其中单个字被替换的概率越低 - char_error_rate = self.error_rate * (0.7 ** (len(word) - 1)) - if random.random() < char_error_rate: + # 如果不进行整词替换,则进行单字替换 + if len(word) == 1: + char = word + py = word_pinyin[0] + if random.random() < self.error_rate: similar_chars = self._get_similar_frequency_chars(char, py) if similar_chars: typo_char = random.choice(similar_chars) - orig_freq = self.char_frequency.get(char, 0) typo_freq = self.char_frequency.get(typo_char, 0) - # 根据频率计算最终是否替换 - if random.random() < self._calculate_replacement_probability(orig_freq, typo_freq): - new_word.append(typo_char) - typo_py = pinyin(typo_char, style=Style.TONE3) + orig_freq = self.char_frequency.get(char, 0) + replace_prob = self._calculate_replacement_probability(orig_freq, typo_freq) + if random.random() < replace_prob: + result.append(typo_char) + typo_py = pinyin(typo_char, style=Style.TONE3)[0][0] typo_info.append((char, typo_char, py, typo_py, orig_freq, typo_freq)) - char_typos.append((typo_char, char)) + char_typos.append((typo_char, char)) # 记录(错字,正确字)对 + current_pos += 1 continue - # 如果不替换,则保留原字 - new_word.append(char) + result.append(char) + current_pos += 1 + else: + # 处理多字词的单字替换 + word_result = [] + for _, (char, py) in enumerate(zip(word, word_pinyin, strict=False)): + # 词中的字替换概率降低 + word_error_rate = self.error_rate * (0.7 ** (len(word) - 1)) - result.append("".join(new_word)) + if random.random() < word_error_rate: + similar_chars = self._get_similar_frequency_chars(char, py) + if similar_chars: + typo_char = random.choice(similar_chars) + typo_freq = self.char_frequency.get(typo_char, 0) + orig_freq = self.char_frequency.get(char, 0) + replace_prob = self._calculate_replacement_probability(orig_freq, typo_freq) + if random.random() < replace_prob: + word_result.append(typo_char) + typo_py = pinyin(typo_char, style=Style.TONE3)[0][0] + typo_info.append((char, typo_char, py, typo_py, orig_freq, typo_freq)) + char_typos.append((typo_char, char)) # 记录(错字,正确字)对 + continue + word_result.append(char) + result.append("".join(word_result)) + current_pos += len(word) - # 步骤3: 生成修正建议 + # 优先从词语错误中选择,如果没有则从单字错误中选择 correction_suggestion = None - # 有50%的概率提供一个修正建议 + # 50%概率返回纠正建议 if random.random() < 0.5: - # 优先从整词错误中选择 if word_typos: - _, correct_word = random.choice(word_typos) + wrong_word, correct_word = random.choice(word_typos) correction_suggestion = correct_word - # 其次从单字错误中选择 elif char_typos: - _, correct_char = random.choice(char_typos) + wrong_char, correct_char = random.choice(char_typos) correction_suggestion = correct_char - return sentence, "".join(result), correction_suggestion + return "".join(result), correction_suggestion @staticmethod def format_typo_info(typo_info): """ - 将错别字生成过程中的详细信息格式化为可读字符串。 + 格式化错别字信息 - Args: - typo_info (list): `create_typo_sentence` 方法生成的详细信息列表。 + 参数: + typo_info: 错别字信息列表 - Returns: - str: 格式化后的字符串,用于调试和分析。 + 返回: + 格式化后的错别字信息字符串 """ if not typo_info: return "未生成错别字" result = [] for orig, typo, orig_py, typo_py, orig_freq, typo_freq in typo_info: - # 判断是整词替换还是单字替换 + # 判断是否为词语替换 is_word = " " in orig_py if is_word: error_type = "整词替换" else: - # 判断是声调错误还是同音字替换 tone_error = orig_py[:-1] == typo_py[:-1] and orig_py[-1] != typo_py[-1] error_type = "声调错误" if tone_error else "同音字替换" @@ -469,22 +430,21 @@ class ChineseTypoGenerator: def set_params(self, **kwargs): """ - 动态设置生成器的参数。 + 设置参数 - Args: - **kwargs: 键值对参数,可设置的参数包括: - - error_rate (float) - - min_freq (int) - - tone_error_rate (float) - - word_replace_rate (float) - - max_freq_diff (int) + 可设置参数: + error_rate: 单字替换概率 + min_freq: 最小字频阈值 + tone_error_rate: 声调错误概率 + word_replace_rate: 整词替换概率 + max_freq_diff: 最大允许的频率差异 """ for key, value in kwargs.items(): if hasattr(self, key): setattr(self, key, value) - logger.info(f"参数 {key} 已更新为 {value}") + print(f"参数 {key} 已设置为 {value}") else: - logger.warning(f"尝试设置不存在的参数: {key}") + print(f"警告: 参数 {key} 不存在") def main(): @@ -496,10 +456,10 @@ def main(): # 创建包含错别字的句子 start_time = time.time() - original_sentence, typo_sentence, correction_suggestion = typo_generator.create_typo_sentence(sentence) + typo_sentence, correction_suggestion = typo_generator.create_typo_sentence(sentence) # 打印结果 - print("\n原句:", original_sentence) + print("\n原句:", sentence) print("错字版:", typo_sentence) # 打印纠正建议 diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index d0aa0b6c5..501bf382d 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -293,11 +293,9 @@ def random_remove_punctuation(text: str) -> str: return result -def process_llm_response( - text: str, enable_splitter: bool = True, enable_chinese_typo: bool = True -) -> list[dict[str, str]]: +def process_llm_response(text: str, enable_splitter: bool = True, enable_chinese_typo: bool = True) -> list[str]: if not global_config.response_post_process.enable_response_post_process: - return [{"type": "text", "content": text}] + return [text] # 先保护颜文字 if global_config.response_splitter.enable_kaomoji_protection: @@ -313,7 +311,7 @@ def process_llm_response( cleaned_text = pattern.sub("", protected_text) if cleaned_text == "": - return [{"type": "text", "content": "呃呃"}] + return ["呃呃"] logger.debug(f"{text}去除括号处理后的文本: {cleaned_text}") @@ -323,7 +321,7 @@ def process_llm_response( # 如果基本上是中文,则进行长度过滤 if get_western_ratio(cleaned_text) < 0.1 and len(cleaned_text) > max_length: logger.warning(f"回复过长 ({len(cleaned_text)} 字符),返回默认回复") - return [{"type": "text", "content": "懒得说"}] + return ["懒得说"] typo_generator = ChineseTypoGenerator( error_rate=global_config.chinese_typo.error_rate, @@ -340,24 +338,16 @@ def process_llm_response( sentences = [] for sentence in split_sentences: if global_config.chinese_typo.enable and enable_chinese_typo: - original_sentence, typo_sentence, typo_corrections = typo_generator.create_typo_sentence(sentence) + typoed_text, typo_corrections = typo_generator.create_typo_sentence(sentence) + sentences.append(typoed_text) if typo_corrections: - sentences.append( - { - "type": "typo", - "original": original_sentence, - "typo": typo_sentence, - "correction": typo_corrections, - } - ) - else: - sentences.append({"type": "text", "content": sentence}) + sentences.append(typo_corrections) else: - sentences.append({"type": "text", "content": sentence}) + sentences.append(sentence) if len(sentences) > max_sentence_num: logger.warning(f"分割后消息数量过多 ({len(sentences)} 条),返回默认回复") - return [{"type": "text", "content": f"{global_config.bot.nickname}不知道哦"}] + return [f"{global_config.bot.nickname}不知道哦"] # if extracted_contents: # for content in extracted_contents: @@ -365,20 +355,7 @@ def process_llm_response( # 在所有句子处理完毕后,对包含占位符的列表进行恢复 if global_config.response_splitter.enable_kaomoji_protection: - # sentences中的元素可能是dict,也可能是str,所以要分开处理 - recovered_sentences = [] - for s in sentences: - if isinstance(s, dict) and s.get("type") == "typo": - s["original"] = recover_kaomoji(s["original"], kaomoji_mapping) - s["typo"] = recover_kaomoji(s["typo"], kaomoji_mapping) - s["correction"] = recover_kaomoji(s["correction"], kaomoji_mapping) - recovered_sentences.append(s) - elif isinstance(s, dict) and s.get("type") == "text": - s["content"] = recover_kaomoji(s["content"], kaomoji_mapping) - recovered_sentences.append(s) - else: - recovered_sentences.append(recover_kaomoji([s], kaomoji_mapping)) - sentences = recovered_sentences + sentences = recover_kaomoji(sentences, kaomoji_mapping) return sentences diff --git a/src/plugin_system/apis/generator_api.py b/src/plugin_system/apis/generator_api.py index 8d8d04eb3..5ffae7298 100644 --- a/src/plugin_system/apis/generator_api.py +++ b/src/plugin_system/apis/generator_api.py @@ -84,7 +84,7 @@ async def generate_reply( return_prompt: bool = False, request_type: str = "generator_api", from_plugin: bool = True, -) -> Tuple[bool, List[Dict[str, Any]], Optional[str]]: +) -> Tuple[bool, List[Tuple[str, Any]], Optional[str]]: """生成回复 Args: @@ -170,7 +170,7 @@ async def rewrite_reply( reply_to: str = "", return_prompt: bool = False, request_type: str = "generator_api", -) -> Tuple[bool, List[Dict[str, Any]], Optional[str]]: +) -> Tuple[bool, List[Tuple[str, Any]], Optional[str]]: """重写回复 Args: @@ -229,9 +229,7 @@ async def rewrite_reply( return False, [], None -def process_human_text( - content: str, enable_splitter: bool, enable_chinese_typo: bool -) -> List[Dict[str, Any]]: +def process_human_text(content: str, enable_splitter: bool, enable_chinese_typo: bool) -> List[Tuple[str, Any]]: """将文本处理为更拟人化的文本 Args: @@ -248,11 +246,9 @@ def process_human_text( processed_response = process_llm_response(content, enable_splitter, enable_chinese_typo) reply_set = [] - for item in processed_response: - if item["type"] == "typo": - reply_set.append(item) - else: - reply_set.append({"type": "text", "content": item["content"]}) + for text in processed_response: + reply_seg = ("text", text) + reply_set.append(reply_seg) return reply_set diff --git a/src/plugin_system/apis/send_api.py b/src/plugin_system/apis/send_api.py index 565bc9f25..334308795 100644 --- a/src/plugin_system/apis/send_api.py +++ b/src/plugin_system/apis/send_api.py @@ -179,7 +179,7 @@ async def _send_to_target( # 构建机器人用户信息 bot_user_info = UserInfo( - user_id=str(global_config.bot.qq_account), + user_id=global_config.bot.qq_account, user_nickname=global_config.bot.nickname, platform=target_stream.platform, ) @@ -190,13 +190,10 @@ async def _send_to_target( # 处理回复消息 if reply_to_message: anchor_message = message_dict_to_message_recv(message_dict=reply_to_message) - if anchor_message and anchor_message.message_info and anchor_message.message_info.user_info: - anchor_message.update_chat_stream(target_stream) - reply_to_platform_id = ( - f"{anchor_message.message_info.platform}:{anchor_message.message_info.user_info.user_id}" - ) - else: - reply_to_platform_id = None + anchor_message.update_chat_stream(target_stream) + reply_to_platform_id = ( + f"{anchor_message.message_info.platform}:{anchor_message.message_info.user_info.user_id}" + ) else: anchor_message = None reply_to_platform_id = None @@ -426,10 +423,10 @@ async def adapter_command_to_stream( # 创建临时的用户信息和聊天流 - temp_user_info = UserInfo(user_id="system", user_nickname="System", platform=platform or "qq") + temp_user_info = UserInfo(user_id="system", user_nickname="System", platform=platform) temp_chat_stream = ChatStream( - stream_id=stream_id, platform=platform or "qq", user_info=temp_user_info, group_info=None + stream_id=stream_id, platform=platform, user_info=temp_user_info, group_info=None ) target_stream = temp_chat_stream @@ -446,7 +443,7 @@ async def adapter_command_to_stream( # 构建机器人用户信息 bot_user_info = UserInfo( - user_id=str(global_config.bot.qq_account), + user_id=global_config.bot.qq_account, user_nickname=global_config.bot.nickname, platform=target_stream.platform, ) @@ -499,23 +496,3 @@ async def adapter_command_to_stream( logger.error(f"[SendAPI] 发送适配器命令时出错: {e}") traceback.print_exc() return {"status": "error", "message": f"发送适配器命令时出错: {str(e)}"} - - -async def recall_message(message_id: str, stream_id: str) -> bool: - """撤回消息 - - Args: - message_id: 消息ID - stream_id: 聊天流ID - - Returns: - bool: 是否成功 - """ - command_data = {"name": "delete_msg", "args": message_id} - - success = await command_to_stream( - command=command_data, - stream_id=stream_id, - storage_message=True, - ) - return success \ No newline at end of file From ef41f4eaf7a7abd98c6c6af587871b5d22217b57 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 20:58:49 +0800 Subject: [PATCH 45/69] =?UTF-8?q?feat(plugin=5Fsystem):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0ON=5FSTOP=E4=BA=8B=E4=BB=B6=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为插件系统增加ON_STOP事件,用于处理程序停止时的任务。 --- src/plugin_system/base/component_types.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index f740d0c96..5737b4fb6 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -80,6 +80,7 @@ class EventType(Enum): """ ON_START = "on_start" # 启动事件,用于调用按时任务 + ON_STOP ="on_stop" ON_MESSAGE = "on_message" ON_PLAN = "on_plan" POST_LLM = "post_llm" From 62dfc35682bfb369ab8bd35889bc949af9dd3c0c Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sat, 6 Sep 2025 21:24:17 +0800 Subject: [PATCH 46/69] =?UTF-8?q?napcat=E6=8F=92=E4=BB=B6=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E8=BF=81=E7=A7=BB=E5=9F=BA=E6=9C=AC=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../built_in/napcat_adapter_plugin/plugin.py | 43 +++- .../src/config/__init__.py | 2 - .../src/config/config_base.py | 136 ----------- .../src/config/config_utils.py | 145 ------------ .../src/config/migrate_features.py | 215 ------------------ .../src/config/official_configs.py | 74 ------ .../src/recv_handler/message_handler.py | 43 +++- 7 files changed, 66 insertions(+), 592 deletions(-) delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/config/__init__.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/config/config_base.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/config/config_utils.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/config/migrate_features.py delete mode 100644 src/plugins/built_in/napcat_adapter_plugin/src/config/official_configs.py diff --git a/src/plugins/built_in/napcat_adapter_plugin/plugin.py b/src/plugins/built_in/napcat_adapter_plugin/plugin.py index 4f7c1ee8e..39082d0f3 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/plugin.py +++ b/src/plugins/built_in/napcat_adapter_plugin/plugin.py @@ -18,7 +18,6 @@ from .src.recv_handler.meta_event_handler import meta_event_handler from .src.recv_handler.notice_handler import notice_handler from .src.recv_handler.message_sending import message_send_instance from .src.send_handler import send_handler -from .src.config.migrate_features import auto_migrate_features from .src.mmc_com_layer import mmc_start_com, router, mmc_stop_com from .src.response_pool import put_response, check_timeout_response from .src.websocket_manager import websocket_manager @@ -221,16 +220,10 @@ class LauchNapcatAdapterHandler(BaseEventHandler): init_subscribe = [EventType.ON_START] async def execute(self, kwargs): - # 执行功能配置迁移(如果需要) - logger.info("检查功能配置迁移...") - auto_migrate_features() - # 启动消息重组器的清理任务 logger.info("启动消息重组器...") await reassembler.start_cleanup_task() - # 功能管理器已迁移到插件系统配置 - logger.info("功能配置已迁移到插件系统") logger.info("开始启动Napcat Adapter") message_send_instance.maibot_router = router # 创建单独的异步任务,防止阻塞主线程 @@ -270,7 +263,7 @@ class NapcatAdapterPlugin(BasePlugin): "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.2.0", description="配置文件版本"), + "config_version": ConfigField(type=str, default="1.3.0", description="配置文件版本"), "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), }, "inner": { @@ -301,6 +294,37 @@ class NapcatAdapterPlugin(BasePlugin): }, "debug": { "level": ConfigField(type=str, default="INFO", description="日志等级(DEBUG, INFO, WARNING, ERROR, CRITICAL)", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), + }, + "features": { + # 权限设置 + "group_list_type": ConfigField(type=str, default="blacklist", description="群聊列表类型:whitelist(白名单)或 blacklist(黑名单)", choices=["whitelist", "blacklist"]), + "group_list": ConfigField(type=list, default=[], description="群聊ID列表"), + "private_list_type": ConfigField(type=str, default="blacklist", description="私聊列表类型:whitelist(白名单)或 blacklist(黑名单)", choices=["whitelist", "blacklist"]), + "private_list": ConfigField(type=list, default=[], description="用户ID列表"), + "ban_user_id": ConfigField(type=list, default=[], description="全局禁止用户ID列表,这些用户无法在任何地方使用机器人"), + "ban_qq_bot": ConfigField(type=bool, default=False, description="是否屏蔽QQ官方机器人消息"), + + # 聊天功能设置 + "enable_poke": ConfigField(type=bool, default=True, description="是否启用戳一戳功能"), + "ignore_non_self_poke": ConfigField(type=bool, default=False, description="是否无视不是针对自己的戳一戳"), + "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_video_analysis": ConfigField(type=bool, default=True, description="是否启用视频识别功能"), + "max_video_size_mb": ConfigField(type=int, default=100, description="视频文件最大大小限制(MB)"), + "download_timeout": ConfigField(type=int, default=60, description="视频下载超时时间(秒)"), + "supported_formats": ConfigField(type=list, default=["mp4", "avi", "mov", "mkv", "flv", "wmv", "webm"], description="支持的视频格式"), + + # 消息缓冲设置 + "enable_message_buffer": ConfigField(type=bool, default=True, description="是否启用消息缓冲合并功能"), + "message_buffer_enable_group": ConfigField(type=bool, default=True, description="是否启用群聊消息缓冲合并"), + "message_buffer_enable_private": ConfigField(type=bool, default=True, description="是否启用私聊消息缓冲合并"), + "message_buffer_interval": ConfigField(type=float, default=3.0, description="消息合并间隔时间(秒),在此时间内的连续消息将被合并"), + "message_buffer_initial_delay": ConfigField(type=float, default=0.5, description="消息缓冲初始延迟(秒),收到第一条消息后等待此时间开始合并"), + "message_buffer_max_components": ConfigField(type=int, default=50, description="单个会话最大缓冲消息组件数量,超过此数量将强制合并"), + "message_buffer_block_prefixes": ConfigField(type=list, default=["/", "!", "!", ".", "。", "#", "%"], description="消息缓冲屏蔽前缀,以这些前缀开头的消息不会被缓冲"), } } @@ -313,7 +337,8 @@ class NapcatAdapterPlugin(BasePlugin): "maibot_server": "连接麦麦的ws服务设置", "voice": "发送语音设置", "slicing": "WebSocket消息切片设置", - "debug": "调试设置" + "debug": "调试设置", + "features": "功能设置(权限控制、聊天功能、视频处理、消息缓冲等)" } def register_events(self): diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/config/__init__.py b/src/plugins/built_in/napcat_adapter_plugin/src/config/__init__.py deleted file mode 100644 index 99c6f490c..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/config/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# 配置已迁移到插件系统,此文件不再需要 -# 所有配置访问应通过插件系统的 config_api 进行 diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/config/config_base.py b/src/plugins/built_in/napcat_adapter_plugin/src/config/config_base.py deleted file mode 100644 index 87cb079d2..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/config/config_base.py +++ /dev/null @@ -1,136 +0,0 @@ -from dataclasses import dataclass, fields, MISSING -from typing import TypeVar, Type, Any, get_origin, get_args, Literal, Dict, Union - -T = TypeVar("T", bound="ConfigBase") - -TOML_DICT_TYPE = { - int, - float, - str, - bool, - list, - dict, -} - - -@dataclass -class ConfigBase: - """配置类的基类""" - - @classmethod - def from_dict(cls: Type[T], data: Dict[str, Any]) -> T: - """从字典加载配置字段""" - if not isinstance(data, dict): - raise TypeError(f"Expected a dictionary, got {type(data).__name__}") - - init_args: Dict[str, Any] = {} - - for f in fields(cls): - field_name = f.name - field_type = f.type - if field_name.startswith("_"): - # 跳过以 _ 开头的字段 - continue - - if field_name not in data: - if f.default is not MISSING or f.default_factory is not MISSING: - # 跳过未提供且有默认值/默认构造方法的字段 - continue - else: - raise ValueError(f"Missing required field: '{field_name}'") - - value = data[field_name] - try: - init_args[field_name] = cls._convert_field(value, field_type) - except TypeError as e: - raise TypeError(f"字段 '{field_name}' 出现类型错误: {e}") from e - except Exception as e: - raise RuntimeError(f"无法将字段 '{field_name}' 转换为目标类型,出现错误: {e}") from e - - return cls(**init_args) - - @classmethod - def _convert_field(cls, value: Any, field_type: Type[Any]) -> Any: - """ - 转换字段值为指定类型 - - 1. 对于嵌套的 dataclass,递归调用相应的 from_dict 方法 - 2. 对于泛型集合类型(list, set, tuple),递归转换每个元素 - 3. 对于基础类型(int, str, float, bool),直接转换 - 4. 对于其他类型,尝试直接转换,如果失败则抛出异常 - """ - # 如果是嵌套的 dataclass,递归调用 from_dict 方法 - if isinstance(field_type, type) and issubclass(field_type, ConfigBase): - return field_type.from_dict(value) - - field_origin_type = get_origin(field_type) - field_args_type = get_args(field_type) - - # 处理泛型集合类型(list, set, tuple) - if field_origin_type in {list, set, tuple}: - # 检查提供的value是否为list - if not isinstance(value, list): - raise TypeError(f"Expected an list for {field_type.__name__}, got {type(value).__name__}") - - if field_origin_type is list: - return [cls._convert_field(item, field_args_type[0]) for item in value] - if field_origin_type is set: - return {cls._convert_field(item, field_args_type[0]) for item in value} - if field_origin_type is tuple: - # 检查提供的value长度是否与类型参数一致 - if len(value) != len(field_args_type): - raise TypeError( - f"Expected {len(field_args_type)} items for {field_type.__name__}, got {len(value)}" - ) - return tuple(cls._convert_field(item, arg_type) for item, arg_type in zip(value, field_args_type)) - - if field_origin_type is dict: - # 检查提供的value是否为dict - if not isinstance(value, dict): - raise TypeError(f"Expected a dictionary for {field_type.__name__}, got {type(value).__name__}") - - # 检查字典的键值类型 - if len(field_args_type) != 2: - raise TypeError(f"Expected a dictionary with two type arguments for {field_type.__name__}") - key_type, value_type = field_args_type - - return {cls._convert_field(k, key_type): cls._convert_field(v, value_type) for k, v in value.items()} - - # 处理Optional类型 - if field_origin_type is Union: # assert get_origin(Optional[Any]) is Union - if value is None: - return None - # 如果有数据,检查实际类型 - if type(value) not in field_args_type: - raise TypeError(f"Expected {field_args_type} for {field_type.__name__}, got {type(value).__name__}") - return cls._convert_field(value, field_args_type[0]) - - # 处理int, str, float, bool等基础类型 - if field_origin_type is None: - if isinstance(value, field_type): - return field_type(value) - else: - raise TypeError(f"Expected {field_type.__name__}, got {type(value).__name__}") - - # 处理Literal类型 - if field_origin_type is Literal: - # 获取Literal的允许值 - allowed_values = get_args(field_type) - if value in allowed_values: - return value - else: - raise TypeError(f"Value '{value}' is not in allowed values {allowed_values} for Literal type") - - # 处理其他类型 - if field_type is Any: - return value - - # 其他类型直接转换 - try: - return field_type(value) - except (ValueError, TypeError) as e: - raise TypeError(f"无法将 {type(value).__name__} 转换为 {field_type.__name__}") from e - - def __str__(self): - """返回配置类的字符串表示""" - return f"{self.__class__.__name__}({', '.join(f'{f.name}={getattr(self, f.name)}' for f in fields(self))})" diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/config/config_utils.py b/src/plugins/built_in/napcat_adapter_plugin/src/config/config_utils.py deleted file mode 100644 index a275b3078..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/config/config_utils.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -配置文件工具模块 -提供统一的配置文件生成和管理功能 -""" - -import os -import shutil -from pathlib import Path -from datetime import datetime -from typing import Optional - -from src.common.logger import get_logger - -logger = get_logger("napcat_adapter") - - -def ensure_config_directories(): - """确保配置目录存在""" - os.makedirs("config", exist_ok=True) - os.makedirs("config/old", exist_ok=True) - - -def create_config_from_template( - config_path: str, template_path: str, config_name: str = "配置文件", should_exit: bool = True -) -> bool: - """ - 从模板创建配置文件的统一函数 - - Args: - config_path: 配置文件路径 - template_path: 模板文件路径 - config_name: 配置文件名称(用于日志显示) - should_exit: 创建后是否退出程序 - - Returns: - bool: 是否成功创建配置文件 - """ - try: - # 确保配置目录存在 - ensure_config_directories() - - config_path_obj = Path(config_path) - template_path_obj = Path(template_path) - - # 检查配置文件是否存在 - if config_path_obj.exists(): - return False # 配置文件已存在,无需创建 - - logger.info(f"{config_name}不存在,从模板创建新配置") - - # 检查模板文件是否存在 - if not template_path_obj.exists(): - logger.error(f"模板文件不存在: {template_path}") - if should_exit: - logger.critical("无法创建配置文件,程序退出") - quit(1) - return False - - # 确保配置文件目录存在 - config_path_obj.parent.mkdir(parents=True, exist_ok=True) - - # 复制模板文件到配置目录 - shutil.copy2(template_path_obj, config_path_obj) - logger.info(f"已创建新{config_name}: {config_path}") - - if should_exit: - logger.info("程序将退出,请检查配置文件后重启") - quit(0) - - return True - - except Exception as e: - logger.error(f"创建{config_name}失败: {e}") - if should_exit: - logger.critical("无法创建配置文件,程序退出") - quit(1) - return False - - -def create_default_config_dict(default_values: dict, config_path: str, config_name: str = "配置文件") -> bool: - """ - 创建默认配置文件(使用字典数据) - - Args: - default_values: 默认配置值字典 - config_path: 配置文件路径 - config_name: 配置文件名称(用于日志显示) - - Returns: - bool: 是否成功创建配置文件 - """ - try: - import tomlkit - - config_path_obj = Path(config_path) - - # 确保配置文件目录存在 - config_path_obj.parent.mkdir(parents=True, exist_ok=True) - - # 写入默认配置 - with open(config_path_obj, "w", encoding="utf-8") as f: - tomlkit.dump(default_values, f) - - logger.info(f"已创建默认{config_name}: {config_path}") - return True - - except Exception as e: - logger.error(f"创建默认{config_name}失败: {e}") - return False - - -def backup_config_file(config_path: str, backup_dir: str = "config/old") -> Optional[str]: - """ - 备份配置文件 - - Args: - config_path: 要备份的配置文件路径 - backup_dir: 备份目录 - - Returns: - Optional[str]: 备份文件路径,失败时返回None - """ - try: - config_path_obj = Path(config_path) - if not config_path_obj.exists(): - return None - - # 确保备份目录存在 - backup_dir_obj = Path(backup_dir) - backup_dir_obj.mkdir(parents=True, exist_ok=True) - - # 创建备份文件名 - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - backup_filename = f"{config_path_obj.stem}.toml.bak.{timestamp}" - backup_path = backup_dir_obj / backup_filename - - # 备份文件 - shutil.copy2(config_path_obj, backup_path) - logger.info(f"已备份配置文件到: {backup_path}") - - return str(backup_path) - - except Exception as e: - logger.error(f"备份配置文件失败: {e}") - return None diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/config/migrate_features.py b/src/plugins/built_in/napcat_adapter_plugin/src/config/migrate_features.py deleted file mode 100644 index e721029c0..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/config/migrate_features.py +++ /dev/null @@ -1,215 +0,0 @@ -""" -功能配置迁移脚本 -用于将旧的配置文件中的聊天、权限、视频处理等设置迁移到新的独立功能配置文件 -""" - -import os -import shutil -from pathlib import Path -import tomlkit -from src.common.logger import get_logger - -logger = get_logger("napcat_adapter") - - -def migrate_features_from_config( - old_config_path: str = "plugins/napcat_adapter_plugin/config/config.toml", - new_features_path: str = "plugins/napcat_adapter_plugin/config/features.toml", - template_path: str = "plugins/napcat_adapter_plugin/template/features_template.toml", -): - """ - 从旧配置文件迁移功能设置到新的功能配置文件 - - Args: - old_config_path: 旧配置文件路径 - new_features_path: 新功能配置文件路径 - template_path: 功能配置模板路径 - """ - try: - # 检查旧配置文件是否存在 - if not os.path.exists(old_config_path): - logger.warning(f"旧配置文件不存在: {old_config_path}") - return False - - # 读取旧配置文件 - with open(old_config_path, "r", encoding="utf-8") as f: - old_config = tomlkit.load(f) - - # 检查是否有chat配置段和video配置段 - chat_config = old_config.get("chat", {}) - video_config = old_config.get("video", {}) - - # 检查是否有权限相关配置 - permission_keys = [ - "group_list_type", - "group_list", - "private_list_type", - "private_list", - "ban_user_id", - "ban_qq_bot", - "enable_poke", - "ignore_non_self_poke", - "poke_debounce_seconds", - ] - video_keys = ["enable_video_analysis", "max_video_size_mb", "download_timeout", "supported_formats"] - - has_permission_config = any(key in chat_config for key in permission_keys) - has_video_config = any(key in video_config for key in video_keys) - - if not has_permission_config and not has_video_config: - logger.info("旧配置文件中没有找到功能相关配置,无需迁移") - return False - - # 确保新功能配置目录存在 - new_features_dir = Path(new_features_path).parent - new_features_dir.mkdir(parents=True, exist_ok=True) - - # 如果新功能配置文件已存在,先备份 - if os.path.exists(new_features_path): - backup_path = f"{new_features_path}.backup" - shutil.copy2(new_features_path, backup_path) - logger.info(f"已备份现有功能配置文件到: {backup_path}") - - # 创建新的功能配置 - new_features_config = { - "group_list_type": chat_config.get("group_list_type", "whitelist"), - "group_list": chat_config.get("group_list", []), - "private_list_type": chat_config.get("private_list_type", "whitelist"), - "private_list": chat_config.get("private_list", []), - "ban_user_id": chat_config.get("ban_user_id", []), - "ban_qq_bot": chat_config.get("ban_qq_bot", False), - "enable_poke": chat_config.get("enable_poke", True), - "ignore_non_self_poke": chat_config.get("ignore_non_self_poke", False), - "poke_debounce_seconds": chat_config.get("poke_debounce_seconds", 3), - "enable_video_analysis": video_config.get("enable_video_analysis", True), - "max_video_size_mb": video_config.get("max_video_size_mb", 100), - "download_timeout": video_config.get("download_timeout", 60), - "supported_formats": video_config.get( - "supported_formats", ["mp4", "avi", "mov", "mkv", "flv", "wmv", "webm"] - ), - } - - # 写入新的功能配置文件 - with open(new_features_path, "w", encoding="utf-8") as f: - tomlkit.dump(new_features_config, f) - - logger.info(f"功能配置已成功迁移到: {new_features_path}") - - # 显示迁移的配置内容 - logger.info("迁移的配置内容:") - for key, value in new_features_config.items(): - logger.info(f" {key}: {value}") - - return True - - except Exception as e: - logger.error(f"功能配置迁移失败: {e}") - return False - - -def remove_features_from_old_config(config_path: str = "plugins/napcat_adapter_plugin/config/config.toml"): - """ - 从旧配置文件中移除功能相关配置,并将旧配置移动到 config/old/ 目录 - - Args: - config_path: 配置文件路径 - """ - try: - if not os.path.exists(config_path): - logger.warning(f"配置文件不存在: {config_path}") - return False - - # 确保 config/old 目录存在 - old_config_dir = "plugins/napcat_adapter_plugin/config/old" - os.makedirs(old_config_dir, exist_ok=True) - - # 备份原配置文件到 config/old 目录 - old_config_path = os.path.join(old_config_dir, "config_with_features.toml") - shutil.copy2(config_path, old_config_path) - logger.info(f"已备份包含功能配置的原文件到: {old_config_path}") - - # 读取配置文件 - with open(config_path, "r", encoding="utf-8") as f: - config = tomlkit.load(f) - - # 移除chat段中的功能相关配置 - removed_keys = [] - if "chat" in config: - chat_config = config["chat"] - permission_keys = [ - "group_list_type", - "group_list", - "private_list_type", - "private_list", - "ban_user_id", - "ban_qq_bot", - "enable_poke", - "ignore_non_self_poke", - "poke_debounce_seconds", - ] - - for key in permission_keys: - if key in chat_config: - del chat_config[key] - removed_keys.append(key) - - if removed_keys: - logger.info(f"已从chat配置段中移除功能相关配置: {removed_keys}") - - # 移除video段中的配置 - if "video" in config: - video_config = config["video"] - video_keys = ["enable_video_analysis", "max_video_size_mb", "download_timeout", "supported_formats"] - - video_removed_keys = [] - for key in video_keys: - if key in video_config: - del video_config[key] - video_removed_keys.append(key) - - if video_removed_keys: - logger.info(f"已从video配置段中移除配置: {video_removed_keys}") - removed_keys.extend(video_removed_keys) - - # 如果video段为空,则删除整个段 - if not video_config: - del config["video"] - logger.info("已删除空的video配置段") - - if removed_keys: - logger.info(f"总共移除的配置项: {removed_keys}") - - # 写回配置文件 - with open(config_path, "w", encoding="utf-8") as f: - f.write(tomlkit.dumps(config)) - - logger.info(f"已更新配置文件: {config_path}") - return True - - except Exception as e: - logger.error(f"移除功能配置失败: {e}") - return False - - -def auto_migrate_features(): - """ - 自动执行功能配置迁移 - """ - logger.info("开始自动功能配置迁移...") - - # 执行迁移 - if migrate_features_from_config(): - logger.info("功能配置迁移成功") - - # 询问是否要从旧配置文件中移除功能配置 - logger.info("功能配置已迁移到独立文件,建议从主配置文件中移除相关配置") - # 在实际使用中,这里可以添加用户确认逻辑 - # 为了自动化,这里直接执行移除 - remove_features_from_old_config() - - else: - logger.info("功能配置迁移跳过或失败") - - -if __name__ == "__main__": - auto_migrate_features() diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/config/official_configs.py b/src/plugins/built_in/napcat_adapter_plugin/src/config/official_configs.py deleted file mode 100644 index 35bac1d57..000000000 --- a/src/plugins/built_in/napcat_adapter_plugin/src/config/official_configs.py +++ /dev/null @@ -1,74 +0,0 @@ -from dataclasses import dataclass, field -from typing import Literal - -from .config_base import ConfigBase - -""" -须知: -1. 本文件中记录了所有的配置项 -2. 所有新增的class都需要继承自ConfigBase -3. 所有新增的class都应在config.py中的Config类中添加字段 -4. 对于新增的字段,若为可选项,则应在其后添加field()并设置default_factory或default -""" - -ADAPTER_PLATFORM = "qq" - - -@dataclass -class NicknameConfig(ConfigBase): - nickname: str - """机器人昵称""" - - -@dataclass -class NapcatServerConfig(ConfigBase): - mode: Literal["reverse", "forward"] = "reverse" - """连接模式:reverse=反向连接(作为服务器), forward=正向连接(作为客户端)""" - - host: str = "localhost" - """主机地址""" - - port: int = 8095 - """端口号""" - - url: str = "" - """正向连接时的完整WebSocket URL,如 ws://localhost:8080/ws""" - - access_token: str = "" - """WebSocket 连接的访问令牌,用于身份验证""" - - heartbeat_interval: int = 30 - """心跳间隔时间,单位为秒""" - - -@dataclass -class MaiBotServerConfig(ConfigBase): - platform_name: str = field(default=ADAPTER_PLATFORM, init=False) - """平台名称,“qq”""" - - host: str = "localhost" - """MaiMCore的主机地址""" - - port: int = 8000 - """MaiMCore的端口号""" - - -@dataclass -class VoiceConfig(ConfigBase): - use_tts: bool = False - """是否启用TTS功能""" - - -@dataclass -class SlicingConfig(ConfigBase): - max_frame_size: int = 64 - """WebSocket帧的最大大小,单位为字节,默认64KB""" - - delay_ms: int = 10 - """切片发送间隔时间,单位为毫秒""" - - -@dataclass -class DebugConfig(ConfigBase): - level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO" - """日志级别,默认为INFO""" 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 f761dc33f..7f60fe9da 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 @@ -97,21 +97,41 @@ class MessageHandler: # 使用新的权限管理器检查权限 if group_id: - if not config_api.get_plugin_config(self.plugin_config, f"features.group_allowed.{group_id}", True): - logger.warning("群聊不在聊天权限范围内,消息被丢弃") - return False + # 检查群聊黑白名单 + group_list_type = config_api.get_plugin_config(self.plugin_config, "features.group_list_type", "blacklist") + group_list = config_api.get_plugin_config(self.plugin_config, "features.group_list", []) + + if group_list_type == "whitelist": + if group_id not in group_list: + logger.warning("群聊不在白名单中,消息被丢弃") + return False + else: # blacklist + if group_id in group_list: + logger.warning("群聊在黑名单中,消息被丢弃") + return False else: - if not config_api.get_plugin_config(self.plugin_config, f"features.private_allowed.{user_id}", True): - logger.warning("私聊不在聊天权限范围内,消息被丢弃") - return False + # 检查私聊黑白名单 + private_list_type = config_api.get_plugin_config(self.plugin_config, "features.private_list_type", "blacklist") + private_list = config_api.get_plugin_config(self.plugin_config, "features.private_list", []) + + if private_list_type == "whitelist": + if user_id not in private_list: + logger.warning("私聊不在白名单中,消息被丢弃") + return False + else: # blacklist + if user_id in private_list: + logger.warning("私聊在黑名单中,消息被丢弃") + return False # 检查全局禁止名单 - if not ignore_global_list and config_api.get_plugin_config(self.plugin_config, f"features.user_banned.{user_id}", False): + ban_user_id = config_api.get_plugin_config(self.plugin_config, "features.ban_user_id", []) + if not ignore_global_list and user_id in ban_user_id: logger.warning("用户在全局黑名单中,消息被丢弃") return False # 检查QQ官方机器人 - if config_api.get_plugin_config(self.plugin_config, "features.qq_bot_banned", False) and group_id and not ignore_bot: + ban_qq_bot = config_api.get_plugin_config(self.plugin_config, "features.ban_qq_bot", False) + if ban_qq_bot and group_id and not ignore_bot: logger.debug("开始判断是否为机器人") member_info = await get_member_info(self.get_server_connection(), group_id, user_id) if member_info: @@ -267,14 +287,15 @@ class MessageHandler: return None # 检查是否需要使用消息缓冲 - if config_api.get_plugin_config(self.plugin_config, "features.message_buffer_enabled", False): + enable_message_buffer = config_api.get_plugin_config(self.plugin_config, "features.enable_message_buffer", True) + if enable_message_buffer: # 检查消息类型是否启用缓冲 message_type = raw_message.get("message_type") should_use_buffer = False - if message_type == "group" and config_api.get_plugin_config(self.plugin_config, "features.message_buffer_group_enabled", False): + if message_type == "group" and config_api.get_plugin_config(self.plugin_config, "features.message_buffer_enable_group", True): should_use_buffer = True - elif message_type == "private" and config_api.get_plugin_config(self.plugin_config, "features.message_buffer_private_enabled", False): + elif message_type == "private" and config_api.get_plugin_config(self.plugin_config, "features.message_buffer_enable_private", True): should_use_buffer = True if should_use_buffer: From cb6fe9e411dd8ca3ebace552c38060fe2e761123 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 21:26:54 +0800 Subject: [PATCH 47/69] =?UTF-8?q?refactor(planner):=20=E7=A7=BB=E9=99=A4pr?= =?UTF-8?q?oactive=E6=A8=A1=E5=BC=8F=E4=B8=8B=E7=9A=84do=5Fnothing?= =?UTF-8?q?=E5=8A=A8=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 从动作规划器中删除了在PROACTIVE模式下硬编码的`do_nothing`选项。此更改旨在简化动作列表,并依赖于后续的决策逻辑来确定是否需要执行任何操作,而不是将其作为一个明确的动作选项提供。 --- src/chat/planner_actions/planner.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index a1efd8d57..a56c4d68e 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -236,17 +236,6 @@ class ActionPlanner: 构建动作选项 """ action_options_block = "" - - if mode == ChatMode.PROACTIVE: - action_options_block += """动作:do_nothing -动作描述:保持沉默,不主动发起任何动作或对话。 -- 当你分析了所有信息后,觉得当前不是一个发起互动的好时机时 -{{ - "action": "do_nothing", - "reason":"决定保持沉默的具体原因" -}} - -""" for action_name, action_info in current_available_actions.items(): # TODO: 增加一个字段来判断action是否支持在PROACTIVE模式下使用 From db3c3ebd637496a6fe506af6b1ef03d2ca123daf Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 6 Sep 2025 21:44:37 +0800 Subject: [PATCH 48/69] =?UTF-8?q?feat(chat):=20=E6=96=B0=E5=A2=9E=E7=A7=81?= =?UTF-8?q?=E8=81=8A=E5=BC=BA=E5=88=B6=E5=9B=9E=E5=A4=8D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当在私聊场景下,如果大脑决策为“no_reply”,此功能会将其强制修改为“reply”,以确保机器人总能响应私聊消息。 该功能通过新的配置项 `force_reply_private` 控制,默认为关闭状态。同时,原配置项 `force_focus_private` 已被重命名为 `force_reply_private` 以更准确地反映其功能。 --- src/chat/planner_actions/planner.py | 6 ++++++ src/config/official_configs.py | 2 +- template/bot_config_template.toml | 6 +++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index a56c4d68e..a84f2581c 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -582,6 +582,12 @@ class ActionPlanner: 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") diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 3a8b46d03..30d33006a 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -75,7 +75,7 @@ class ChatConfig(ValidatedConfigBase): at_bot_inevitable_reply: bool = Field(default=False, description="@机器人的必然回复") talk_frequency_adjust: list[list[str]] = Field(default_factory=lambda: [], description="聊天频率调整") focus_value: float = Field(default=1.0, description="专注值") - force_focus_private: bool = Field(default=False, description="强制专注私聊") + force_reply_private: bool = Field(default=False, description="强制回复私聊") group_chat_mode: Literal["auto", "normal", "focus"] = Field(default="auto", description="群聊模式") timestamp_display_mode: Literal["normal", "normal_no_YMD", "relative"] = Field( default="normal_no_YMD", description="时间戳显示模式" diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 0ee513b6e..1cbd32819 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.7.4" +version = "6.7.5" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -114,8 +114,8 @@ focus_value = 1 # breaking模式配置 enable_breaking_mode = true # 是否启用自动进入breaking模式,关闭后不会自动进入breaking形式 -# 强制私聊专注模式 -force_focus_private = false # 是否强制私聊进入专注模式,开启后私聊将始终保持专注状态 +# 强制私聊回复 +force_reply_private = false # 是否强制私聊回复,开启后私聊将强制回复 max_context_size = 25 # 上下文长度 thinking_timeout = 40 # MoFox-Bot一次回复最长思考规划时间,超过这个时间的思考会放弃(往往是api反应太慢) From 8a99e7fe634826741a3c9d89bfa8c618102fba3f Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Sat, 6 Sep 2025 22:47:58 +0800 Subject: [PATCH 49/69] =?UTF-8?q?refactor(napcat):=20=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E7=AE=A1=E7=90=86=E5=92=8C=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E7=BA=A7=E5=88=AB=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加MaiBot连接重试机制,支持非阻塞启动和自动重连 - 优化日志输出级别,减少非关键信息的INFO日志 - 增强消息发送的连接状态检查和故障恢复 - 改进调试模式下的原始消息记录逻辑 --- .../built_in/napcat_adapter_plugin/plugin.py | 33 +++++++++++++++-- .../src/message_buffer.py | 10 +++--- .../src/recv_handler/message_handler.py | 33 ++++++++--------- .../src/recv_handler/message_sending.py | 36 +++++++++++++++++++ .../src/recv_handler/notice_handler.py | 14 ++++---- .../src/response_pool.py | 7 ++-- 6 files changed, 97 insertions(+), 36 deletions(-) diff --git a/src/plugins/built_in/napcat_adapter_plugin/plugin.py b/src/plugins/built_in/napcat_adapter_plugin/plugin.py index 39082d0f3..7dfdd02d8 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/plugin.py +++ b/src/plugins/built_in/napcat_adapter_plugin/plugin.py @@ -40,7 +40,9 @@ async def message_recv(server_connection: Server.ServerConnection): asyncio.create_task(notice_handler.set_server_connection(server_connection)) await send_handler.set_server_connection(server_connection) async for raw_message in server_connection: - logger.debug(f"{raw_message[:1500]}..." if (len(raw_message) > 1500) else raw_message) + # 只在debug模式下记录原始消息 + if logger.level <= 10: # DEBUG level + logger.debug(f"{raw_message[:1500]}..." if (len(raw_message) > 1500) else raw_message) decoded_raw_message: dict = json.loads(raw_message) try: # 首先尝试解析原始消息 @@ -225,13 +227,38 @@ class LauchNapcatAdapterHandler(BaseEventHandler): await reassembler.start_cleanup_task() logger.info("开始启动Napcat Adapter") - message_send_instance.maibot_router = router + # 创建单独的异步任务,防止阻塞主线程 + asyncio.create_task(self._start_maibot_connection()) asyncio.create_task(napcat_server(self.plugin_config)) - asyncio.create_task(mmc_start_com(self.plugin_config)) asyncio.create_task(message_process()) asyncio.create_task(check_timeout_response()) + async def _start_maibot_connection(self): + """非阻塞方式启动MaiBot连接,等待主服务启动后再连接""" + # 等待一段时间让MaiBot主服务完全启动 + await asyncio.sleep(5) + + max_attempts = 10 + attempt = 0 + + while attempt < max_attempts: + try: + logger.info(f"尝试连接MaiBot (第{attempt + 1}次)") + await mmc_start_com(self.plugin_config) + message_send_instance.maibot_router = router + logger.info("MaiBot router连接已建立") + return + except Exception as e: + attempt += 1 + if attempt >= max_attempts: + logger.error(f"MaiBot连接失败,已达到最大重试次数: {e}") + return + else: + delay = min(2 + attempt, 10) # 逐渐增加延迟,最大10秒 + logger.warning(f"MaiBot连接失败: {e},{delay}秒后重试") + await asyncio.sleep(delay) + class StopNapcatAdapterHandler(BaseEventHandler): """关闭Adapter""" diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/message_buffer.py b/src/plugins/built_in/napcat_adapter_plugin/src/message_buffer.py index 1988e6c40..64a1e3faa 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/src/message_buffer.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/message_buffer.py @@ -159,7 +159,7 @@ class SimpleMessageBuffer: # 检查是否超过最大组件数量 if len(session.messages) >= config_api.get_plugin_config(self.plugin_config, "features.message_buffer_max_components", 5): - logger.info(f"会话 {session_id} 消息数量达到上限,强制合并") + logger.debug(f"会话 {session_id} 消息数量达到上限,强制合并") asyncio.create_task(self._force_merge_session(session_id)) self.buffer_pool[session_id] = BufferedSession(session_id=session_id, original_event=original_event) session = self.buffer_pool[session_id] @@ -240,7 +240,7 @@ class SimpleMessageBuffer: merged_text = ",".join(text_parts) # 使用中文逗号连接 message_count = len(session.messages) - logger.info(f"合并会话 {session_id} 的 {message_count} 条文本消息: {merged_text[:100]}...") + logger.debug(f"合并会话 {session_id} 的 {message_count} 条文本消息: {merged_text[:100]}...") # 调用回调函数 if self.merge_callback: @@ -294,13 +294,13 @@ class SimpleMessageBuffer: expired_sessions.append(session_id) for session_id in expired_sessions: - logger.info(f"清理过期会话: {session_id}") + logger.debug(f"清理过期会话: {session_id}") await self._force_merge_session(session_id) async def shutdown(self): """关闭消息缓冲器""" self._shutdown = True - logger.info("正在关闭简化消息缓冲器...") + logger.debug("正在关闭简化消息缓冲器...") # 刷新所有缓冲区 await self.flush_all() @@ -311,4 +311,4 @@ class SimpleMessageBuffer: await self._cancel_session_timers(session) self.buffer_pool.clear() - logger.info("简化消息缓冲器已关闭") + logger.debug("简化消息缓冲器已关闭") 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 7f60fe9da..172eaa667 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 @@ -300,7 +300,6 @@ class MessageHandler: if should_use_buffer: logger.debug(f"尝试缓冲消息,消息类型: {message_type}, 用户: {user_info.user_id}") - logger.debug(f"原始消息段: {raw_message.get('message', [])}") # 尝试添加到缓冲器 buffered = await self.message_buffer.add_text_message( @@ -314,10 +313,10 @@ class MessageHandler: ) if buffered: - logger.info(f"✅ 文本消息已成功缓冲: {user_info.user_id}") + logger.debug(f"✅ 文本消息已成功缓冲: {user_info.user_id}") return None # 缓冲成功,不立即发送 # 如果缓冲失败(消息包含非文本元素),走正常处理流程 - logger.info(f"❌ 消息缓冲失败,包含非文本元素,走正常处理流程: {user_info.user_id}") + logger.debug(f"❌ 消息缓冲失败,包含非文本元素,走正常处理流程: {user_info.user_id}") # 缓冲失败时继续执行后面的正常处理流程,不要直接返回 logger.debug(f"准备发送消息到MaiBot,消息段数量: {len(seg_message)}") @@ -335,7 +334,7 @@ class MessageHandler: raw_message=raw_message.get("raw_message"), ) - logger.info("发送到Maibot处理信息") + logger.debug("发送到Maibot处理信息") await message_send_instance.message_send(message_base) async def handle_real_message(self, raw_message: dict, in_reply: bool = False) -> List[Seg] | None: @@ -530,9 +529,7 @@ class MessageHandler: message_data: dict = raw_message.get("data") image_sub_type = message_data.get("sub_type") try: - logger.debug(f"开始下载图片: {message_data.get('url')}") image_base64 = await get_image_base64(message_data.get("url")) - logger.debug(f"图片下载成功,大小: {len(image_base64)} 字符") except Exception as e: logger.error(f"图片消息处理失败: {str(e)}") return None @@ -623,8 +620,8 @@ class MessageHandler: video_url = message_data.get("url") file_path = message_data.get("filePath") or message_data.get("file_path") - logger.info(f"视频URL: {video_url}") - logger.info(f"视频文件路径: {file_path}") + logger.debug(f"视频URL: {video_url}") + logger.debug(f"视频文件路径: {file_path}") # 优先使用本地文件路径,其次使用URL video_source = file_path if file_path else video_url @@ -637,14 +634,14 @@ class MessageHandler: try: # 检查是否为本地文件路径 if file_path and Path(file_path).exists(): - logger.info(f"使用本地视频文件: {file_path}") + logger.debug(f"使用本地视频文件: {file_path}") # 直接读取本地文件 with open(file_path, "rb") as f: video_data = f.read() # 将视频数据编码为base64用于传输 video_base64 = base64.b64encode(video_data).decode("utf-8") - logger.info(f"视频文件大小: {len(video_data) / (1024 * 1024):.2f} MB") + logger.debug(f"视频文件大小: {len(video_data) / (1024 * 1024):.2f} MB") # 返回包含详细信息的字典格式 return Seg( @@ -657,7 +654,7 @@ class MessageHandler: ) elif video_url: - logger.info(f"使用视频URL下载: {video_url}") + logger.debug(f"使用视频URL下载: {video_url}") # 使用video_handler下载视频 video_downloader = get_video_downloader() download_result = await video_downloader.download_video(video_url) @@ -669,7 +666,7 @@ class MessageHandler: # 将视频数据编码为base64用于传输 video_base64 = base64.b64encode(download_result["data"]).decode("utf-8") - logger.info(f"视频下载成功,大小: {len(download_result['data']) / (1024 * 1024):.2f} MB") + logger.debug(f"视频下载成功,大小: {len(download_result['data']) / (1024 * 1024):.2f} MB") # 返回包含详细信息的字典格式 return Seg( @@ -738,15 +735,15 @@ class MessageHandler: processed_message: Seg if image_count < 5 and image_count > 0: # 处理图片数量小于5的情况,此时解析图片为base64 - logger.info("图片数量小于5,开始解析图片为base64") + logger.debug("图片数量小于5,开始解析图片为base64") processed_message = await self._recursive_parse_image_seg(handled_message, True) elif image_count > 0: - logger.info("图片数量大于等于5,开始解析图片为占位符") + logger.debug("图片数量大于等于5,开始解析图片为占位符") # 处理图片数量大于等于5的情况,此时解析图片为占位符 processed_message = await self._recursive_parse_image_seg(handled_message, False) else: # 处理没有图片的情况,此时直接返回 - logger.info("没有图片,直接返回") + logger.debug("没有图片,直接返回") processed_message = handled_message # 添加转发消息提示 @@ -880,7 +877,7 @@ class MessageHandler: return Seg(type="text", data="[表情包]") return Seg(type="emoji", data=encoded_image) else: - logger.info(f"不处理类型: {seg_data.type}") + logger.debug(f"不处理类型: {seg_data.type}") return seg_data else: if seg_data.type == "seglist": @@ -894,7 +891,7 @@ class MessageHandler: elif seg_data.type == "emoji": return Seg(type="text", data="[动画表情]") else: - logger.info(f"不处理类型: {seg_data.type}") + logger.debug(f"不处理类型: {seg_data.type}") return seg_data async def _handle_forward_message(self, message_list: list, layer: int) -> Tuple[Seg, int] | Tuple[None, int]: @@ -1069,7 +1066,7 @@ class MessageHandler: raw_message=raw_message.get("raw_message", ""), ) - logger.info(f"发送缓冲合并消息到Maibot处理: {session_id}") + logger.debug(f"发送缓冲合并消息到Maibot处理: {session_id}") await message_send_instance.message_send(message_base) except Exception as e: diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_sending.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_sending.py index 3372aa262..b7ca408d9 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_sending.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_sending.py @@ -15,6 +15,8 @@ class MessageSending: maibot_router: Router = None plugin_config = None + _connection_retries = 0 + _max_retries = 3 def __init__(self): pass @@ -23,6 +25,25 @@ class MessageSending: """设置插件配置""" self.plugin_config = plugin_config + async def _attempt_reconnect(self): + """尝试重新连接MaiBot router""" + if self._connection_retries < self._max_retries: + self._connection_retries += 1 + logger.warning(f"尝试重新连接MaiBot router (第{self._connection_retries}次)") + try: + # 重新导入router + from ..mmc_com_layer import router + self.maibot_router = router + if self.maibot_router is not None: + logger.info("MaiBot router重连成功") + self._connection_retries = 0 # 重置重试计数 + return True + except Exception as e: + logger.error(f"重连失败: {e}") + else: + logger.error(f"已达到最大重连次数({self._max_retries}),停止重试") + return False + async def message_send(self, message_base: MessageBase) -> bool: """ 发送消息(Ada -> MMC 方向,需要实现切片) @@ -30,6 +51,13 @@ class MessageSending: message_base: MessageBase: 消息基类,包含发送目标和消息内容等信息 """ try: + # 检查maibot_router是否已初始化 + if self.maibot_router is None: + logger.warning("MaiBot router未初始化,尝试重新连接") + if not await self._attempt_reconnect(): + logger.error("MaiBot router重连失败,无法发送消息") + logger.error("请检查与MaiBot之间的连接") + return False # 检查是否需要切片发送 message_dict = message_base.to_dict() @@ -45,6 +73,14 @@ class MessageSending: # 获取对应的客户端并发送切片 platform = message_base.message_info.platform + + # 再次检查router状态(防止运行时被重置) + if self.maibot_router is None or not hasattr(self.maibot_router, 'clients'): + logger.warning("MaiBot router连接已断开,尝试重新连接") + if not await self._attempt_reconnect(): + logger.error("MaiBot router重连失败,切片发送中止") + return False + if platform not in self.maibot_router.clients: logger.error(f"平台 {platform} 未连接") return False 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 0efdcd352..b756530cb 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 @@ -119,7 +119,7 @@ class NoticeHandler: if config_api.get_plugin_config(self.plugin_config, "features.poke_enabled", True) and await message_handler.check_allow_to_chat( user_id, group_id, False, False ): - logger.info("处理戳一戳消息") + logger.debug("处理戳一戳消息") handled_message, user_info = await self.handle_poke_notify(raw_message, group_id, user_id) else: logger.warning("戳一戳消息被禁用,取消戳一戳处理") @@ -191,7 +191,7 @@ class NoticeHandler: if system_notice: await self.put_notice(message_base) else: - logger.info("发送到Maibot处理通知信息") + logger.debug("发送到Maibot处理通知信息") await message_send_instance.message_send(message_base) async def handle_poke_notify( @@ -215,7 +215,7 @@ class NoticeHandler: if self.last_poke_time > 0: time_diff = current_time - self.last_poke_time if time_diff < debounce_seconds: - logger.info(f"戳一戳防抖:用户 {user_id} 的戳一戳被忽略(距离上次戳一戳 {time_diff:.2f} 秒)") + logger.debug(f"戳一戳防抖:用户 {user_id} 的戳一戳被忽略(距离上次戳一戳 {time_diff:.2f} 秒)") return None, None # 记录这次戳一戳的时间 @@ -234,7 +234,7 @@ class NoticeHandler: else: user_name = "QQ用户" user_cardname = "QQ用户" - logger.info("无法获取戳一戳对方的用户昵称") + logger.debug("无法获取戳一戳对方的用户昵称") # 计算Seg if self_id == target_id: @@ -248,7 +248,7 @@ class NoticeHandler: else: # 如果配置为忽略不是针对自己的戳一戳,则直接返回None if config_api.get_plugin_config(self.plugin_config, "features.non_self_poke_ignored", False): - logger.info("忽略不是针对自己的戳一戳消息") + logger.debug("忽略不是针对自己的戳一戳消息") return None, None # 老实说这一步判定没啥意义,毕竟私聊是没有其他人之间的戳一戳,但是感觉可以有这个判定来强限制群聊环境 @@ -258,7 +258,7 @@ class NoticeHandler: target_name = fetched_member_info.get("nickname") else: target_name = "QQ用户" - logger.info("无法获取被戳一戳方的用户昵称") + logger.debug("无法获取被戳一戳方的用户昵称") display_name = user_name else: return None, None @@ -521,7 +521,7 @@ class NoticeHandler: continue if ban_record.lift_time <= int(time.time()): # 触发自然解除禁言 - logger.info(f"检测到用户 {ban_record.user_id} 在群 {ban_record.group_id} 的禁言已解除") + logger.debug(f"检测到用户 {ban_record.user_id} 在群 {ban_record.group_id} 的禁言已解除") self.lifted_list.append(ban_record) self.banned_list.remove(ban_record) await asyncio.sleep(5) diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/response_pool.py b/src/plugins/built_in/napcat_adapter_plugin/src/response_pool.py index 0c5072fa5..3e8e5c4a4 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/src/response_pool.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/response_pool.py @@ -20,7 +20,7 @@ def set_plugin_config(config: dict): async def get_response(request_id: str, timeout: int = 10) -> dict: response = await asyncio.wait_for(_get_response(request_id), timeout) _ = response_time_dict.pop(request_id) - logger.info(f"响应信息id: {request_id} 已从响应字典中取出") + logger.debug(f"响应信息id: {request_id} 已从响应字典中取出") return response @@ -38,7 +38,7 @@ async def put_response(response: dict): now_time = time.time() response_dict[echo_id] = response response_time_dict[echo_id] = now_time - logger.info(f"响应信息id: {echo_id} 已存入响应字典") + logger.debug(f"响应信息id: {echo_id} 已存入响应字典") async def check_timeout_response() -> None: @@ -57,5 +57,6 @@ async def check_timeout_response() -> None: response_dict.pop(echo_id) response_time_dict.pop(echo_id) logger.warning(f"响应消息 {echo_id} 超时,已删除") - logger.info(f"已删除 {cleaned_message_count} 条超时响应消息") + if cleaned_message_count > 0: + logger.info(f"已删除 {cleaned_message_count} 条超时响应消息") await asyncio.sleep(heartbeat_interval) From d53e39e85dac31877ae1f8142bb39d8bbeb6b9fc Mon Sep 17 00:00:00 2001 From: Furina-1013-create <189647097+Furina-1013-create@users.noreply.github.com> Date: Sat, 6 Sep 2025 22:58:50 +0800 Subject: [PATCH 50/69] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E5=85=B3=E4=BA=8E=E6=95=B0=E6=8D=AE=E5=BA=93=E5=9C=A8=E5=89=8D?= =?UTF-8?q?=E9=9D=A2=E6=B2=A1=E6=9C=89=E6=AD=A3=E7=A1=AE=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=BA=AB=E4=BB=BD=E7=9A=84=E6=97=B6=E5=80=99?= =?UTF-8?q?=E7=9A=84=E6=9C=80=E5=B0=8F=E5=9B=9E=E9=80=80=E5=B9=B6=E6=94=B9?= =?UTF-8?q?=E8=BF=9Bada=E6=8F=92=E4=BB=B6=E7=9A=84=E5=85=B3=E4=BA=8E?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=A4=84=E7=90=86=E6=97=B6=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E4=BC=9A=E9=94=99=E8=AF=AF=E5=9C=B0=E6=8A=8A@=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E8=AF=86=E5=88=AB=E4=B8=BA[=E8=A7=86=E9=A2=91]?= =?UTF-8?q?=E7=9A=84=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/person_info/person_info.py | 30 +++++++++++++++++ .../src/recv_handler/message_handler.py | 33 +++++++++++++++++-- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/person_info/person_info.py b/src/person_info/person_info.py index dd5b60a20..2f89c43ff 100644 --- a/src/person_info/person_info.py +++ b/src/person_info/person_info.py @@ -150,6 +150,18 @@ class PersonInfoManager: # Ensure person_id is correctly set from the argument final_data["person_id"] = person_id + # 你们的英文注释是何意味? + + # 检查并修复关键字段为None的情况喵 + if final_data.get("user_id") is None: + logger.warning(f"user_id为None,使用'unknown'作为默认值 person_id={person_id}") + final_data["user_id"] = "unknown" + + if final_data.get("platform") is None: + logger.warning(f"platform为None,使用'unknown'作为默认值 person_id={person_id}") + final_data["platform"] = "unknown" + + # 这里的目的是为了防止在识别出错的情况下有一个最小回退,不只是针对@消息识别成视频后的报错问题 # Serialize JSON fields for key in JSON_SERIALIZED_FIELDS: @@ -199,6 +211,15 @@ class PersonInfoManager: # Ensure person_id is correctly set from the argument final_data["person_id"] = person_id + + # 检查并修复关键字段为None的情况 + if final_data.get("user_id") is None: + logger.warning(f"user_id为None,使用'unknown'作为默认值 person_id={person_id}") + final_data["user_id"] = "unknown" + + if final_data.get("platform") is None: + logger.warning(f"platform为None,使用'unknown'作为默认值 person_id={person_id}") + final_data["platform"] = "unknown" # Serialize JSON fields for key in JSON_SERIALIZED_FIELDS: @@ -295,6 +316,15 @@ class PersonInfoManager: creation_data["platform"] = data["platform"] if data and "user_id" in data: creation_data["user_id"] = data["user_id"] + + # 额外检查关键字段,如果为None则使用默认值 + if creation_data.get("user_id") is None: + logger.warning(f"创建用户时user_id为None,使用'unknown'作为默认值 person_id={person_id}") + creation_data["user_id"] = "unknown" + + if creation_data.get("platform") is None: + logger.warning(f"创建用户时platform为None,使用'unknown'作为默认值 person_id={person_id}") + creation_data["platform"] = "unknown" # 使用安全的创建方法,处理竞态条件 await self._safe_create_person_info(person_id, creation_data) 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 7f60fe9da..5ab78a97a 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 @@ -156,6 +156,21 @@ class MessageHandler: Parameters: raw_message: dict: 原始消息 """ + + # 添加原始消息调试日志,特别关注message字段 + logger.debug(f"收到原始消息: message_type={raw_message.get('message_type')}, message_id={raw_message.get('message_id')}") + logger.debug(f"原始消息内容: {raw_message.get('message', [])}") + + # 检查是否包含@或video消息段 + message_segments = raw_message.get('message', []) + if message_segments: + for i, seg in enumerate(message_segments): + seg_type = seg.get('type') + if seg_type in ['at', 'video']: + logger.info(f"检测到 {seg_type.upper()} 消息段 [{i}]: {seg}") + elif seg_type not in ['text', 'face', 'image']: + logger.warning(f"检测到特殊消息段 [{i}]: type={seg_type}, data={seg.get('data', {})}") + message_type: str = raw_message.get("message_type") message_id: int = raw_message.get("message_id") # message_time: int = raw_message.get("time") @@ -354,6 +369,18 @@ class MessageHandler: for sub_message in real_message: sub_message: dict sub_message_type = sub_message.get("type") + + # 添加详细的消息类型调试信息 + logger.debug(f"处理消息段: type={sub_message_type}, data={sub_message.get('data', {})}") + + # 特别关注 at 和 video 消息的识别 + if sub_message_type == "at": + logger.debug(f"检测到@消息: {sub_message}") + elif sub_message_type == "video": + logger.debug(f"检测到VIDEO消息: {sub_message}") + elif sub_message_type not in ["text", "face", "image", "record"]: + logger.warning(f"检测到特殊消息类型: {sub_message_type}, 完整消息: {sub_message}") + match sub_message_type: case RealMessageType.text: ret_seg = await self.handle_text_message(sub_message) @@ -407,6 +434,7 @@ class MessageHandler: else: logger.warning("record处理失败或不支持") case RealMessageType.video: + logger.debug(f"开始处理VIDEO消息段: {sub_message}") ret_seg = await self.handle_video_message(sub_message) if ret_seg: await event_manager.trigger_event( @@ -414,8 +442,9 @@ class MessageHandler: ) seg_message.append(ret_seg) else: - logger.warning("video处理失败") + logger.warning(f"video处理失败,原始消息: {sub_message}") case RealMessageType.at: + logger.debug(f"开始处理AT消息段: {sub_message}") ret_seg = await self.handle_at_message( sub_message, raw_message.get("self_id"), @@ -427,7 +456,7 @@ class MessageHandler: ) seg_message.append(ret_seg) else: - logger.warning("at处理失败") + logger.warning(f"at处理失败,原始消息: {sub_message}") case RealMessageType.rps: ret_seg = await self.handle_rps_message(sub_message) if ret_seg: From 3684ffe8813d396d1b9607cb295c57c6ba560b08 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Sat, 6 Sep 2025 23:03:21 +0800 Subject: [PATCH 51/69] =?UTF-8?q?refactor(napcat):=20=E5=B0=86enable=5Fplu?= =?UTF-8?q?gin=E6=94=B9=E4=B8=BA=E5=8A=A8=E6=80=81=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除硬编码的enable_plugin布尔值 - 添加enable_plugin属性方法,支持通过配置文件动态控制插件启用状态 - 默认状态改为禁用,提高系统安全性 - 支持运行时通过_is_enabled属性缓存配置状态 --- src/plugins/built_in/napcat_adapter_plugin/plugin.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/plugins/built_in/napcat_adapter_plugin/plugin.py b/src/plugins/built_in/napcat_adapter_plugin/plugin.py index 7dfdd02d8..966edd19c 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/plugin.py +++ b/src/plugins/built_in/napcat_adapter_plugin/plugin.py @@ -277,11 +277,19 @@ class StopNapcatAdapterHandler(BaseEventHandler): @register_plugin class NapcatAdapterPlugin(BasePlugin): plugin_name = CONSTS.PLUGIN_NAME - enable_plugin: bool = True dependencies: List[str] = [] # 插件依赖列表 python_dependencies: List[str] = [] # Python包依赖列表 config_file_name: str = "config.toml" # 配置文件名 + @property + def enable_plugin(self) -> bool: + """通过配置文件动态控制插件启用状态""" + # 如果已经通过配置加载了状态,使用配置中的值 + if hasattr(self, '_is_enabled'): + return self._is_enabled + # 否则使用默认值(禁用状态) + return False + # 配置节描述 config_section_descriptions = {"plugin": "插件基本信息"} From e499e663f2c513a7d8ee7784421fca2efe7ded6e Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Sat, 6 Sep 2025 23:15:11 +0800 Subject: [PATCH 52/69] =?UTF-8?q?refactor(sleep=5Fmanager):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=A8=A1=E5=9D=97=E5=86=85=E7=9A=84=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 `sleep_manager` 和 `wakeup_manager` 中的绝对导入路径更改为相对导入,以解决潜在的循环依赖问题。 --- src/chat/chat_loop/sleep_manager/sleep_manager.py | 2 +- src/chat/chat_loop/sleep_manager/wakeup_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chat/chat_loop/sleep_manager/sleep_manager.py b/src/chat/chat_loop/sleep_manager/sleep_manager.py index 0ae9e39e0..7eb1d3d61 100644 --- a/src/chat/chat_loop/sleep_manager/sleep_manager.py +++ b/src/chat/chat_loop/sleep_manager/sleep_manager.py @@ -10,7 +10,7 @@ from .time_checker import TimeChecker from .notification_sender import NotificationSender if TYPE_CHECKING: - from mmc.src.chat.chat_loop.sleep_manager.wakeup_manager import WakeUpManager + from .wakeup_manager import WakeUpManager logger = get_logger("sleep_manager") diff --git a/src/chat/chat_loop/sleep_manager/wakeup_manager.py b/src/chat/chat_loop/sleep_manager/wakeup_manager.py index df2bf835e..28c91dd3d 100644 --- a/src/chat/chat_loop/sleep_manager/wakeup_manager.py +++ b/src/chat/chat_loop/sleep_manager/wakeup_manager.py @@ -138,7 +138,7 @@ class WakeUpManager: return False # 只有在休眠且非失眠状态下才累积唤醒度 - from mmc.src.chat.chat_loop.sleep_manager.sleep_manager import SleepState + from .sleep_state import SleepState sleep_manager = self.context.sleep_manager if not sleep_manager: From d826bc2391538a1ebc0b4a2d59fddbb11f2bc7c5 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Sun, 7 Sep 2025 00:21:33 +0800 Subject: [PATCH 53/69] =?UTF-8?q?feat(config):=20=E4=B8=BA=E7=9D=A1?= =?UTF-8?q?=E7=9C=A0=E7=B3=BB=E7=BB=9F=E6=B7=BB=E5=8A=A0=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E6=94=AF=E6=8C=81=E5=8F=8A=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在模板配置中增加定时睡觉/起床参数,新增根据日程表自动睡眠开关、固定作息 时间设置以及唤醒后重新入睡延迟,以便更灵活地控制机器人的睡眠行为。 --- template/bot_config_template.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 1cbd32819..523ad38ad 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.7.5" +version = "6.7.6" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -427,6 +427,9 @@ search_strategy = "single" # 搜索策略: "single"(使用第一个可用引擎) [sleep_system] enable = false #"是否启用睡眠系统" +sleep_by_schedule = true #"是否根据日程表进行睡觉" +fixed_sleep_time = "23:00" #"固定的睡觉时间" +fixed_wake_up_time = "07:00" #"固定的起床时间" wakeup_threshold = 15.0 #唤醒阈值,达到此值时会被唤醒" private_message_increment = 3.0 #"私聊消息增加的唤醒度" group_mention_increment = 2.0 #"群聊艾特增加的唤醒度" @@ -434,6 +437,7 @@ decay_rate = 0.2 #"每次衰减的唤醒度数值" decay_interval = 30.0 #"唤醒度衰减间隔(秒)" angry_duration = 300.0 #"愤怒状态持续时间(秒)" angry_prompt = "你被人吵醒了非常生气,说话带着怒气" # "被吵醒后的愤怒提示词" +re_sleep_delay_minutes = 5 # "被唤醒后,如果多久没有新消息则尝试重新入睡(分钟)" # --- 失眠机制相关参数 --- enable_insomnia_system = false # 是否启用失眠系统 From 3d8da9f073f52fe9bd535ac43eed6d6a7557256e Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sun, 7 Sep 2025 03:11:09 +0800 Subject: [PATCH 54/69] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=BB=E5=8A=A8?= =?UTF-8?q?=E6=80=9D=E8=80=83=E5=9B=9E=E5=A4=8D=E5=A5=87=E6=80=AA=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C=E4=BF=AE=E6=94=B9?= =?UTF-8?q?napcat=E6=8F=92=E4=BB=B6=E9=81=97=E7=95=99=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat_loop/response_handler.py | 2 +- .../napcat_adapter_plugin/src/recv_handler/notice_handler.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/chat/chat_loop/response_handler.py b/src/chat/chat_loop/response_handler.py index f21a33afd..b115abe40 100644 --- a/src/chat/chat_loop/response_handler.py +++ b/src/chat/chat_loop/response_handler.py @@ -154,7 +154,7 @@ class ResponseHandler: if isinstance(data, list): data = "".join(map(str, data)) - reply_text += data + reply_text += data.get("data",{}).get("text","") # 如果是主动思考且内容为“沉默”,则不发送 if is_proactive_thinking and data.strip() == "沉默": 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 b756530cb..e3af0ea83 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 @@ -116,7 +116,7 @@ class NoticeHandler: sub_type = raw_message.get("sub_type") match sub_type: case NoticeType.Notify.poke: - if config_api.get_plugin_config(self.plugin_config, "features.poke_enabled", True) and await message_handler.check_allow_to_chat( + if config_api.get_plugin_config(self.plugin_config, "features.enable_poke", True) and await message_handler.check_allow_to_chat( user_id, group_id, False, False ): logger.debug("处理戳一戳消息") @@ -247,7 +247,7 @@ class NoticeHandler: else: # 如果配置为忽略不是针对自己的戳一戳,则直接返回None - if config_api.get_plugin_config(self.plugin_config, "features.non_self_poke_ignored", False): + if config_api.get_plugin_config(self.plugin_config, "features.ignore_non_self_poke", False): logger.debug("忽略不是针对自己的戳一戳消息") return None, None From 653599b7e7f35de10794ab90691fb13543acd7fe Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sun, 7 Sep 2025 03:34:14 +0800 Subject: [PATCH 55/69] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E5=B0=86=E7=BA=AF?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E5=8C=85=E8=A3=85=E6=88=90=20ResponseSet=20?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat_loop/proactive/proactive_thinker.py | 4 ++-- src/chat/chat_loop/response_handler.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index 2ee715a4c..6f5f60825 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -219,8 +219,8 @@ class ProactiveThinker: ) if response_text: - # 将纯文本包装成 ResponseSet 格式 - response_set = [{"type": "text", "data": {"text": response_text}}] + # 不要将纯文本包装成 ResponseSet 格式! + response_set = [response_text] await self.cycle_processor.response_handler.send_response( response_set, time.time(), action_result.get("action_message") ) diff --git a/src/chat/chat_loop/response_handler.py b/src/chat/chat_loop/response_handler.py index b115abe40..354addc45 100644 --- a/src/chat/chat_loop/response_handler.py +++ b/src/chat/chat_loop/response_handler.py @@ -1,4 +1,5 @@ import time +import orjson import random from typing import Dict, Any, Tuple @@ -154,7 +155,7 @@ class ResponseHandler: if isinstance(data, list): data = "".join(map(str, data)) - reply_text += data.get("data",{}).get("text","") + reply_text += data # 如果是主动思考且内容为“沉默”,则不发送 if is_proactive_thinking and data.strip() == "沉默": From cb994a4e170df0a6b311f29304d9d948da93b1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=85=E8=AF=BA=E7=8B=90?= <212194964+foxcyber907@users.noreply.github.com> Date: Sun, 7 Sep 2025 08:20:39 +0800 Subject: [PATCH 56/69] =?UTF-8?q?feat(sleep):=20=E4=B8=BA=E7=9D=A1?= =?UTF-8?q?=E7=9C=A0=E7=B3=BB=E7=BB=9F=E6=B7=BB=E5=8A=A0=E9=9A=8F=E6=9C=BA?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E5=81=8F=E7=A7=BB=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增每日睡眠和起床时间随机偏移量配置选项 - 实现缓存机制确保同一天内使用相同的偏移量 - 重构睡眠时间检查逻辑以支持动态时间偏移 - 更新相关配置类和插件清单格式 --- .gitignore | 1 + .../chat_loop/sleep_manager/time_checker.py | 60 +++++++++++++++---- src/chat/replyer/default_generator.py | 2 + src/config/official_configs.py | 2 + .../built_in/set_typing_status/_manifest.json | 2 + .../built_in/set_typing_status/plugin.py | 1 - 6 files changed, 55 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 0d6bfa6f7..18085f317 100644 --- a/.gitignore +++ b/.gitignore @@ -337,3 +337,4 @@ MaiBot.code-workspace /tests .kilocode/rules/MoFox.md src/chat/planner_actions/planner (2).py +rust_video/Cargo.lock diff --git a/src/chat/chat_loop/sleep_manager/time_checker.py b/src/chat/chat_loop/sleep_manager/time_checker.py index 342d0d62e..32521d408 100644 --- a/src/chat/chat_loop/sleep_manager/time_checker.py +++ b/src/chat/chat_loop/sleep_manager/time_checker.py @@ -1,5 +1,6 @@ -from datetime import datetime, time +from datetime import datetime, time, timedelta from typing import Optional +import random from src.common.logger import get_logger from src.config.config import global_config @@ -10,15 +11,37 @@ logger = get_logger("time_checker") class TimeChecker: def __init__(self, schedule_source): self.schedule_source = schedule_source + # 缓存当天的偏移量,确保一天内使用相同的偏移量 + self._daily_sleep_offset = None + self._daily_wake_offset = None + self._offset_date = None + + def _get_daily_offsets(self): + """获取当天的睡眠和起床时间偏移量,每天生成一次""" + today = datetime.now().date() + + # 如果是新的一天,重新生成偏移量 + if self._offset_date != today: + sleep_offset_range = global_config.sleep_system.sleep_time_offset_minutes + wake_offset_range = global_config.sleep_system.wake_up_time_offset_minutes + + # 生成 ±offset_range 范围内的随机偏移量 + self._daily_sleep_offset = random.randint(-sleep_offset_range, sleep_offset_range) + self._daily_wake_offset = random.randint(-wake_offset_range, wake_offset_range) + self._offset_date = today + + logger.debug(f"生成新的每日偏移量 - 睡觉时间偏移: {self._daily_sleep_offset}分钟, 起床时间偏移: {self._daily_wake_offset}分钟") + + return self._daily_sleep_offset, self._daily_wake_offset def is_in_theoretical_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]: if global_config.sleep_system.sleep_by_schedule: if self.schedule_source.get_today_schedule(): return self._is_in_schedule_sleep_time(now_time) else: - return self._is_in_fixed_sleep_time(now_time) + return self._is_in_sleep_time(now_time) else: - return self._is_in_fixed_sleep_time(now_time) + return self._is_in_sleep_time(now_time) def _is_in_schedule_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]: """检查当前时间是否落在日程表的任何一个睡眠活动中""" @@ -49,20 +72,33 @@ class TimeChecker: continue return False, None - def _is_in_fixed_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]: - """检查当前时间是否在固定的睡眠时间内""" + def _is_in_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]: + """检查当前时间是否在固定的睡眠时间内(应用偏移量)""" try: start_time_str = global_config.sleep_system.fixed_sleep_time end_time_str = global_config.sleep_system.fixed_wake_up_time - start_time = datetime.strptime(start_time_str, "%H:%M").time() - end_time = datetime.strptime(end_time_str, "%H:%M").time() + + # 获取当天的偏移量 + sleep_offset, wake_offset = self._get_daily_offsets() + + # 解析基础时间 + base_start_time = datetime.strptime(start_time_str, "%H:%M") + base_end_time = datetime.strptime(end_time_str, "%H:%M") + + # 应用偏移量 + actual_start_time = (base_start_time + timedelta(minutes=sleep_offset)).time() + actual_end_time = (base_end_time + timedelta(minutes=wake_offset)).time() + + logger.debug(f"固定睡眠时间检查 - 基础时间: {start_time_str}-{end_time_str}, " + f"偏移后时间: {actual_start_time.strftime('%H:%M')}-{actual_end_time.strftime('%H:%M')}, " + f"当前时间: {now_time.strftime('%H:%M')}") - if start_time <= end_time: - if start_time <= now_time < end_time: - return True, "固定睡眠时间" + if actual_start_time <= actual_end_time: + if actual_start_time <= now_time < actual_end_time: + return True, f"固定睡眠时间(偏移后: {actual_start_time.strftime('%H:%M')}-{actual_end_time.strftime('%H:%M')})" else: - if now_time >= start_time or now_time < end_time: - return True, "固定睡眠时间" + if now_time >= actual_start_time or now_time < actual_end_time: + return True, f"固定睡眠时间(偏移后: {actual_start_time.strftime('%H:%M')}-{actual_end_time.strftime('%H:%M')})" except ValueError as e: logger.error(f"固定的睡眠时间格式不正确,请使用 HH:MM 格式: {e}") return False, None \ No newline at end of file diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index f53b8ad99..3c71ef1d2 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -283,6 +283,7 @@ class DefaultReplyer: return False, None, None from src.plugin_system.core.event_manager import event_manager + # 触发 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 @@ -304,6 +305,7 @@ class DefaultReplyer: "model": model_name, "tool_calls": tool_call, } + # 触发 AFTER_LLM 事件 if not from_plugin: result = await event_manager.trigger_event( diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 30d33006a..10bc3ae71 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -613,6 +613,8 @@ class SleepSystemConfig(ValidatedConfigBase): sleep_by_schedule: bool = Field(default=True, description="是否根据日程表进行睡觉") fixed_sleep_time: str = Field(default="23:00", description="固定的睡觉时间") fixed_wake_up_time: str = Field(default="07:00", description="固定的起床时间") + sleep_time_offset_minutes: int = Field(default=15, ge=0, le=60, description="睡觉时间随机偏移量范围(分钟),实际睡觉时间会在±该值范围内随机") + wake_up_time_offset_minutes: int = Field(default=15, ge=0, le=60, description="起床时间随机偏移量范围(分钟),实际起床时间会在±该值范围内随机") wakeup_threshold: float = Field(default=15.0, ge=1.0, description="唤醒阈值,达到此值时会被唤醒") private_message_increment: float = Field(default=3.0, ge=0.1, description="私聊消息增加的唤醒度") group_mention_increment: float = Field(default=2.0, ge=0.1, description="群聊艾特增加的唤醒度") diff --git a/src/plugins/built_in/set_typing_status/_manifest.json b/src/plugins/built_in/set_typing_status/_manifest.json index 0ba1ff237..45364c44a 100644 --- a/src/plugins/built_in/set_typing_status/_manifest.json +++ b/src/plugins/built_in/set_typing_status/_manifest.json @@ -1,4 +1,6 @@ { + + "manifest_version": 1, "name": "Set Typing Status", "description": "一个在LLM生成回复时设置私聊输入状态的插件。", "version": "1.0.0", diff --git a/src/plugins/built_in/set_typing_status/plugin.py b/src/plugins/built_in/set_typing_status/plugin.py index 5fe4c94e7..6eef98b19 100644 --- a/src/plugins/built_in/set_typing_status/plugin.py +++ b/src/plugins/built_in/set_typing_status/plugin.py @@ -29,7 +29,6 @@ class SetTypingStatusHandler(BaseEventHandler): user_id = message.message_info.user_info.user_id if not user_id: return HandlerResult(success=False, continue_process=True, message="无法获取用户ID") - try: params = {"user_id": user_id, "event_type": 1} await send_api.adapter_command_to_stream( From 2f290bd272adca58a5a3a1b484894d69b3ab3c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=85=E8=AF=BA=E7=8B=90?= <212194964+foxcyber907@users.noreply.github.com> Date: Sun, 7 Sep 2025 08:46:27 +0800 Subject: [PATCH 57/69] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/bot_config_template.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 523ad38ad..6f6dd3c5e 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.7.6" +version = "6.7.7" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -430,6 +430,8 @@ enable = false #"是否启用睡眠系统" sleep_by_schedule = true #"是否根据日程表进行睡觉" fixed_sleep_time = "23:00" #"固定的睡觉时间" fixed_wake_up_time = "07:00" #"固定的起床时间" +sleep_time_offset_minutes = 15 #"睡觉时间随机偏移量范围(分钟),实际睡觉时间会在±该值范围内随机" +wake_up_time_offset_minutes = 15 #"起床时间随机偏移量范围(分钟),实际起床时间会在±该值范围内随机" wakeup_threshold = 15.0 #唤醒阈值,达到此值时会被唤醒" private_message_increment = 3.0 #"私聊消息增加的唤醒度" group_mention_increment = 2.0 #"群聊艾特增加的唤醒度" From da4504c0ec4367e929c5ed4db70eadb14caf0db4 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Sun, 7 Sep 2025 12:13:00 +0800 Subject: [PATCH 58/69] =?UTF-8?q?feat(at=5Fuser=5Fplugin):=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E5=9B=9E=E5=A4=8D=E5=99=A8=E7=94=9F=E6=88=90=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E8=89=BE=E7=89=B9=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 艾特用户动作现在通过回复器生成智能回复内容,而不是直接发送固定消息。 这样可以使艾特消息更具上下文感知能力和智能化,提升用户体验。 增加了错误处理机制,确保在回复生成失败或聊天流不存在时能正确处理。 --- src/plugins/built_in/at_user_plugin/plugin.py | 77 ++++++++++++++++--- 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/src/plugins/built_in/at_user_plugin/plugin.py b/src/plugins/built_in/at_user_plugin/plugin.py index 4b422a525..1611486be 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -60,19 +60,72 @@ class AtAction(BaseAction): if not user_info or not user_info.get("user_id"): logger.info(f"找不到名为 '{user_name}' 的用户。") return False, "用户不存在" - await self.send_command( - "SEND_AT_MESSAGE", - args={"qq_id": user_info.get("user_id"), "text": at_message}, - display_message=f"艾特用户 {user_name} 并发送消息: {at_message}", - ) - await self.store_action_info( - action_build_into_prompt=True, - action_prompt_display=f"执行了艾特用户动作:艾特用户 {user_name} 并发送消息: {at_message}", - action_done=True, - ) - logger.info("艾特用户的动作已触发,但具体实现待完成。") - return True, "艾特用户的动作已触发,但具体实现待完成。" + try: + # 使用回复器生成艾特回复,而不是直接发送命令 + from src.chat.replyer.default_generator import DefaultReplyer + from src.chat.message_receive.chat_stream import get_chat_manager + + # 获取当前聊天流 + chat_manager = get_chat_manager() + chat_stream = chat_manager.get_stream(self.chat_id) + + if not chat_stream: + logger.error(f"找不到聊天流: {self.stream_id}") + return False, "聊天流不存在" + + # 创建回复器实例 + replyer = DefaultReplyer(chat_stream) + + # 构建回复对象,将艾特消息作为回复目标 + reply_to = f"{user_name}:{at_message}" + extra_info = f"你需要艾特用户 {user_name} 并回复他们说: {at_message}" + + # 使用回复器生成回复 + success, llm_response, prompt = await replyer.generate_reply_with_context( + reply_to=reply_to, + extra_info=extra_info, + enable_tool=False, # 艾特回复通常不需要工具调用 + from_plugin=True # 标识来自插件 + ) + + if success and llm_response: + # 获取生成的回复内容 + reply_content = llm_response.get("content", "") + if reply_content: + # 获取用户QQ号,发送真正的艾特消息 + user_id = user_info.get("user_id") + + # 发送真正的艾特命令,使用回复器生成的智能内容 + await self.send_command( + "SEND_AT_MESSAGE", + args={"qq_id": user_id, "text": reply_content}, + display_message=f"艾特用户 {user_name} 并发送智能回复: {reply_content}", + ) + + await self.store_action_info( + action_build_into_prompt=True, + action_prompt_display=f"执行了艾特用户动作:艾特用户 {user_name} 并发送智能回复: {reply_content}", + action_done=True, + ) + + logger.info(f"成功通过回复器生成智能内容并发送真正的艾特消息给 {user_name}: {reply_content}") + return True, "智能艾特消息发送成功" + else: + logger.warning("回复器生成了空内容") + return False, "回复内容为空" + else: + logger.error("回复器生成回复失败") + return False, "回复生成失败" + + except Exception as e: + logger.error(f"执行艾特用户动作时发生异常: {e}", exc_info=True) + await self.store_action_info( + action_build_into_prompt=True, + action_prompt_display=f"执行艾特用户动作失败:{str(e)}", + action_done=False, + ) + return False, f"执行失败: {str(e)}" class AtCommand(BaseCommand): From c1cb7aafea892bcfff17c83d3c0d4e25e9151361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=85=E8=AF=BA=E7=8B=90?= <212194964+foxcyber907@users.noreply.github.com> Date: Sun, 7 Sep 2025 12:16:11 +0800 Subject: [PATCH 59/69] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=81=A5=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/remote.py | 165 +++++++++++++++++++++++++++++++++++++++++++ src/main.py | 4 ++ 2 files changed, 169 insertions(+) create mode 100644 src/common/remote.py diff --git a/src/common/remote.py b/src/common/remote.py new file mode 100644 index 000000000..f38791f7f --- /dev/null +++ b/src/common/remote.py @@ -0,0 +1,165 @@ +import asyncio + +import aiohttp +import platform + +from datetime import datetime, timezone +from src.common.logger import get_logger +from src.common.tcp_connector import get_tcp_connector +from src.config.config import global_config +from src.manager.async_task_manager import AsyncTask +from src.manager.local_store_manager import local_storage + +logger = get_logger("remote") + +TELEMETRY_SERVER_URL = "http://124.248.67.228:10058" +"""遥测服务地址""" + + +class TelemetryHeartBeatTask(AsyncTask): + HEARTBEAT_INTERVAL = 300 + + def __init__(self): + super().__init__(task_name="Telemetry Heart Beat Task", run_interval=self.HEARTBEAT_INTERVAL) + self.server_url = TELEMETRY_SERVER_URL + """遥测服务地址""" + + self.client_uuid: str | None = local_storage["mmc_uuid"] if "mmc_uuid" in local_storage else None # type: ignore + """客户端UUID""" + + self.info_dict = self._get_sys_info() + """系统信息字典""" + + @staticmethod + def _get_sys_info() -> dict[str, str]: + """获取系统信息""" + info_dict = { + "os_type": "Unknown", + "py_version": platform.python_version(), + "mmc_version": global_config.MMC_VERSION, + } + + match platform.system(): + case "Windows": + info_dict["os_type"] = "Windows" + case "Linux": + info_dict["os_type"] = "Linux" + case "Darwin": + info_dict["os_type"] = "macOS" + case _: + info_dict["os_type"] = "Unknown" + + return info_dict + + async def _req_uuid(self) -> bool: + """ + 向服务端请求UUID(不应在已存在UUID的情况下调用,会覆盖原有的UUID) + """ + + if "deploy_time" not in local_storage: + logger.error("本地存储中缺少部署时间,无法请求UUID") + return False + + try_count: int = 0 + while True: + # 如果不存在,则向服务端请求一个新的UUID(注册客户端) + logger.info("正在向遥测服务端请求UUID...") + + try: + async with aiohttp.ClientSession(connector=await get_tcp_connector()) as session: + async with session.post( + f"{TELEMETRY_SERVER_URL}/stat/reg_client", + json={"deploy_time": datetime.fromtimestamp(local_storage["deploy_time"], tz=timezone.utc).isoformat()}, + timeout=aiohttp.ClientTimeout(total=5), # 设置超时时间为5秒 + ) as response: + logger.debug(f"{TELEMETRY_SERVER_URL}/stat/reg_client") + logger.debug(local_storage["deploy_time"]) # type: ignore + logger.debug(f"Response status: {response.status}") + + if response.status == 200: + data = await response.json() + if client_id := data.get("mmc_uuid"): + # 将UUID存储到本地 + local_storage["mmc_uuid"] = client_id + self.client_uuid = client_id + logger.info(f"成功获取UUID: {self.client_uuid}") + return True # 成功获取UUID,返回True + else: + logger.error("无效的服务端响应") + else: + response_text = await response.text() + logger.error( + f"请求UUID失败,不过你还是可以正常使用墨狐,状态码: {response.status}, 响应内容: {response_text}" + ) + except Exception as e: + import traceback + + error_msg = str(e) or "未知错误" + logger.warning( + f"请求UUID出错,不过你还是可以正常使用墨狐: {type(e).__name__}: {error_msg}" + ) # 可能是网络问题 + logger.debug(f"完整错误信息: {traceback.format_exc()}") + + # 请求失败,重试次数+1 + try_count += 1 + if try_count > 3: + # 如果超过3次仍然失败,则退出 + logger.error("获取UUID失败,请检查网络连接或服务端状态") + return False + else: + # 如果可以重试,等待后继续(指数退避) + logger.info(f"获取UUID失败,将于 {4**try_count} 秒后重试...") + await asyncio.sleep(4**try_count) + + async def _send_heartbeat(self): + """向服务器发送心跳""" + headers = { + "Client-UUID": self.client_uuid, + "User-Agent": f"HeartbeatClient/{self.client_uuid[:8]}", # type: ignore + } + + logger.debug(f"正在发送心跳到服务器: {self.server_url}") + logger.debug(str(headers)) + + try: + async with aiohttp.ClientSession(connector=await get_tcp_connector()) as session: + async with session.post( + f"{self.server_url}/stat/client_heartbeat", + headers=headers, + json=self.info_dict, + timeout=aiohttp.ClientTimeout(total=5), # 设置超时时间为5秒 + ) as response: + logger.debug(f"Response status: {response.status}") + + # 处理响应 + if 200 <= response.status < 300: + # 成功 + logger.debug(f"心跳发送成功,状态码: {response.status}") + elif response.status == 403: + # 403 Forbidden + logger.warning( + "(此消息不会影响正常使用)心跳发送失败,403 Forbidden: 可能是UUID无效或未注册。" + "处理措施:重置UUID,下次发送心跳时将尝试重新注册。" + ) + self.client_uuid = None + del local_storage["mmc_uuid"] # 删除本地存储的UUID + else: + # 其他错误 + response_text = await response.text() + logger.warning( + f"(此消息不会影响正常使用)状态未发送,状态码: {response.status}, 响应内容: {response_text}" + ) + except Exception as e: + import traceback + + error_msg = str(e) or "未知错误" + logger.warning(f"(此消息不会影响正常使用)状态未发生: {type(e).__name__}: {error_msg}") + logger.debug(f"完整错误信息: {traceback.format_exc()}") + + async def run(self): + # 发送心跳 + if self.client_uuid is None and not await self._req_uuid(): + logger.warning("获取UUID失败,跳过此次心跳") + return + + await self._send_heartbeat() \ No newline at end of file diff --git a/src/main.py b/src/main.py index d76dfd0d3..f641c54e4 100644 --- a/src/main.py +++ b/src/main.py @@ -7,6 +7,7 @@ from maim_message import MessageServer from src.manager.async_task_manager import async_task_manager from src.chat.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask +from src.common.remote import TelemetryHeartBeatTask from src.chat.emoji_system.emoji_manager import get_emoji_manager from src.chat.message_receive.chat_stream import get_chat_manager from src.config.config import global_config @@ -202,6 +203,9 @@ MoFox_Bot(第三方修改版) # 添加统计信息输出任务 await async_task_manager.add_task(StatisticOutputTask()) + + # 添加遥测心跳任务 + await async_task_manager.add_task(TelemetryHeartBeatTask()) # 注册默认事件 event_manager.init_default_events() From 1e2eb977bbee74821990de7fc80fd0a250c6a877 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sun, 7 Sep 2025 12:28:15 +0800 Subject: [PATCH 60/69] =?UTF-8?q?refactor(sleep):=20=E8=A7=A3=E8=80=A6`Sle?= =?UTF-8?q?epManager`=E4=B8=8E`ScheduleManager`=E7=9A=84=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E5=85=B3=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将日程表获取的逻辑从`SleepManager`转移到`TimeChecker`中,并使其直接从全局的`schedule_manager`实例获取日程,消除了`SleepManager`对日程表的直接管理责任。 这一重构简化了`SleepManager`的职责,使其更专注于睡眠状态的管理,同时提高了模块间的独立性。 - `SleepManager`不再持有`today_schedule`状态。 - `TimeChecker`现在直接从`schedule_manager`获取日程,不再依赖于`SleepManager`传递。 - 移除了`ScheduleManager`中对`sleep_manager`的引用和更新操作。 --- .../chat_loop/sleep_manager/sleep_manager.py | 11 +---------- .../chat_loop/sleep_manager/time_checker.py | 18 +++++++++++------- src/schedule/schedule_manager.py | 4 ---- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/chat/chat_loop/sleep_manager/sleep_manager.py b/src/chat/chat_loop/sleep_manager/sleep_manager.py index 7eb1d3d61..3bf099be7 100644 --- a/src/chat/chat_loop/sleep_manager/sleep_manager.py +++ b/src/chat/chat_loop/sleep_manager/sleep_manager.py @@ -25,8 +25,7 @@ class SleepManager: """ 初始化睡眠管理器。 """ - self.time_checker = TimeChecker(self) # 时间检查器,用于判断当前是否处于理论睡眠时间 - self.today_schedule: Optional[List[Dict[str, Any]]] = None # 当天的日程安排 + self.time_checker = TimeChecker() # 时间检查器,用于判断当前是否处于理论睡眠时间 self.last_sleep_log_time = 0 # 上次记录睡眠日志的时间戳 self.sleep_log_interval = 35 # 睡眠日志记录间隔(秒) @@ -284,14 +283,6 @@ class SleepManager: logger.info(f"将在 {re_sleep_delay_minutes} 分钟后尝试重新入睡。") self._save_sleep_state() - def get_today_schedule(self) -> Optional[List[Dict[str, Any]]]: - """获取今天的日程安排。""" - return self.today_schedule - - def update_today_schedule(self, schedule: Optional[List[Dict[str, Any]]]): - """更新今天的日程安排。""" - self.today_schedule = schedule - def _save_sleep_state(self): """将当前所有睡眠相关的状态打包并保存到本地存储。""" state_data = { diff --git a/src/chat/chat_loop/sleep_manager/time_checker.py b/src/chat/chat_loop/sleep_manager/time_checker.py index 32521d408..cbe3d45e8 100644 --- a/src/chat/chat_loop/sleep_manager/time_checker.py +++ b/src/chat/chat_loop/sleep_manager/time_checker.py @@ -1,19 +1,19 @@ from datetime import datetime, time, timedelta -from typing import Optional +from typing import Optional, List, Dict, Any import random from src.common.logger import get_logger from src.config.config import global_config +from src.schedule.schedule_manager import schedule_manager logger = get_logger("time_checker") class TimeChecker: - def __init__(self, schedule_source): - self.schedule_source = schedule_source + def __init__(self): # 缓存当天的偏移量,确保一天内使用相同的偏移量 - self._daily_sleep_offset = None - self._daily_wake_offset = None + self._daily_sleep_offset: int = 0 + self._daily_wake_offset: int = 0 self._offset_date = None def _get_daily_offsets(self): @@ -34,9 +34,13 @@ class TimeChecker: return self._daily_sleep_offset, self._daily_wake_offset + def get_today_schedule(self) -> Optional[List[Dict[str, Any]]]: + """从全局 ScheduleManager 获取今天的日程安排。""" + return schedule_manager.today_schedule + def is_in_theoretical_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]: if global_config.sleep_system.sleep_by_schedule: - if self.schedule_source.get_today_schedule(): + if self.get_today_schedule(): return self._is_in_schedule_sleep_time(now_time) else: return self._is_in_sleep_time(now_time) @@ -46,7 +50,7 @@ class TimeChecker: def _is_in_schedule_sleep_time(self, now_time: time) -> tuple[bool, Optional[str]]: """检查当前时间是否落在日程表的任何一个睡眠活动中""" sleep_keywords = ["休眠", "睡觉", "梦乡"] - today_schedule = self.schedule_source.get_today_schedule() + today_schedule = self.get_today_schedule() if today_schedule: for event in today_schedule: try: diff --git a/src/schedule/schedule_manager.py b/src/schedule/schedule_manager.py index 491f106fa..f97d7c03c 100644 --- a/src/schedule/schedule_manager.py +++ b/src/schedule/schedule_manager.py @@ -7,7 +7,6 @@ from src.common.database.sqlalchemy_models import Schedule, get_db_session from src.config.config import global_config from src.common.logger import get_logger from src.manager.async_task_manager import AsyncTask, async_task_manager -from ..chat.chat_loop.sleep_manager.sleep_manager import SleepManager from .database import update_plan_usage from .llm_generator import ScheduleLLMGenerator from .plan_manager import PlanManager @@ -23,7 +22,6 @@ class ScheduleManager: self.plan_manager = PlanManager() self.daily_task_started = False self.schedule_generation_running = False - self.sleep_manager = SleepManager() async def start_daily_schedule_generation(self): if not self.daily_task_started: @@ -45,7 +43,6 @@ class ScheduleManager: schedule_data = self._load_schedule_from_db(today_str) if schedule_data: self.today_schedule = schedule_data - self.sleep_manager.update_today_schedule(self.today_schedule) self._log_loaded_schedule(today_str) return @@ -100,7 +97,6 @@ class ScheduleManager: if schedule_data: self._save_schedule_to_db(today_str, schedule_data) self.today_schedule = schedule_data - self.sleep_manager.update_today_schedule(self.today_schedule) self._log_generated_schedule(today_str, schedule_data) if sampled_plans: From 905afec9d5bc21415ab0c8b66aac13cc48d7475d Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sun, 7 Sep 2025 12:41:12 +0800 Subject: [PATCH 61/69] =?UTF-8?q?refactor(proactive):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=B8=BB=E5=8A=A8=E5=AF=B9=E8=AF=9D=E7=9A=84prompt=E5=B9=B6?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=93=8D=E5=BA=94=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新并细化了主动对话生成器的prompt,使其更具指导性,以生成更自然、更简洁的回复。 - 移除了冗余的注释和空行,使prompt结构更清晰。 - 对生成器返回的`response_text`调用`process_human_text`方法进行处理,以应用全局的文本分割和错别字修正配置,确保输出格式统一。 --- .../chat_loop/proactive/proactive_thinker.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index 6f5f60825..c243a9caf 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -8,6 +8,7 @@ from src.plugin_system.base.component_types import ChatMode from ..hfc_context import HfcContext from .events import ProactiveTriggerEvent from src.plugin_system.apis import generator_api +from src.plugin_system.apis.generator_api import process_human_text from src.schedule.schedule_manager import schedule_manager from src.plugin_system import tool_api from src.plugin_system.base.component_types import ComponentType @@ -184,8 +185,6 @@ class ProactiveThinker: mood_block = f"你现在的心情是:{mood_manager.get_mood_by_chat_id(self.context.stream_id).mood_state}" final_prompt = f""" -# 主动对话生成 - ## 你的角色 {identity_block} @@ -209,6 +208,12 @@ class ProactiveThinker: - 巧妙地将日程安排或最新信息融入到你的开场白中。 - 风格要符合你的角色设定。 - 直接输出你想要说的内容,不要包含其他额外信息。 + +你的回复应该: +1. 可以分享你的看法、提出相关问题,或者开个合适的玩笑。 +2. 目的是让对话更有趣、更深入。 +3. 不要浮夸,不要夸张修辞,不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。 +最终请输出一条简短、完整且口语化的回复。 """ # 5. 调用生成器API并发送 @@ -219,8 +224,11 @@ class ProactiveThinker: ) if response_text: - # 不要将纯文本包装成 ResponseSet 格式! - response_set = [response_text] + response_set = process_human_text( + content=response_text, + enable_splitter=global_config.response_splitter.enable, + enable_chinese_typo=global_config.chinese_typo.enable, + ) await self.cycle_processor.response_handler.send_response( response_set, time.time(), action_result.get("action_message") ) From ece6699a2aa8c1d6a7232527e87236776d37c1ae Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sun, 7 Sep 2025 12:43:34 +0800 Subject: [PATCH 62/69] =?UTF-8?q?feat(prompt):=20=E4=B8=B0=E5=AF=8C?= =?UTF-8?q?=E4=B8=BB=E5=8A=A8=E5=AF=B9=E8=AF=9D=E4=B8=AD=E7=9A=84=E4=BA=BA?= =?UTF-8?q?=E8=AE=BEprompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将`personality_side`、`identity`和`reply_style`添加到主动对话的`identity_block`中,以生成更具个性化的对话内容。 --- src/chat/chat_loop/proactive/proactive_thinker.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index c243a9caf..66236bb32 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -181,7 +181,12 @@ class ProactiveThinker: # 4. 构建最终的生成提示词 bot_name = global_config.bot.nickname - identity_block = f"你的名字是{bot_name},你{global_config.personality.personality_core}:" + personality = global_config.personality + identity_block = ( + f"你的名字是{bot_name}。\n" + f"关于你:{personality.personality_core},并且{personality.personality_side}。\n" + f"你的身份是{personality.identity},平时说话风格是{personality.reply_style}。" + ) mood_block = f"你现在的心情是:{mood_manager.get_mood_by_chat_id(self.context.stream_id).mood_state}" final_prompt = f""" From 22e99607076365a952dce30bb9e05369809c0d88 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sun, 7 Sep 2025 12:44:49 +0800 Subject: [PATCH 63/69] =?UTF-8?q?refactor(web=5Fsearch):=20=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E6=90=9C=E7=B4=A2=E5=BC=95=E6=93=8E=E7=9A=84API?= =?UTF-8?q?=E5=AF=86=E9=92=A5=E9=85=8D=E7=BD=AE=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将Exa和Tavily搜索引擎的API密钥配置路径从独立的顶级配置(如`exa.api_keys`)更改为`web_search`下的统一路径(`web_search.exa_api_keys`和`web_search.tavily_api_keys`)。 这使得配置结构更加清晰和一致,便于管理所有与网络搜索相关的凭据。 --- src/plugins/built_in/web_search_tool/engines/exa_engine.py | 2 +- src/plugins/built_in/web_search_tool/engines/tavily_engine.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/built_in/web_search_tool/engines/exa_engine.py b/src/plugins/built_in/web_search_tool/engines/exa_engine.py index 2bb515e8e..7327afaeb 100644 --- a/src/plugins/built_in/web_search_tool/engines/exa_engine.py +++ b/src/plugins/built_in/web_search_tool/engines/exa_engine.py @@ -26,7 +26,7 @@ class ExaSearchEngine(BaseSearchEngine): def _initialize_clients(self): """初始化Exa客户端""" # 从主配置文件读取API密钥 - exa_api_keys = config_api.get_global_config("exa.api_keys", None) + exa_api_keys = config_api.get_global_config("web_search.exa_api_keys", None) # 创建API密钥管理器 self.api_manager = create_api_key_manager_from_config( diff --git a/src/plugins/built_in/web_search_tool/engines/tavily_engine.py b/src/plugins/built_in/web_search_tool/engines/tavily_engine.py index affb303fc..d7cf61d6c 100644 --- a/src/plugins/built_in/web_search_tool/engines/tavily_engine.py +++ b/src/plugins/built_in/web_search_tool/engines/tavily_engine.py @@ -25,7 +25,7 @@ class TavilySearchEngine(BaseSearchEngine): def _initialize_clients(self): """初始化Tavily客户端""" # 从主配置文件读取API密钥 - tavily_api_keys = config_api.get_global_config("tavily.api_keys", None) + tavily_api_keys = config_api.get_global_config("web_search.tavily_api_keys", None) # 创建API密钥管理器 self.api_manager = create_api_key_manager_from_config( From e14bd950c38de985aa276b01c645979fb6152ac7 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sun, 7 Sep 2025 12:53:06 +0800 Subject: [PATCH 64/69] =?UTF-8?q?refactor(planner):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=A4=A7=E5=B0=8F=E8=84=91=E8=A7=84=E5=88=92=E5=99=A8=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E4=BB=A5=E7=AE=80=E5=8C=96=E5=86=B3=E7=AD=96=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次重构删除了`PlannerType`(大小脑规划器)的枚举及其在动作规划和组件定义中的相关逻辑。通过移除大小脑的概念,简化了`ActionPlanner`的决策过程,使其不再需要根据规划器类型来筛选可用动作。 这一变更统一了动作的处理方式,降低了系统的复杂性,使得未来的功能扩展和维护更加直接和清晰。 --- src/chat/planner_actions/planner.py | 10 +--------- src/plugin_system/base/base_action.py | 3 +-- src/plugin_system/base/component_types.py | 12 ------------ 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index a84f2581c..0f9703bab 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -28,7 +28,6 @@ from src.plugin_system.base.component_types import ( ChatMode, ComponentType, ActionActivationType, - PlannerType, ) from src.plugin_system.core.component_registry import component_registry from src.schedule.schedule_manager import schedule_manager @@ -498,8 +497,6 @@ class ActionPlanner: try: sub_planner_actions: Dict[str, ActionInfo] = {} for action_name, action_info in available_actions.items(): - if action_info.planner_type not in [PlannerType.SMALL_BRAIN, PlannerType.ALL]: - continue if action_info.activation_type in [ActionActivationType.LLM_JUDGE, ActionActivationType.ALWAYS]: sub_planner_actions[action_name] = action_info @@ -548,15 +545,10 @@ class ActionPlanner: # --- 3. 大脑独立思考是否回复 --- action, reasoning, action_data, target_message = "no_reply", "大脑初始化默认", {}, None try: - big_brain_actions = { - name: info - for name, info in available_actions.items() - if info.planner_type in [PlannerType.BIG_BRAIN, PlannerType.ALL] - } prompt, _ = await self.build_planner_prompt( is_group_chat=is_group_chat, chat_target_info=chat_target_info, - current_available_actions=big_brain_actions, + current_available_actions={}, mode=mode, chat_content_block_override=chat_content_block, message_id_list_override=message_id_list, diff --git a/src/plugin_system/base/base_action.py b/src/plugin_system/base/base_action.py index 38d7f5a5b..cc9477c3d 100644 --- a/src/plugin_system/base/base_action.py +++ b/src/plugin_system/base/base_action.py @@ -6,7 +6,7 @@ from typing import Tuple, Optional from src.common.logger import get_logger from src.chat.message_receive.chat_stream import ChatStream -from src.plugin_system.base.component_types import ActionActivationType, ChatMode, ActionInfo, ComponentType, ChatType,PlannerType +from src.plugin_system.base.component_types import ActionActivationType, ChatMode, ActionInfo, ComponentType, ChatType, from src.plugin_system.apis import send_api, database_api, message_api @@ -92,7 +92,6 @@ class BaseAction(ABC): self.parallel_action: bool = getattr(self.__class__, "parallel_action", True) self.associated_types: list[str] = getattr(self.__class__, "associated_types", []).copy() self.chat_type_allow: ChatType = getattr(self.__class__, "chat_type_allow", ChatType.ALL) - self.planner_type: PlannerType = getattr(self.__class__, "planner_type", ChatType.ALL) # ============================================================================= diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index 5737b4fb6..0bcb0060e 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -51,17 +51,6 @@ class ChatMode(Enum): # 聊天类型枚举 -class PlannerType(Enum): - """规划器类型枚举""" - - BIG_BRAIN = "big_brain" # 大脑,负责宏观决策 - SMALL_BRAIN = "small_brain" # 小脑,负责具体动作 - ALL = "all" # 通用 - - def __str__(self): - return self.value - - class ChatType(Enum): """聊天类型枚举,用于限制插件在不同聊天环境中的使用""" @@ -152,7 +141,6 @@ class ActionInfo(ComponentInfo): mode_enable: ChatMode = ChatMode.ALL parallel_action: bool = False chat_type_allow: ChatType = ChatType.ALL # 允许的聊天类型 - planner_type: PlannerType = PlannerType.ALL def __post_init__(self): super().__post_init__() From dc67eb68fc7dbcc5882d11858b29e538809b9846 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sun, 7 Sep 2025 13:02:10 +0800 Subject: [PATCH 65/69] =?UTF-8?q?feat(planner):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=B8=BB=E5=8A=A8=E5=AF=B9=E8=AF=9D=E5=86=B3=E7=AD=96=E5=B9=B6?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=8A=A8=E4=BD=9C=E5=8E=86=E5=8F=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在主动对话规划器中引入最近的动作历史作为决策依据,以避免重复或不合时宜的主动行为。同时,在主动回复后,将该行为作为动作信息存储到数据库中,以便于未来的决策和分析。 - 在 `ActionPlanner` 中,获取并向prompt中添加最近5条动作历史记录 - 在 `ProactiveThinker` 中,当主动回复成功后,调用 `store_action_info` 记录动作 - 移除了 `@user` 插件中已废弃的 `planner_type` 属性 --- src/chat/chat_loop/proactive/proactive_thinker.py | 8 ++++++++ src/chat/planner_actions/planner.py | 13 +++++++++++++ src/plugins/built_in/at_user_plugin/plugin.py | 1 - 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index 66236bb32..3522c0dd4 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -15,6 +15,7 @@ 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 if TYPE_CHECKING: from ..cycle_processor import CycleProcessor @@ -237,6 +238,13 @@ class ProactiveThinker: await self.cycle_processor.response_handler.send_response( response_set, time.time(), action_result.get("action_message") ) + await store_action_info( + chat_stream=self.context.chat_stream, + action_name="proactive_reply", + action_data={"topic": topic, "response": response_text}, + action_prompt_display=f"主动发起对话: {topic}", + action_done=True, + ) else: logger.error(f"{self.context.log_prefix} 主动思考生成回复失败。") diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 0f9703bab..9b071d954 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -98,6 +98,9 @@ def init_prompt(): ## 最近的聊天内容 {chat_content_block} +## 最近的动作历史 +{actions_before_now_block} + ## 任务 基于以上所有信息(特别是最近的聊天内容),分析当前情况,决定是否适合主动开启一个**新的、但又与当前氛围相关**的话题。 @@ -678,6 +681,15 @@ class ActionPlanner: ) prompt_template = await global_prompt_manager.get_prompt_async("proactive_planner_prompt") + actions_before_now = get_actions_by_timestamp_with_chat( + chat_id=self.chat_id, + timestamp_start=time.time() - 3600, + timestamp_end=time.time(), + limit=5, + ) + actions_before_now_block = build_readable_actions(actions=actions_before_now) + actions_before_now_block = f"你刚刚选择并执行过的action是:\n{actions_before_now_block}" + prompt = prompt_template.format( time_block=time_block, identity_block=identity_block, @@ -685,6 +697,7 @@ class ActionPlanner: mood_block=mood_block, long_term_memory_block=long_term_memory_block, chat_content_block=chat_content_block or "最近没有聊天内容。", + actions_before_now_block=actions_before_now_block, ) return prompt, [] diff --git a/src/plugins/built_in/at_user_plugin/plugin.py b/src/plugins/built_in/at_user_plugin/plugin.py index 1611486be..48262cd04 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -24,7 +24,6 @@ class AtAction(BaseAction): activation_type = ActionActivationType.LLM_JUDGE # 消息接收时激活(?) parallel_action = False chat_type_allow = ChatType.GROUP - planner_type = PlannerType.BIG_BRAIN # === 功能描述(必须填写)=== action_parameters = {"user_name": "需要艾特用户的名字", "at_message": "艾特用户时要发送的消,注意消息里不要有@"} From 6d891f2d54fdd8d0d83f39e71a2ab93b1e4ad731 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sun, 7 Sep 2025 13:03:51 +0800 Subject: [PATCH 66/69] 1 --- src/plugins/built_in/at_user_plugin/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/built_in/at_user_plugin/plugin.py b/src/plugins/built_in/at_user_plugin/plugin.py index 48262cd04..8329480a7 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -10,7 +10,7 @@ from src.plugin_system import ( ) from src.person_info.person_info import get_person_info_manager from src.common.logger import get_logger -from src.plugin_system.base.component_types import ChatType,PlannerType +from src.plugin_system.base.component_types import ChatType logger = get_logger(__name__) From 14fe86d93875ca2ea88d7f24405231f85b10493b Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Sun, 7 Sep 2025 13:14:48 +0800 Subject: [PATCH 67/69] =?UTF-8?q?refactor(plugin=5Fsystem):=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=20ChatType=20?= =?UTF-8?q?=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugin_system/base/base_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugin_system/base/base_action.py b/src/plugin_system/base/base_action.py index cc9477c3d..5962d69fe 100644 --- a/src/plugin_system/base/base_action.py +++ b/src/plugin_system/base/base_action.py @@ -6,7 +6,7 @@ from typing import Tuple, Optional from src.common.logger import get_logger from src.chat.message_receive.chat_stream import ChatStream -from src.plugin_system.base.component_types import ActionActivationType, ChatMode, ActionInfo, ComponentType, ChatType, +from src.plugin_system.base.component_types import ActionActivationType, ChatMode, ActionInfo, ComponentType, ChatType from src.plugin_system.apis import send_api, database_api, message_api From c5ab307c36e48c7d8994781ec519f6e24340325b Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sun, 7 Sep 2025 14:02:28 +0800 Subject: [PATCH 68/69] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E8=BF=98=E6=B2=A1=E6=9C=89=E5=AE=8C=E6=88=90=E7=9A=84=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=80=A7=E5=8A=9F=E8=83=BD,=E6=88=91=E8=A6=81?= =?UTF-8?q?=E5=85=88=E8=B7=91=E8=B7=AF=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/frequency_analyzer/analyzer.py | 144 ++++++++++++++++++++++++ src/chat/frequency_analyzer/tracker.py | 77 +++++++++++++ src/chat/frequency_analyzer/trigger.py | 119 ++++++++++++++++++++ 3 files changed, 340 insertions(+) create mode 100644 src/chat/frequency_analyzer/analyzer.py create mode 100644 src/chat/frequency_analyzer/tracker.py create mode 100644 src/chat/frequency_analyzer/trigger.py 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("基于聊天频率的主动思考触发器已停止。") From ab180db1ddeff0dc028c2f522942adf03c6300fd Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sun, 7 Sep 2025 15:02:51 +0800 Subject: [PATCH 69/69] =?UTF-8?q?at=E7=8E=B0=E5=9C=A8=E5=8F=AF=E4=BB=A5?= =?UTF-8?q?=E8=A7=A6=E5=8F=91post=5Fllm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/built_in/at_user_plugin/plugin.py | 7 +++ test_plugin_config_fix.py | 53 ------------------- 2 files changed, 7 insertions(+), 53 deletions(-) delete mode 100644 test_plugin_config_fix.py diff --git a/src/plugins/built_in/at_user_plugin/plugin.py b/src/plugins/built_in/at_user_plugin/plugin.py index 8329480a7..5e24458ef 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -80,6 +80,13 @@ 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, 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