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}”的话题。 +- **重要**: 在开始你的话题前,必须先用一句通用的、礼貌的开场白进行问候(例如:“哈喽,大家好呀~”、“下午好!”),然后再自然地衔接你的话题,确保整个回复在一条消息内流畅、自然、像人类的说话方式。 - 你的语气应该更活泼、更具包容性,以吸引更多群成员参与讨论。你的语气应该符合你的人设)。 - 请结合以上所有情境信息,自然地开启对话。 - 可以分享你的看法、提出相关问题,或者开个合适的玩笑。