From 275157c58ae470d6a58977ebb4ec60a9bd9b0927 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 3 Oct 2025 17:15:34 +0800 Subject: [PATCH 01/14] =?UTF-8?q?refactor(proactive=5Fthinker):=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=B8=8A=E4=B8=8B=E6=96=87=E6=94=B6=E9=9B=86?= =?UTF-8?q?=E4=B8=8E=E6=8F=90=E7=A4=BA=E8=AF=8D=E6=9E=84=E5=BB=BA=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=BB=A5=E6=94=AF=E6=8C=81=E7=BE=A4=E8=81=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原有的上下文收集函数 `_gather_context` 和提示词构建函数 `_make_decision`, `_build_plan_prompt` 拆分为更细粒度的模块,以分别处理私聊和群聊场景。 主要变更: - `_gather_context`: 现在能根据聊天流是群聊还是私聊,收集不同的上下文信息,并添加 `chat_type` 字段以作区分。 - `_build_decision_prompt`: 新增函数,根据 `chat_type` 构建不同的决策提示词,使决策更贴合场景。 - `_build_plan_prompt`: 重构为调度函数,内部调用新增的 `_build_private_plan_prompt` 和 `_build_group_plan_prompt` 来生成特定场景的规划提示词。 - 整体逻辑更加清晰,增强了代码的可读性和可扩展性,为未来支持更多聊天类型奠定了基础。 --- .../proactive_thinker_executor.py | 228 ++++++++++++------ 1 file changed, 151 insertions(+), 77 deletions(-) diff --git a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py index 30ffb3a60..1f83d75e3 100644 --- a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py +++ b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py @@ -104,61 +104,31 @@ class ProactiveThinkerExecutor: async def _gather_context(self, stream_id: str) -> dict[str, Any] | None: """ - 收集构建提示词所需的所有上下文信息 + 收集构建提示词所需的所有上下文信息. + 根据聊天流是私聊还是群聊, 收集不同的上下文. """ stream = self._get_stream_from_id(stream_id) if not stream: logger.warning(f"无法找到 stream_id 为 {stream_id} 的聊天流") return None - user_info = stream.user_info - if not user_info or not user_info.platform or not user_info.user_id: - logger.warning(f"Stream {stream_id} 的 user_info 不完整") - return None - - person_id = person_api.get_person_id(user_info.platform, int(user_info.user_id)) - person_info_manager = get_person_info_manager() - - # 获取日程 + # 1. 收集通用信息 (日程, 聊天历史, 动作历史) schedules = await schedule_api.ScheduleAPI.get_today_schedule() - schedule_context = ( - "\n".join( - [ - f"- {s.get('time_range', '未知时间')}: {s.get('activity', '未知活动')}" - for s in schedules - ] - ) - if schedules - else "今天没有日程安排。" - ) - - # 获取关系信息 - short_impression = await person_info_manager.get_value(person_id, "short_impression") or "无" - impression = await person_info_manager.get_value(person_id, "impression") or "无" - attitude = await person_info_manager.get_value(person_id, "attitude") or 50 - - # 获取最近聊天记录 + schedule_context = "\n".join([f"- {s.get('time_range', '未知时间')}: {s.get('activity', '未知活动')}" for s in schedules]) if schedules else "今天没有日程安排。" + recent_messages = await message_api.get_recent_messages(stream_id, limit=10) - recent_chat_history = ( - await message_api.build_readable_messages_to_str(recent_messages) if recent_messages else "无" - ) - - # 获取最近的动作历史 - action_history = await database_api.db_query( + recent_chat_history = await message_api.build_readable_messages_to_str(recent_messages) if recent_messages else "无" + + action_history_list = await database_api.db_query( database_api.MODEL_MAPPING["ActionRecords"], filters={"chat_id": stream_id, "action_name": "proactive_decision"}, limit=3, order_by=["-time"], ) - action_history_context = "无" - if isinstance(action_history, list): - action_history_context = ( - "\n".join([f"- {a['action_data']}" for a in action_history if isinstance(a, dict)]) or "无" - ) + action_history_context = "\n".join([f"- {a['action_data']}" for a in action_history_list if isinstance(a, dict)]) if isinstance(action_history_list, list) else "无" - return { - "person_id": person_id, - "user_info": user_info, + # 2. 构建基础上下文 + base_context = { "schedule_context": schedule_context, "recent_chat_history": recent_chat_history, "action_history_context": action_history_context, @@ -170,21 +140,63 @@ class ProactiveThinkerExecutor: "current_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), } - async def _make_decision(self, context: dict[str, Any], start_mode: str) -> dict[str, Any] | None: - """ - 决策模块:判断是否应该主动发起对话,以及聊什么话题 - """ - persona = context["persona"] - user_info = context["user_info"] - relationship = context["relationship"] + # 3. 根据聊天类型补充特定上下文 + if stream.group_info: # 群聊场景 + base_context.update({ + "chat_type": "group", + "group_info": { + "group_name": stream.group_info.group_name, + "group_id": stream.group_info.group_id + } + }) + return base_context + elif stream.user_info: # 私聊场景 + user_info = stream.user_info + if not user_info.platform or not user_info.user_id: + logger.warning(f"Stream {stream_id} 的 user_info 不完整") + return None + person_id = person_api.get_person_id(user_info.platform, int(user_info.user_id)) + person_info_manager = get_person_info_manager() + + # 获取关系信息 + short_impression = await person_info_manager.get_value(person_id, "short_impression") or "无" + impression = await person_info_manager.get_value(person_id, "impression") or "无" + attitude = await person_info_manager.get_value(person_id, "attitude") or 50 + + base_context.update({ + "chat_type": "private", + "person_id": person_id, + "user_info": user_info, + "relationship": { + "short_impression": short_impression, + "impression": impression, + "attitude": attitude + } + }) + return base_context + else: + logger.warning(f"Stream {stream_id} 既没有 group_info 也没有 user_info") + return None + + def _build_decision_prompt(self, context: dict[str, Any], start_mode: str) -> str: + """ 根据上下文构建决策prompt """ + chat_type = context["chat_type"] + persona = context["persona"] + + # 构建通用头部 prompt = f""" # 角色 你的名字是{global_config.bot.nickname},你的人设如下: - 核心人设: {persona["core"]} - 侧面人设: {persona["side"]} - 身份: {persona["identity"]} - +""" + # 根据聊天类型构建任务和情境 + if chat_type == "private": + user_info = context["user_info"] + relationship = context["relationship"] + prompt += f""" # 任务 现在是 {context["current_time"]},你需要根据当前的情境,决定是否要主动向用户 '{user_info.user_nickname}' 发起对话。 @@ -198,7 +210,24 @@ class ProactiveThinkerExecutor: - 好感度: {relationship["attitude"]}/100 4. **最近的聊天摘要**: {context["recent_chat_history"]} +""" + elif chat_type == "group": + group_info = context["group_info"] + prompt += f""" +# 任务 +现在是 {context["current_time"]},你需要根据当前的情境,决定是否要主动向群聊 '{group_info["group_name"]}' 发起对话。 +# 情境分析 +1. **启动模式**: {start_mode} ({"首次加入/很久未发言" if start_mode == "cold_start" else "日常唤醒"}) +2. **你的日程**: +{context["schedule_context"]} +3. **群聊信息**: + - 群名称: {group_info["group_name"]} +4. **最近的聊天摘要**: +{context["recent_chat_history"]} +""" + # 构建通用尾部 + prompt += """ # 决策指令 请综合以上所有信息,做出决策。你的决策需要以JSON格式输出,包含以下字段: - `should_reply`: bool, 是否应该发起对话。 @@ -209,22 +238,34 @@ class ProactiveThinkerExecutor: 示例1 (应该回复): {{ "should_reply": true, - "topic": "提醒Ta今天下午有'项目会议'的日程", - "reason": "现在是上午,Ta下午有个重要会议,我觉得应该主动提醒一下,这会显得我很贴心。" + "topic": "提醒大家今天下午有'项目会议'的日程", + "reason": "现在是上午,下午有个重要会议,我觉得应该主动提醒一下大家,这会显得我很贴心。" }} 示例2 (不应回复): {{ "should_reply": false, "topic": null, - "reason": "虽然我们的关系不错,但现在是深夜,而且Ta今天的日程都已经完成了,我没有合适的理由去打扰Ta。" + "reason": "虽然群里很活跃,但现在是深夜,而且最近的聊天话题我也不熟悉,没有合适的理由去打扰大家。" }} --- 请输出你的决策: """ + return prompt + + async def _make_decision(self, context: dict[str, Any], start_mode: str) -> dict[str, Any] | None: + """ + 决策模块:判断是否应该主动发起对话,以及聊什么话题 + """ + if context["chat_type"] not in ["private", "group"]: + return {"should_reply": False, "reason": "未知的聊天类型"} + + prompt = self._build_decision_prompt(context, start_mode) + if global_config.debug.show_prompt: - logger.info(f"主动思考规划器原始提示词:{prompt}") + logger.info(f"主动思考决策器原始提示词:{prompt}") + is_success, response, _, _ = await llm_api.generate_with_model( prompt=prompt, model_config=model_config.model_task_config.utils ) @@ -233,31 +274,21 @@ class ProactiveThinkerExecutor: return {"should_reply": False, "reason": "决策模型生成失败"} try: - # 假设LLM返回JSON格式的决策结果 if global_config.debug.show_prompt: - logger.info(f"主动思考规划器响应:{response}") + logger.info(f"主动思考决策器响应:{response}") decision = orjson.loads(response) return decision except orjson.JSONDecodeError: logger.error(f"决策LLM返回的JSON格式无效: {response}") return {"should_reply": False, "reason": "决策模型返回格式错误"} - def _build_plan_prompt(self, context: dict[str, Any], start_mode: str, topic: str, reason: str) -> str: - """ - 根据启动模式和决策话题,构建最终的规划提示词 - """ - persona = context["persona"] + def _build_private_plan_prompt(self, context: dict[str, Any], start_mode: str, topic: str, reason: str) -> str: + """ 构建私聊的规划Prompt """ user_info = context["user_info"] relationship = context["relationship"] - + if start_mode == "cold_start": - prompt = f""" -# 角色 -你的名字是{global_config.bot.nickname},你的人设如下: -- 核心人设: {persona["core"]} -- 侧面人设: {persona["side"]} -- 身份: {persona["identity"]} - + return f""" # 任务 你需要主动向一个新朋友 '{user_info.user_nickname}' 发起对话。这是你们的第一次交流,或者很久没聊了。 @@ -272,16 +303,9 @@ class ProactiveThinkerExecutor: - 你的目标是“破冰”,让对话自然地开始。 - 你应该围绕这个话题展开: {topic} - 你的语气应该符合你的人设,友好且真诚。 -- 直接输出你要说的第一句话,不要包含任何额外的前缀或解释。 """ else: # wake_up - prompt = f""" -# 角色 -你的名字是{global_config.bot.nickname},你的人设如下: -- 核心人设: {persona["core"]} -- 侧面人设: {persona["side"]} -- 身份: {persona["identity"]} - + return f""" # 任务 现在是 {context["current_time"]},你需要主动向你的朋友 '{user_info.user_nickname}' 发起对话。 @@ -303,8 +327,58 @@ class ProactiveThinkerExecutor: - 你决定和Ta聊聊关于“{topic}”的话题。 - 请结合以上所有情境信息,自然地开启对话。 - 你的语气应该符合你的人设以及你对Ta的好感度。 -- 直接输出你要说的第一句话,不要包含任何额外的前缀或解释。 """ + + def _build_group_plan_prompt(self, context: dict[str, Any], topic: str, reason: str) -> str: + """ 构建群聊的规划Prompt """ + group_info = context["group_info"] + return f""" +# 任务 +现在是 {context["current_time"]},你需要主动向群聊 '{group_info["group_name"]}' 发起对话。 + +# 决策上下文 +- **决策理由**: {reason} + +# 情境分析 +1. **你的日程**: +{context["schedule_context"]} +2. **群聊信息**: + - 群名称: {group_info["group_name"]} +3. **最近的聊天摘要**: +{context["recent_chat_history"]} +4. **你最近的相关动作**: +{context["action_history_context"]} + +# 对话指引 +- 你决定和大家聊聊关于“{topic}”的话题。 +- 你的语气应该更活泼、更具包容性,以吸引更多群成员参与讨论。 +- 请结合以上所有情境信息,自然地开启对话。 +""" + + def _build_plan_prompt(self, context: dict[str, Any], start_mode: str, topic: str, reason: str) -> str: + """ + 根据启动模式和决策话题,构建最终的规划提示词 + """ + persona = context["persona"] + chat_type = context["chat_type"] + + # 1. 构建通用角色头部 + prompt = f""" +# 角色 +你的名字是{global_config.bot.nickname},你的人设如下: +- 核心人设: {persona["core"]} +- 侧面人设: {persona["side"]} +- 身份: {persona["identity"]} +""" + # 2. 根据聊天类型构建特定内容 + if chat_type == "private": + prompt += self._build_private_plan_prompt(context, start_mode, topic, reason) + elif chat_type == "group": + prompt += self._build_group_plan_prompt(context, topic, reason) + + # 3. 添加通用结尾 + prompt += "\n- 直接输出你要说的第一句话,不要包含任何额外的前缀或解释。" + if global_config.debug.show_prompt: logger.info(f"主动思考回复器原始提示词:{prompt}") return prompt From 923b9896577d8e11dfe6ba59de4d8bfb265d9553 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 3 Oct 2025 17:17:36 +0800 Subject: [PATCH 02/14] =?UTF-8?q?docs(proactive=5Fthinker):=20=E4=B8=BA?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E5=99=A8=E6=A8=A1=E5=9D=97=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AF=A6=E7=BB=86=E7=9A=84=E4=B8=AD=E6=96=87=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 `ProactiveThinkerExecutor` 类及其所有公共和私有方法添加了详细的 Docstrings。这包括对类、方法功能、参数和返回值的清晰描述,以提高代码的可读性和可维护性。 --- .../proactive_thinker_executor.py | 140 ++++++++++++++---- 1 file changed, 108 insertions(+), 32 deletions(-) diff --git a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py index 1f83d75e3..4465f76e6 100644 --- a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py +++ b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py @@ -29,7 +29,10 @@ class ProactiveThinkerExecutor: """ def __init__(self): - # 可以在此初始化所需模块,例如LLM请求器等 + """ + 初始化 ProactiveThinkerExecutor 实例。 + 目前无需初始化操作。 + """ pass async def execute(self, stream_id: str, start_mode: str = "wake_up"): @@ -91,21 +94,37 @@ class ProactiveThinkerExecutor: logger.warning(f"无法发送消息,因为找不到 stream_id 为 {stream_id} 的聊天流") def _get_stream_from_id(self, stream_id: str): - """根据stream_id解析并获取stream对象""" + """ + 根据 stream_id 解析并获取对应的聊天流对象。 + + Args: + stream_id: 聊天流的唯一标识符,格式为 "platform:chat_id:stream_type"。 + + Returns: + 对应的 ChatStream 对象,如果解析失败或找不到则返回 None。 + """ try: platform, chat_id, stream_type = stream_id.split(":") if stream_type == "private": return chat_api.ChatManager.get_private_stream_by_user_id(platform=platform, user_id=chat_id) elif stream_type == "group": - return chat_api.ChatManager.get_group_stream_by_group_id(platform=platform,group_id=chat_id) + return chat_api.ChatManager.get_group_stream_by_group_id(platform=platform, group_id=chat_id) except Exception as e: logger.error(f"解析 stream_id ({stream_id}) 或获取 stream 失败: {e}") return None async def _gather_context(self, stream_id: str) -> dict[str, Any] | None: """ - 收集构建提示词所需的所有上下文信息. - 根据聊天流是私聊还是群聊, 收集不同的上下文. + 收集构建决策和规划提示词所需的所有上下文信息。 + + 此函数会根据聊天流是私聊还是群聊,收集不同的信息, + 包括但不限于日程、聊天历史、人设、关系信息等。 + + Args: + stream_id: 聊天流ID。 + + Returns: + 一个包含所有上下文信息的字典,如果找不到聊天流则返回 None。 """ stream = self._get_stream_from_id(stream_id) if not stream: @@ -114,18 +133,28 @@ class ProactiveThinkerExecutor: # 1. 收集通用信息 (日程, 聊天历史, 动作历史) schedules = await schedule_api.ScheduleAPI.get_today_schedule() - schedule_context = "\n".join([f"- {s.get('time_range', '未知时间')}: {s.get('activity', '未知活动')}" for s in schedules]) if schedules else "今天没有日程安排。" - + schedule_context = ( + "\n".join([f"- {s.get('time_range', '未知时间')}: {s.get('activity', '未知活动')}" for s in schedules]) + if schedules + else "今天没有日程安排。" + ) + recent_messages = await message_api.get_recent_messages(stream_id, limit=10) - recent_chat_history = await message_api.build_readable_messages_to_str(recent_messages) if recent_messages else "无" - + recent_chat_history = ( + await message_api.build_readable_messages_to_str(recent_messages) if recent_messages else "无" + ) + action_history_list = await database_api.db_query( database_api.MODEL_MAPPING["ActionRecords"], filters={"chat_id": stream_id, "action_name": "proactive_decision"}, limit=3, order_by=["-time"], ) - action_history_context = "\n".join([f"- {a['action_data']}" for a in action_history_list if isinstance(a, dict)]) if isinstance(action_history_list, list) else "无" + action_history_context = ( + "\n".join([f"- {a['action_data']}" for a in action_history_list if isinstance(a, dict)]) + if isinstance(action_history_list, list) + else "无" + ) # 2. 构建基础上下文 base_context = { @@ -142,13 +171,12 @@ class ProactiveThinkerExecutor: # 3. 根据聊天类型补充特定上下文 if stream.group_info: # 群聊场景 - base_context.update({ - "chat_type": "group", - "group_info": { - "group_name": stream.group_info.group_name, - "group_id": stream.group_info.group_id + base_context.update( + { + "chat_type": "group", + "group_info": {"group_name": stream.group_info.group_name, "group_id": stream.group_info.group_id}, } - }) + ) return base_context elif stream.user_info: # 私聊场景 user_info = stream.user_info @@ -164,23 +192,34 @@ class ProactiveThinkerExecutor: impression = await person_info_manager.get_value(person_id, "impression") or "无" attitude = await person_info_manager.get_value(person_id, "attitude") or 50 - base_context.update({ - "chat_type": "private", - "person_id": person_id, - "user_info": user_info, - "relationship": { - "short_impression": short_impression, - "impression": impression, - "attitude": attitude + base_context.update( + { + "chat_type": "private", + "person_id": person_id, + "user_info": user_info, + "relationship": { + "short_impression": short_impression, + "impression": impression, + "attitude": attitude, + }, } - }) + ) return base_context else: logger.warning(f"Stream {stream_id} 既没有 group_info 也没有 user_info") return None def _build_decision_prompt(self, context: dict[str, Any], start_mode: str) -> str: - """ 根据上下文构建决策prompt """ + """ + 根据收集到的上下文信息,构建用于决策的提示词。 + + Args: + context: 包含所有上下文信息的字典。 + start_mode: 启动模式 ('cold_start' 或 'wake_up')。 + + Returns: + 构建完成的决策提示词字符串。 + """ chat_type = context["chat_type"] persona = context["persona"] @@ -256,10 +295,18 @@ class ProactiveThinkerExecutor: async def _make_decision(self, context: dict[str, Any], start_mode: str) -> dict[str, Any] | None: """ - 决策模块:判断是否应该主动发起对话,以及聊什么话题 + 调用 LLM 进行决策,判断是否应该主动发起对话,以及聊什么话题。 + + Args: + context: 包含所有上下文信息的字典。 + start_mode: 启动模式。 + + Returns: + 一个包含决策结果的字典 (例如: {"should_reply": bool, "topic": str, "reason": str}), + 如果决策过程失败则返回 None 或包含错误信息的字典。 """ if context["chat_type"] not in ["private", "group"]: - return {"should_reply": False, "reason": "未知的聊天类型"} + return {"should_reply": False, "reason": "未知的聊天类型"} prompt = self._build_decision_prompt(context, start_mode) @@ -283,10 +330,20 @@ class ProactiveThinkerExecutor: return {"should_reply": False, "reason": "决策模型返回格式错误"} def _build_private_plan_prompt(self, context: dict[str, Any], start_mode: str, topic: str, reason: str) -> str: - """ 构建私聊的规划Prompt """ + """ + 为私聊场景构建生成对话内容的规划提示词。 + + Args: + context: 上下文信息字典。 + start_mode: 启动模式。 + topic: 决策模块决定的话题。 + reason: 决策模块给出的理由。 + + Returns: + 构建完成的私聊规划提示词字符串。 + """ user_info = context["user_info"] relationship = context["relationship"] - if start_mode == "cold_start": return f""" # 任务 @@ -330,7 +387,17 @@ class ProactiveThinkerExecutor: """ def _build_group_plan_prompt(self, context: dict[str, Any], topic: str, reason: str) -> str: - """ 构建群聊的规划Prompt """ + """ + 为群聊场景构建生成对话内容的规划提示词。 + + Args: + context: 上下文信息字典。 + topic: 决策模块决定的话题。 + reason: 决策模块给出的理由。 + + Returns: + 构建完成的群聊规划提示词字符串。 + """ group_info = context["group_info"] return f""" # 任务 @@ -357,7 +424,16 @@ class ProactiveThinkerExecutor: def _build_plan_prompt(self, context: dict[str, Any], start_mode: str, topic: str, reason: str) -> str: """ - 根据启动模式和决策话题,构建最终的规划提示词 + 根据聊天类型、启动模式和决策结果,构建最终生成对话内容的规划提示词。 + + Args: + context: 上下文信息字典。 + start_mode: 启动模式。 + topic: 决策模块决定的话题。 + reason: 决策模块给出的理由。 + + Returns: + 最终的规划提示词字符串。 """ persona = context["persona"] chat_type = context["chat_type"] From f4079f10fedd3252da7b290aa90653f166babb5e Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 3 Oct 2025 17:41:58 +0800 Subject: [PATCH 03/14] =?UTF-8?q?feat(proactive=5Fthinker):=20=E5=9C=A8?= =?UTF-8?q?=E4=B8=BB=E5=8A=A8=E6=80=9D=E8=80=83=E4=B8=AD=E8=9E=8D=E5=85=A5?= =?UTF-8?q?=E6=83=85=E7=BB=AA=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将情绪状态整合到主动思考的上下文和提示词中。这使得AI在主动发起对话时,其语气和内容能够更好地反映当前的情绪,使交互更加生动和人性化。 主要变更: - 在上下文收集中增加当前的情绪状态。 - 将情绪状态变量加入到私聊和群聊的提示词模板中,引导模型生成符合情绪的回复。 - 优化了最终的输出指令,使其更清晰、更严格,以提高生成内容的质量。 --- .../proactive_thinker_executor.py | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py index 4465f76e6..f268aea32 100644 --- a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py +++ b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py @@ -5,6 +5,7 @@ import orjson from src.common.logger import get_logger from src.config.config import global_config, model_config +from src.mood.mood_manager import mood_manager from src.person_info.person_info import get_person_info_manager from src.plugin_system.apis import ( chat_api, @@ -110,8 +111,8 @@ class ProactiveThinkerExecutor: elif stream_type == "group": return chat_api.ChatManager.get_group_stream_by_group_id(platform=platform, group_id=chat_id) except Exception as e: - logger.error(f"解析 stream_id ({stream_id}) 或获取 stream 失败: {e}") - return None + logger.error(f"获取 stream_id ({stream_id}) 失败: {e}") + return None async def _gather_context(self, stream_id: str) -> dict[str, Any] | None: """ @@ -157,10 +158,12 @@ class ProactiveThinkerExecutor: ) # 2. 构建基础上下文 + mood_state = mood_manager.get_mood_by_chat_id(stream_id).mood_state base_context = { "schedule_context": schedule_context, "recent_chat_history": recent_chat_history, "action_history_context": action_history_context, + "mood_state": mood_state, "persona": { "core": global_config.personality.personality_core, "side": global_config.personality.personality_side, @@ -230,6 +233,8 @@ class ProactiveThinkerExecutor: - 核心人设: {persona["core"]} - 侧面人设: {persona["side"]} - 身份: {persona["identity"]} + +你的当前情绪状态是: {context["mood_state"]} """ # 根据聊天类型构建任务和情境 if chat_type == "private": @@ -359,7 +364,7 @@ class ProactiveThinkerExecutor: # 对话指引 - 你的目标是“破冰”,让对话自然地开始。 - 你应该围绕这个话题展开: {topic} -- 你的语气应该符合你的人设,友好且真诚。 +- 你的语气应该符合你的人设和你当前的心情({context["mood_state"]}),友好且真诚。 """ else: # wake_up return f""" @@ -383,7 +388,7 @@ class ProactiveThinkerExecutor: # 对话指引 - 你决定和Ta聊聊关于“{topic}”的话题。 - 请结合以上所有情境信息,自然地开启对话。 -- 你的语气应该符合你的人设以及你对Ta的好感度。 +- 你的语气应该符合你的人设({context["mood_state"]})以及你对Ta的好感度。 """ def _build_group_plan_prompt(self, context: dict[str, Any], topic: str, reason: str) -> str: @@ -408,6 +413,7 @@ class ProactiveThinkerExecutor: # 情境分析 1. **你的日程**: +你当前的心情({context["mood_state"]} {context["schedule_context"]} 2. **群聊信息**: - 群名称: {group_info["group_name"]} @@ -418,8 +424,9 @@ class ProactiveThinkerExecutor: # 对话指引 - 你决定和大家聊聊关于“{topic}”的话题。 -- 你的语气应该更活泼、更具包容性,以吸引更多群成员参与讨论。 +- 你的语气应该更活泼、更具包容性,以吸引更多群成员参与讨论。你的语气应该符合你的人设)。 - 请结合以上所有情境信息,自然地开启对话。 +- 可以分享你的看法、提出相关问题,或者开个合适的玩笑。 """ def _build_plan_prompt(self, context: dict[str, Any], start_mode: str, topic: str, reason: str) -> str: @@ -453,8 +460,17 @@ class ProactiveThinkerExecutor: prompt += self._build_group_plan_prompt(context, topic, reason) # 3. 添加通用结尾 - prompt += "\n- 直接输出你要说的第一句话,不要包含任何额外的前缀或解释。" + final_instructions = """ + +# 输出要求 +- **简洁**: 不要输出任何多余内容(如前缀、后缀、冒号、引号、at/@等)。 +- **原创**: 不要重复之前的内容,即使意思相近也不行。 +- **直接**: 只输出最终的回复文本本身。 +- **风格**: 回复需简短、完整且口语化。 + +现在,你说:""" + prompt += final_instructions if global_config.debug.show_prompt: logger.info(f"主动思考回复器原始提示词:{prompt}") - return prompt + return prompt \ No newline at end of file From 62fe589c8f49f905610d66324ad81869df67bb2f Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 3 Oct 2025 18:19:18 +0800 Subject: [PATCH 04/14] =?UTF-8?q?fix(proactive=5Fthinker):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=83=85=E7=BB=AA=E6=A8=A1=E5=9D=97=E5=85=B3=E9=97=AD?= =?UTF-8?q?=E6=97=B6=E4=B8=BB=E5=8A=A8=E6=80=9D=E8=80=83=E6=8A=A5=E9=94=99?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当全局情绪模块被禁用时,尝试获取情绪状态会导致`AttributeError`或`KeyError`,从而中断主动思考任务。 本次提交通过以下方式修复了此问题: 1. 在获取情绪状态前,增加对`global_config.mood.enable_mood`的判断。 2. 在情绪获取逻辑中加入`try...except`块,捕获潜在的异常,并在失败时提供一个默认的情绪状态,增强了代码的鲁棒性。 3. 在日常唤醒任务的异常处理中,增加了`traceback.print_exc()`,以便在出现错误时打印完整的堆栈跟踪,方便快速定位问题。 --- .../built_in/proactive_thinker/proacive_thinker_event.py | 2 ++ .../proactive_thinker/proactive_thinker_executor.py | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py b/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py index 020a07798..88fa169e8 100644 --- a/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py +++ b/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py @@ -1,6 +1,7 @@ import asyncio import random import time +import traceback from datetime import datetime from maim_message import UserInfo @@ -199,6 +200,7 @@ class ProactiveThinkingTask(AsyncTask): logger.info("日常唤醒任务被正常取消。") break except Exception as e: + traceback.print_exc() # 打印完整的堆栈跟踪 logger.error(f"【日常唤醒】任务出现错误,将在60秒后重试: {e}", exc_info=True) await asyncio.sleep(60) diff --git a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py index f268aea32..74c23eba4 100644 --- a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py +++ b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py @@ -158,7 +158,12 @@ class ProactiveThinkerExecutor: ) # 2. 构建基础上下文 - mood_state = mood_manager.get_mood_by_chat_id(stream_id).mood_state + if global_config.mood.enable_mood: + try: + mood_state = mood_manager.get_mood_by_chat_id(stream.stream_id) + except Exception as e: + logger.error(f"获取情绪失败,原因:{e}") + mood_state = "暂时没有" base_context = { "schedule_context": schedule_context, "recent_chat_history": recent_chat_history, @@ -473,4 +478,4 @@ class ProactiveThinkerExecutor: if global_config.debug.show_prompt: logger.info(f"主动思考回复器原始提示词:{prompt}") - return prompt \ No newline at end of file + return prompt From 4089e714b74faa0e02d6b53c2de89ae5d4c4581e Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 3 Oct 2025 18:21:21 +0800 Subject: [PATCH 05/14] =?UTF-8?q?fix:(proactive=5Fthinker)=E8=BF=99beyond?= =?UTF-8?q?=E7=9A=84=E5=AD=A9=E5=AD=90=E5=BF=98=E4=BA=86=E5=8A=A0mood=5Fst?= =?UTF-8?q?ate,=E5=AF=BC=E8=87=B4=E4=B8=BB=E5=8A=A8=E6=80=9D=E8=80=83?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=AF=8D=E9=87=8C=E7=9A=84=E6=83=85=E7=BB=AA?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E4=BA=86=E4=B8=80=E5=A0=86=E4=B9=B1=E7=A0=81?= =?UTF-8?q?()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../built_in/proactive_thinker/proactive_thinker_executor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py index 74c23eba4..f4f41426b 100644 --- a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py +++ b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py @@ -160,7 +160,7 @@ class ProactiveThinkerExecutor: # 2. 构建基础上下文 if global_config.mood.enable_mood: try: - mood_state = mood_manager.get_mood_by_chat_id(stream.stream_id) + mood_state = mood_manager.get_mood_by_chat_id(stream.stream_id).mood_state except Exception as e: logger.error(f"获取情绪失败,原因:{e}") mood_state = "暂时没有" From 135909449cac07ded647ab4488a7035510dbaf19 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 3 Oct 2025 19:39:37 +0800 Subject: [PATCH 06/14] =?UTF-8?q?refactor(proactive=5Fthinker):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=94=A4=E9=86=92=E9=80=BB=E8=BE=91=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=98=B2=E6=89=93=E6=89=B0=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构了日常唤醒任务(ProactiveThinkingTask)的逻辑,将其拆分为私聊和群聊的独立处理流程。 - 私聊现在直接遍历白名单,确保能覆盖到所有配置的用户,即使他们当前不在内存中。 - 群聊则继续遍历内存中的活跃流。 这个改动修复了之前版本中,只有当用户发送消息后,bot才有可能对其进行主动唤醒的问题。 同时,在决策模块中引入了防打扰机制: - 在决策提示词中加入了最近的决策历史记录作为上下文。 - 增加了新的决策原则,明确指示模型在近期已主动发起过对话的情况下,应倾向于保持沉默,以避免过度打扰用户。 此外,对冷启动任务(ColdStartTask)进行了微调,将初始的等待时间移至循环的开始,以确保插件加载后能先等待一段时间再开始工作。 --- .../proacive_thinker_event.py | 81 ++++++++++--------- .../proactive_thinker_executor.py | 7 ++ 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py b/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py index 88fa169e8..c310e5c45 100644 --- a/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py +++ b/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py @@ -38,6 +38,8 @@ class ColdStartTask(AsyncTask): while True: try: + #开始就先暂停一小时,等bot聊一会再说() + await asyncio.sleep(3600) logger.info("【冷启动】开始扫描白名单,寻找从未聊过的用户...") # 从全局配置中获取私聊白名单 @@ -83,9 +85,6 @@ class ColdStartTask(AsyncTask): except Exception as e: logger.error(f"【冷启动】处理用户 {chat_id} 时发生未知错误: {e}", exc_info=True) - # 完成一轮检查后,进入长时休眠 - await asyncio.sleep(3600) - except asyncio.CancelledError: logger.info("冷启动任务被正常取消。") break @@ -157,44 +156,50 @@ class ProactiveThinkingTask(AsyncTask): enabled_private = set(global_config.proactive_thinking.enabled_private_chats) enabled_groups = set(global_config.proactive_thinking.enabled_group_chats) - # 获取当前所有聊天流的快照 - all_streams = list(self.chat_manager.streams.values()) - - for stream in all_streams: - # 1. 检查该聊天是否在白名单内(或白名单为空时默认允许) - is_whitelisted = False - if stream.group_info: # 群聊 - if not enabled_groups or f"qq:{stream.group_info.group_id}" in enabled_groups: - is_whitelisted = True - else: # 私聊 - if not enabled_private or f"qq:{stream.user_info.user_id}" in enabled_private: - is_whitelisted = True - - if not is_whitelisted: - continue # 不在白名单内,跳过 - - # 2. 【核心逻辑】检查聊天冷却时间是否足够长 - time_since_last_active = time.time() - stream.last_active_time - if time_since_last_active > next_interval: - logger.info( - f"【日常唤醒】聊天流 {stream.stream_id} 已冷却 {time_since_last_active:.2f} 秒,触发主动对话。" - ) - - # 构建符合 executor 期望的 stream_id 格式 - if stream.group_info and stream.group_info.group_id: - formatted_stream_id = f"{stream.user_info.platform}:{stream.group_info.group_id}:group" - elif stream.user_info and stream.user_info.user_id: - formatted_stream_id = f"{stream.user_info.platform}:{stream.user_info.user_id}:private" - else: - logger.warning(f"【日常唤醒】跳过 stream {stream.stream_id},因为它缺少有效的用户信息或群组信息。") + # 分别处理私聊和群聊 + # 1. 处理私聊:直接遍历白名单,确保能覆盖到所有(包括本次运行尚未活跃的)用户 + for chat_id in enabled_private: + try: + platform, user_id_str = chat_id.split(":") + # 【核心逻辑】检查聊天流是否存在。不存在则跳过,交由ColdStartTask处理。 + stream = chat_api.get_stream_by_user_id(user_id_str, platform) + if not stream: continue - await self.executor.execute(stream_id=formatted_stream_id, start_mode="wake_up") + # 检查冷却时间 + time_since_last_active = time.time() - stream.last_active_time + if time_since_last_active > next_interval: + logger.info( + f"【日常唤醒-私聊】聊天流 {stream.stream_id} 已冷却 {time_since_last_active:.2f} 秒,触发主动对话。" + ) + formatted_stream_id = f"{stream.user_info.platform}:{stream.user_info.user_id}:private" + await self.executor.execute(stream_id=formatted_stream_id, start_mode="wake_up") + stream.update_active_time() + await self.chat_manager._save_stream(stream) - # 【关键步骤】在触发后,立刻更新活跃时间并保存。 - # 这可以防止在同一个检查周期内,对同一个目标因为意外的延迟而发送多条消息。 - stream.update_active_time() - await self.chat_manager._save_stream(stream) + except ValueError: + logger.warning(f"【日常唤醒】私聊白名单条目格式错误,已跳过: {chat_id}") + except Exception as e: + logger.error(f"【日常唤醒】处理私聊用户 {chat_id} 时发生未知错误: {e}", exc_info=True) + + # 2. 处理群聊:遍历内存中的活跃流(群聊不存在冷启动问题) + all_streams = list(self.chat_manager.streams.values()) + for stream in all_streams: + if not stream.group_info: + continue # 只处理群聊 + + # 检查群聊是否在白名单内 + if not enabled_groups or f"qq:{stream.group_info.group_id}" in enabled_groups: + # 检查冷却时间 + time_since_last_active = time.time() - stream.last_active_time + if time_since_last_active > next_interval: + logger.info( + f"【日常唤醒-群聊】聊天流 {stream.stream_id} 已冷却 {time_since_last_active:.2f} 秒,触发主动对话。" + ) + formatted_stream_id = f"{stream.user_info.platform}:{stream.group_info.group_id}:group" + await self.executor.execute(stream_id=formatted_stream_id, start_mode="wake_up") + stream.update_active_time() + await self.chat_manager._save_stream(stream) except asyncio.CancelledError: logger.info("日常唤醒任务被正常取消。") diff --git a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py index f4f41426b..ea5187f1f 100644 --- a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py +++ b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py @@ -240,6 +240,9 @@ class ProactiveThinkerExecutor: - 身份: {persona["identity"]} 你的当前情绪状态是: {context["mood_state"]} + +# 你最近的相关决策历史 (供参考) +{context["action_history_context"]} """ # 根据聊天类型构建任务和情境 if chat_type == "private": @@ -283,6 +286,10 @@ class ProactiveThinkerExecutor: - `topic`: str, 如果 `should_reply` 为 true,你打算聊什么话题?(例如:问候一下今天的日程、关心一下昨天的某件事、分享一个你自己的趣事等) - `reason`: str, 做出此决策的简要理由。 +# 决策原则 +- **避免打扰**: 如果你最近(尤其是在最近的几次决策中)已经主动发起过对话,请倾向于选择“不回复”,除非有非常重要和紧急的事情。 + + --- 示例1 (应该回复): {{ From 09a17e5c121c67084d457c0616a550524ce521d6 Mon Sep 17 00:00:00 2001 From: ikun-11451 <334495606@qq.com> Date: Fri, 3 Oct 2025 20:21:56 +0800 Subject: [PATCH 07/14] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=90=8E?= =?UTF-8?q?=E7=AB=AFapi=E5=96=B5~?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/__init__.py | 1 + src/api/message_router.py | 13 +++++++++++++ src/main.py | 10 ++++++++++ 3 files changed, 24 insertions(+) create mode 100644 src/api/__init__.py create mode 100644 src/api/message_router.py diff --git a/src/api/__init__.py b/src/api/__init__.py new file mode 100644 index 000000000..359eff96e --- /dev/null +++ b/src/api/__init__.py @@ -0,0 +1 @@ +# This file makes src/api a Python package. \ No newline at end of file diff --git a/src/api/message_router.py b/src/api/message_router.py new file mode 100644 index 000000000..5a2dd2d0a --- /dev/null +++ b/src/api/message_router.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter +from src.plugin_system.apis import message_api + +router = APIRouter() + +@router.get("/messages/recent") +async def get_recent_messages(chat_id: str, limit: int = 10): + """ + 获取最近的聊天记录 + """ + # 假设 message_api.get_recent_messages 是一个异步函数 + messages = await message_api.get_recent_messages(chat_id=chat_id, limit=limit) + return {"chat_id": chat_id, "messages": messages} \ No newline at end of file diff --git a/src/main.py b/src/main.py index 68d8e0801..8236f7472 100644 --- a/src/main.py +++ b/src/main.py @@ -245,6 +245,16 @@ MoFox_Bot(第三方修改版) # start_api_server() # logger.info("API服务器启动成功") + # 注册API路由 + try: + from src.api.message_router import router as message_router + self.server.register_router(message_router, prefix="/api") + logger.info("API路由注册成功") + except ImportError as e: + logger.error(f"导入API路由失败: {e}") + except Exception as e: + logger.error(f"注册API路由时发生错误: {e}") + # 加载所有actions,包括默认的和插件的 plugin_manager.load_all_plugins() From 19c37280d489ab2e67587cc85557afec20506925 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 3 Oct 2025 20:43:07 +0800 Subject: [PATCH 08/14] =?UTF-8?q?feat(api):=20=E6=96=B0=E5=A2=9E=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E7=BB=9F=E8=AE=A1API=E7=AB=AF=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原有的获取最近消息的API替换为功能更强大的消息统计API。 新的 `/messages/recent` 端点允许按天数和消息类型(发送、接收或全部)查询机器人的消息数量统计。 - 支持 `days` 和 `message_type` 查询参数。 - 实现统计逻辑,区分机器人发送和接收的消息。 - 增加异常处理,提高API的健壮性。 --- src/api/message_router.py | 47 ++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/api/message_router.py b/src/api/message_router.py index 5a2dd2d0a..687f48f1e 100644 --- a/src/api/message_router.py +++ b/src/api/message_router.py @@ -1,13 +1,48 @@ -from fastapi import APIRouter +import time +from typing import Literal + +from fastapi import APIRouter, HTTPException, Query + +from src.config.config import global_config from src.plugin_system.apis import message_api router = APIRouter() @router.get("/messages/recent") -async def get_recent_messages(chat_id: str, limit: int = 10): +async def get_message_stats( + days: int = Query(1, ge=1, description="指定查询过去多少天的数据"), + message_type: Literal["all", "sent", "received"] = Query("all", description="筛选消息类型: 'sent' (BOT发送的), 'received' (BOT接收的), or 'all' (全部)") +): """ - 获取最近的聊天记录 + 获取BOT在指定天数内的消息统计数据。 """ - # 假设 message_api.get_recent_messages 是一个异步函数 - messages = await message_api.get_recent_messages(chat_id=chat_id, limit=limit) - return {"chat_id": chat_id, "messages": messages} \ No newline at end of file + try: + end_time = time.time() + start_time = end_time - (days * 24 * 3600) + + messages = await message_api.get_messages_by_time(start_time, end_time) + + sent_count = 0 + received_count = 0 + bot_qq = str(global_config.bot.qq_account) + + for msg in messages: + if msg.get("user_id") == bot_qq: + sent_count += 1 + else: + received_count += 1 + if message_type == "sent": + return {"days": days, "message_type": message_type, "count": sent_count} + elif message_type == "received": + return {"days": days, "message_type": message_type, "count": received_count} + else: + return { + "days": days, + "message_type": message_type, + "sent_count": sent_count, + "received_count": received_count, + "total_count": len(messages) + } + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) From 46478d7c149ce0dfc3b9b035c6f9f5189771c869 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 3 Oct 2025 20:47:02 +0800 Subject: [PATCH 09/14] =?UTF-8?q?feat(proactive=5Fthinker):=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E9=98=B2=E5=88=B7=E5=B1=8F=E5=86=B3=E7=AD=96=E5=8E=9F?= =?UTF-8?q?=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在主动思考的决策原则中增加了一条新规则。 如果群聊的上下文中只有AI自己的消息而没有其他人的回应,AI也许将选择不回复,以避免刷屏。 --- .../built_in/proactive_thinker/proactive_thinker_executor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py index ea5187f1f..166837340 100644 --- a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py +++ b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py @@ -288,6 +288,7 @@ class ProactiveThinkerExecutor: # 决策原则 - **避免打扰**: 如果你最近(尤其是在最近的几次决策中)已经主动发起过对话,请倾向于选择“不回复”,除非有非常重要和紧急的事情。 +- **如果上下文中只有你的消息而没有别人的消息**:选择不回复,以防刷屏或者打扰到别人虽然第一 --- From 0e2182efa624e6983692b869c339bed54b63495f Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 3 Oct 2025 21:05:59 +0800 Subject: [PATCH 10/14] =?UTF-8?q?=E5=8A=A8=E4=B8=80=E4=B8=8Bmanifest?= =?UTF-8?q?=E4=BB=A5=E9=98=B2=E5=8A=A0=E8=BD=BD=E7=9A=84=E6=97=B6=E5=80=99?= =?UTF-8?q?=E8=A2=AB=E7=94=A8=E6=88=B7=E7=9C=8B=E7=AC=91=E8=AF=9D()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/built_in/proactive_thinker/_manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/built_in/proactive_thinker/_manifest.json b/src/plugins/built_in/proactive_thinker/_manifest.json index 0ad67e293..30f2a138e 100644 --- a/src/plugins/built_in/proactive_thinker/_manifest.json +++ b/src/plugins/built_in/proactive_thinker/_manifest.json @@ -12,7 +12,7 @@ "host_application": { "min_version": "0.10.0" }, - "keywords": ["emoji", "reaction", "like", "表情", "回应", "点赞"], + "keywords": ["主动思考","自己发消息"], "categories": ["Chat", "Integration"], "default_locale": "zh-CN", From c46a31a7c40db2ebe2f8ff64fe5c8d7451b92f54 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 3 Oct 2025 21:17:50 +0800 Subject: [PATCH 11/14] =?UTF-8?q?refactor(express):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E5=88=9B=E5=BB=BA=E7=9A=84=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 `try-except` 块移至循环外部,以减少重复的异常处理代码。这样可以更清晰地处理在创建多个目录过程中可能发生的任何错误,同时保持代码的简洁性。 --- src/chat/express/expression_learner.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/chat/express/expression_learner.py b/src/chat/express/expression_learner.py index f9e0e68af..517204351 100644 --- a/src/chat/express/expression_learner.py +++ b/src/chat/express/expression_learner.py @@ -511,11 +511,11 @@ class ExpressionLearnerManager: os.path.join(base_dir, "learnt_grammar"), ] - for directory in directories_to_create: - try: + try: + for directory in directories_to_create: os.makedirs(directory, exist_ok=True) - logger.debug(f"确保目录存在: {directory}") - except Exception as e: + logger.debug(f"确保目录存在: {directory}") + except Exception as e: logger.error(f"创建目录失败 {directory}: {e}") @staticmethod From bb4ff48e26725bb47afa265626db751d1d291ce5 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Fri, 3 Oct 2025 21:44:31 +0800 Subject: [PATCH 12/14] =?UTF-8?q?refactor(proactive=5Fthinker):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=BB=E5=8A=A8=E6=80=9D=E8=80=83=E7=9A=84?= =?UTF-8?q?=E5=86=B3=E7=AD=96=E4=B8=8E=E4=BA=A4=E4=BA=92=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E5=85=B6=E6=9B=B4=E8=87=AA=E7=84=B6=E4=B8=94=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E6=89=93=E6=89=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交对主动思考插件进行了多项核心优化,旨在提升其交互的自然度和人性化,并引入了关键的防打扰机制。 主要变更包括: 1. **重构冷启动任务 (`ColdStartTask`)**: - 任务逻辑从一个长期运行的周期性任务,重构为在机器人启动时执行一次的“唤醒”任务。 - 新逻辑不仅能为白名单中的全新用户发起首次问候,还能智能地“唤醒”那些因机器人重启而“沉睡”的聊天流,确保了主动思考功能的连续性。 2. **增强决策提示词 (`_build_plan_prompt`)**: - 引入了更精细的决策原则,核心是增加了防打扰机制。现在模型在决策时会检查上一条消息是否为自己发送,如果对方尚未回复,则倾向于不发起新对话,以表现出耐心和体贴。 - 优化了示例,引导模型优先利用上下文信息,并在无切入点时使用简单的问候,避免创造生硬抽象的话题。 3. **改善回复生成逻辑 (`_build_*_reply_prompt`)**: - 在生成回复的指令中,明确要求模型必须先用一句通用的礼貌问候语(如“在吗?”、“下午好!”)作为开场白,然后再衔接具体话题。这使得主动发起的对话更加自然、流畅,符合人类的沟通习惯。 4. **模型调整**: - 将决策规划阶段的 LLM 模型从 `utils` 调整为 `replyer`,以更好地适应生成对话策略的任务。 --- .../proacive_thinker_event.py | 97 +++++++++---------- .../proactive_thinker_executor.py | 38 ++++++-- 2 files changed, 75 insertions(+), 60 deletions(-) diff --git a/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py b/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py index c310e5c45..ffd663d18 100644 --- a/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py +++ b/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py @@ -21,76 +21,69 @@ logger = get_logger(__name__) class ColdStartTask(AsyncTask): """ - 冷启动任务,专门用于处理那些在白名单里,但从未与机器人发生过交互的用户。 - 它的核心职责是“破冰”,主动创建聊天流并发起第一次问候。 + “冷启动”任务,在机器人启动时执行一次。 + 它的核心职责是“唤醒”那些因重启而“沉睡”的聊天流,确保它们能够接收主动思考。 + 对于在白名单中但从未有过记录的全新用户,它也会发起第一次“破冰”问候。 """ - def __init__(self): + def __init__(self, bot_start_time: float): super().__init__(task_name="ColdStartTask") self.chat_manager = get_chat_manager() self.executor = ProactiveThinkerExecutor() + self.bot_start_time = bot_start_time async def run(self): - """任务主循环,周期性地检查是否有需要“破冰”的新用户。""" - logger.info("冷启动任务已启动,将周期性检查白名单中的新朋友。") - # 初始等待一段时间,确保其他服务(如数据库)完全启动 - await asyncio.sleep(100) + """任务主逻辑,在启动后执行一次白名单扫描。""" + logger.info("冷启动任务已启动,将在短暂延迟后开始唤醒沉睡的聊天流...") + await asyncio.sleep(30) # 延迟以确保所有服务和聊天流已从数据库加载完毕 - while True: - try: - #开始就先暂停一小时,等bot聊一会再说() - await asyncio.sleep(3600) - logger.info("【冷启动】开始扫描白名单,寻找从未聊过的用户...") + try: + logger.info("【冷启动】开始扫描白名单,唤醒沉睡的聊天流...") - # 从全局配置中获取私聊白名单 - enabled_private_chats = global_config.proactive_thinking.enabled_private_chats - if not enabled_private_chats: - logger.debug("【冷启动】私聊白名单为空,任务暂停一小时。") - await asyncio.sleep(3600) # 白名单为空时,没必要频繁检查 - continue + enabled_private_chats = global_config.proactive_thinking.enabled_private_chats + if not enabled_private_chats: + logger.debug("【冷启动】私聊白名单为空,任务结束。") + return - # 遍历白名单中的每一个用户 - for chat_id in enabled_private_chats: - try: - platform, user_id_str = chat_id.split(":") - user_id = int(user_id_str) + for chat_id in enabled_private_chats: + try: + platform, user_id_str = chat_id.split(":") + user_id = int(user_id_str) - # 【核心逻辑】使用 chat_api 检查该用户是否已经存在聊天流(ChatStream) - # 如果返回了 ChatStream 对象,说明已经聊过天了,不是本次任务的目标 - if chat_api.get_stream_by_user_id(user_id_str, platform): - continue # 跳过已存在的用户 + should_wake_up = False + stream = chat_api.get_stream_by_user_id(user_id_str, platform) - logger.info(f"【冷启动】发现白名单新用户 {chat_id},准备发起第一次问候。") + if not stream: + should_wake_up = True + logger.info(f"【冷启动】发现全新用户 {chat_id},准备发起第一次问候。") + elif stream.last_active_time < self.bot_start_time: + should_wake_up = True + logger.info(f"【冷启动】发现沉睡的聊天流 {chat_id} (最后活跃于 {datetime.fromtimestamp(stream.last_active_time)}),准备唤醒。") - # 【增强体验】尝试从关系数据库中获取该用户的昵称 - # 这样打招呼时可以更亲切,而不是只知道一个冷冰冰的ID + if should_wake_up: person_id = person_api.get_person_id(platform, user_id) nickname = await person_api.get_person_value(person_id, "nickname") - - # 如果数据库里有昵称,就用数据库里的;如果没有,就用 "用户+ID" 作为备用 user_nickname = nickname or f"用户{user_id}" - - # 创建 UserInfo 对象,这是创建聊天流的必要信息 user_info = UserInfo(platform=platform, user_id=str(user_id), user_nickname=user_nickname) - - # 【关键步骤】主动创建聊天流。 - # 创建后,该用户就进入了机器人的“好友列表”,后续将由 ProactiveThinkingTask 接管 + + # 使用 get_or_create_stream 来安全地获取或创建流 stream = await self.chat_manager.get_or_create_stream(platform, user_info) + + formatted_stream_id = f"{stream.user_info.platform}:{stream.user_info.user_id}:private" + await self.executor.execute(stream_id=formatted_stream_id, start_mode="cold_start") + logger.info(f"【冷启动】已为用户 {chat_id} (昵称: {user_nickname}) 发送唤醒/问候消息。") - await self.executor.execute(stream_id=stream.stream_id, start_mode="cold_start") - logger.info(f"【冷启动】已为新用户 {chat_id} (昵称: {user_nickname}) 创建聊天流并发送问候。") + except ValueError: + logger.warning(f"【冷启动】白名单条目格式错误或用户ID无效,已跳过: {chat_id}") + except Exception as e: + logger.error(f"【冷启动】处理用户 {chat_id} 时发生未知错误: {e}", exc_info=True) - except ValueError: - logger.warning(f"【冷启动】白名单条目格式错误或用户ID无效,已跳过: {chat_id}") - except Exception as e: - logger.error(f"【冷启动】处理用户 {chat_id} 时发生未知错误: {e}", exc_info=True) - - except asyncio.CancelledError: - logger.info("冷启动任务被正常取消。") - break - except Exception as e: - logger.error(f"【冷启动】任务出现严重错误,将在5分钟后重试: {e}", exc_info=True) - await asyncio.sleep(300) + except asyncio.CancelledError: + logger.info("冷启动任务被正常取消。") + except Exception as e: + logger.error(f"【冷启动】任务出现严重错误: {e}", exc_info=True) + finally: + logger.info("【冷启动】任务执行完毕。") class ProactiveThinkingTask(AsyncTask): @@ -222,13 +215,15 @@ class ProactiveThinkerEventHandler(BaseEventHandler): logger.info("检测到插件启动事件,正在初始化【主动思考】") # 检查总开关 if global_config.proactive_thinking.enable: + bot_start_time = time.time() # 记录“诞生时刻” + # 启动负责“日常唤醒”的核心任务 proactive_task = ProactiveThinkingTask() await async_task_manager.add_task(proactive_task) # 检查“冷启动”功能的独立开关 if global_config.proactive_thinking.enable_cold_start: - cold_start_task = ColdStartTask() + cold_start_task = ColdStartTask(bot_start_time) await async_task_manager.add_task(cold_start_task) else: diff --git a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py index ea5187f1f..96377c800 100644 --- a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py +++ b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py @@ -80,7 +80,7 @@ class ProactiveThinkerExecutor: plan_prompt = self._build_plan_prompt(context, start_mode, topic, reason) is_success, response, _, _ = await llm_api.generate_with_model( - prompt=plan_prompt, model_config=model_config.model_task_config.utils + prompt=plan_prompt, model_config=model_config.model_task_config.replyer ) if is_success and response: @@ -158,12 +158,12 @@ class ProactiveThinkerExecutor: ) # 2. 构建基础上下文 + mood_state = "暂时没有" if global_config.mood.enable_mood: try: mood_state = mood_manager.get_mood_by_chat_id(stream.stream_id).mood_state except Exception as e: logger.error(f"获取情绪失败,原因:{e}") - mood_state = "暂时没有" base_context = { "schedule_context": schedule_context, "recent_chat_history": recent_chat_history, @@ -281,29 +281,47 @@ class ProactiveThinkerExecutor: # 构建通用尾部 prompt += """ # 决策指令 -请综合以上所有信息,做出决策。你的决策需要以JSON格式输出,包含以下字段: +请综合以上所有信息,以稳定、真实、拟人的方式做出决策。你的决策需要以JSON格式输出,包含以下字段: - `should_reply`: bool, 是否应该发起对话。 -- `topic`: str, 如果 `should_reply` 为 true,你打算聊什么话题?(例如:问候一下今天的日程、关心一下昨天的某件事、分享一个你自己的趣事等) +- `topic`: str, 如果 `should_reply` 为 true,你打算聊什么话题? - `reason`: str, 做出此决策的简要理由。 # 决策原则 -- **避免打扰**: 如果你最近(尤其是在最近的几次决策中)已经主动发起过对话,请倾向于选择“不回复”,除非有非常重要和紧急的事情。 +- **谨慎对待未回复的对话**: 在发起新话题前,请检查【最近的聊天摘要】。如果最后一条消息是你自己发送的,请仔细评估等待的时间和上下文,判断再次主动发起对话是否礼貌和自然。如果等待时间很短(例如几分钟或半小时内),通常应该选择“不回复”。 +- **优先利用上下文**: 优先从【情境分析】中已有的信息(如最近的聊天摘要、你的日程、你对Ta的关系印象)寻找自然的话题切入点。 +- **简单问候作为备选**: 如果上下文中没有合适的话题,可以生成一个简单、真诚的日常问候(例如“在忙吗?”,“下午好呀~”)。 +- **避免抽象**: 避免创造过于复杂、抽象或需要对方思考很久才能明白的话题。目标是轻松、自然地开启对话。 +- **避免过于频繁**: 如果你最近(尤其是在最近的几次决策中)已经主动发起过对话,请倾向于选择“不回复”,除非有非常重要和紧急的事情。 --- -示例1 (应该回复): +示例1 (基于上下文): {{ "should_reply": true, - "topic": "提醒大家今天下午有'项目会议'的日程", - "reason": "现在是上午,下午有个重要会议,我觉得应该主动提醒一下大家,这会显得我很贴心。" + "topic": "关心一下Ta昨天提到的那个项目进展如何了", + "reason": "用户昨天在聊天中提到了一个重要的项目,现在主动关心一下进展,会显得很体贴,也能自然地开启对话。" }} -示例2 (不应回复): +示例2 (简单问候): +{{ + "should_reply": true, + "topic": "打个招呼,问问Ta现在在忙些什么", + "reason": "最近没有聊天记录,日程也很常规,没有特别的切入点。一个简单的日常问候是最安全和自然的方式来重新连接。" +}} + +示例3 (不应回复 - 过于频繁): {{ "should_reply": false, "topic": null, "reason": "虽然群里很活跃,但现在是深夜,而且最近的聊天话题我也不熟悉,没有合适的理由去打扰大家。" }} + +示例4 (不应回复 - 等待回应): +{{ + "should_reply": false, + "topic": null, + "reason": "我注意到上一条消息是我几分钟前主动发送的,对方可能正在忙。为了表现出耐心和体贴,我现在最好保持安静,等待对方的回应。" +}} --- 请输出你的决策: @@ -399,6 +417,7 @@ class ProactiveThinkerExecutor: # 对话指引 - 你决定和Ta聊聊关于“{topic}”的话题。 +- **重要**: 在开始你的话题前,必须先用一句通用的、礼貌的开场白进行问候(例如:“在吗?”、“上午好!”、“晚上好呀~”),然后再自然地衔接你的话题,确保整个回复在一条消息内流畅、自然、像人类的说话方式。 - 请结合以上所有情境信息,自然地开启对话。 - 你的语气应该符合你的人设({context["mood_state"]})以及你对Ta的好感度。 """ @@ -436,6 +455,7 @@ class ProactiveThinkerExecutor: # 对话指引 - 你决定和大家聊聊关于“{topic}”的话题。 +- **重要**: 在开始你的话题前,必须先用一句通用的、礼貌的开场白进行问候(例如:“哈喽,大家好呀~”、“下午好!”),然后再自然地衔接你的话题,确保整个回复在一条消息内流畅、自然、像人类的说话方式。 - 你的语气应该更活泼、更具包容性,以吸引更多群成员参与讨论。你的语气应该符合你的人设)。 - 请结合以上所有情境信息,自然地开启对话。 - 可以分享你的看法、提出相关问题,或者开个合适的玩笑。 From a8a42694f552c3042743318eea394ce3e2671400 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Fri, 3 Oct 2025 22:11:49 +0800 Subject: [PATCH 13/14] =?UTF-8?q?refactor(proactive=5Fthinker):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=86=B3=E7=AD=96=E6=8F=90=E7=A4=BA=E8=AF=8D?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E6=97=A5=E7=A8=8B=E4=B8=8E=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E8=AE=B0=E5=BD=95=E4=B8=8A=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为了让主动思考的决策更加贴近当前情境,对决策提示词(Prompt)进行了重构。 - **增强情境感知**:在提示词中增加了日程安排、最近聊天摘要和近期动作历史,为 AI 提供更全面的决策依据。 - **优化结构**:将所有上下文信息整合到“情境分析”部分,使提示词结构更清晰,便于模型理解。 - 修复了获取最近消息时参数传递的错误。 --- .../proactive_thinker/proactive_thinker_executor.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py index 96377c800..8af6bbc35 100644 --- a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py +++ b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py @@ -140,7 +140,7 @@ class ProactiveThinkerExecutor: else "今天没有日程安排。" ) - recent_messages = await message_api.get_recent_messages(stream_id, limit=10) + recent_messages = await message_api.get_recent_messages(stream.stream_id) recent_chat_history = ( await message_api.build_readable_messages_to_str(recent_messages) if recent_messages else "无" ) @@ -386,10 +386,18 @@ class ProactiveThinkerExecutor: # 决策上下文 - **决策理由**: {reason} -- **你和Ta的关系**: + +# 情境分析 +1. **你的日程**: +{context["schedule_context"]} +2. **你和Ta的关系**: - 简短印象: {relationship["short_impression"]} - 详细印象: {relationship["impression"]} - 好感度: {relationship["attitude"]}/100 +3. **最近的聊天摘要**: +{context["recent_chat_history"]} +4. **你最近的相关动作**: +{context["action_history_context"]} # 对话指引 - 你的目标是“破冰”,让对话自然地开始。 From df5a4c717b386c89447d670fe5e8bef8c202a4f3 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Fri, 3 Oct 2025 22:14:58 +0800 Subject: [PATCH 14/14] =?UTF-8?q?refactor(proactive=5Fthinker):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=86=B3=E7=AD=96=E6=8F=90=E7=A4=BA=E8=AF=8D?= =?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=E5=9C=A8=E4=BB=85=E6=9C=89=E8=87=AA?= =?UTF-8?q?=E8=BA=AB=E6=B6=88=E6=81=AF=E6=97=B6=E5=88=B7=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为了防止在没有其他人回复的情况下出现自言自语或刷屏的现象,为主动思考模块的决策提示词增加了一条新规则。 该规则指示模型在判断是否主动发言时,如果上下文中仅存在自身发送的消息,则倾向于不回复,以提升交互的自然性和用户体验。 --- .../built_in/proactive_thinker/proactive_thinker_executor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py index 78d2d6462..e651ecfd5 100644 --- a/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py +++ b/src/plugins/built_in/proactive_thinker/proactive_thinker_executor.py @@ -292,7 +292,8 @@ class ProactiveThinkerExecutor: - **简单问候作为备选**: 如果上下文中没有合适的话题,可以生成一个简单、真诚的日常问候(例如“在忙吗?”,“下午好呀~”)。 - **避免抽象**: 避免创造过于复杂、抽象或需要对方思考很久才能明白的话题。目标是轻松、自然地开启对话。 - **避免过于频繁**: 如果你最近(尤其是在最近的几次决策中)已经主动发起过对话,请倾向于选择“不回复”,除非有非常重要和紧急的事情。 -- **避免打扰**: 如果你最近(尤其是在最近的几次决策中)已经主动发起过对话,请倾向于选择“不回复”,除非有非常重要和紧急的事情。 +- **如果上下文中只有你的消息而没有别人的消息**:选择不回复,以防刷屏或者打扰到别人 + --- 示例1 (基于上下文):