From 6b7c9b5572ca86744810d7dd06984f6f0d19fc52 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Thu, 2 Oct 2025 11:04:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(proactive=5Fthinking):=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E4=B8=80=E5=8D=8A=E5=B9=B6=E9=87=8D=E6=9E=84=E4=B8=BB?= =?UTF-8?q?=E5=8A=A8=E6=80=9D=E8=80=83=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构了主动思考插件的底层逻辑,将其拆分为两个独立的后台任务,以实现更精细和人性化的主动交互策略。 - **`ColdStartTask` (破冰任务)**: - 专门处理在私聊白名单中但从未有过交互的用户。 - 任务启动时,会扫描白名单,为新用户主动创建聊天流并发起初次问候,实现“破冰”效果。 - 解决了之前版本无法主动与全新用户建立联系的问题。 - **`ProactiveThinkingTask` (日常唤醒任务)**: - 负责维护现有聊天流的活跃度。 - 采用动态间隔机制,结合基础间隔、随机扰动和每日不同时段的活跃度因子,模拟更自然的聊天发起时机。 - 持续监控已建立的聊天,在对话冷却后适时地重新发起话题。 - **配置结构优化**: - 在 `bot_config_template.toml` 中新增了 `[proactive_thinking]` 配置节,统一管理所有相关配置。 - 提供了更清晰的选项,如总开关、冷启动开关、白名单设置等,提升了易用性和可配置性。 - 修正了 `config.py` 中错误的字段名 (`ProactiveThinking` -> `proactive_thinking`),确保与配置文件一致。 --- src/config/config.py | 4 +- .../built_in/proactive_thinker/plugin.py | 2 +- .../proacive_thinker_event.py | 212 +++++++++++++++++- template/bot_config_template.toml | 81 +++---- 4 files changed, 238 insertions(+), 61 deletions(-) diff --git a/src/config/config.py b/src/config/config.py index eb98238d4..50b826bf0 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -420,7 +420,9 @@ class Config(ValidatedConfigBase): default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置" ) affinity_flow: AffinityFlowConfig = Field(default_factory=lambda: AffinityFlowConfig(), description="亲和流配置") - ProactiveThinking: ProactiveThinkingConfig = Field(default_factory=lambda: AffinityFlowConfig(), description="主动思考配置") + proactive_thinking: ProactiveThinkingConfig = Field( + default_factory=lambda: ProactiveThinkingConfig(), description="主动思考配置" + ) class APIAdapterConfig(ValidatedConfigBase): diff --git a/src/plugins/built_in/proactive_thinker/plugin.py b/src/plugins/built_in/proactive_thinker/plugin.py index 0a37531ff..c04f82927 100644 --- a/src/plugins/built_in/proactive_thinker/plugin.py +++ b/src/plugins/built_in/proactive_thinker/plugin.py @@ -23,7 +23,7 @@ logger = get_logger(__name__) class ProactiveThinkerPlugin(BasePlugin): """一个主动思考的插件,但现在还只是个空壳子""" plugin_name: str = "proactive_thinker" - enable_plugin: bool = True + enable_plugin: bool = False dependencies: list[str] = [] python_dependencies: list[str] = [] config_file_name: str = "config.toml" 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 91b802afc..6acaca165 100644 --- a/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py +++ b/src/plugins/built_in/proactive_thinker/proacive_thinker_event.py @@ -1,23 +1,217 @@ +import asyncio +import random +import time +from datetime import datetime from typing import List, Union, Type, Optional + +from maim_message import UserInfo + +from src.chat.message_receive.chat_stream import get_chat_manager from src.common.logger import get_logger +from src.config.config import global_config +from src.manager.async_task_manager import async_task_manager, AsyncTask +from src.plugin_system import EventType, BaseEventHandler +from src.plugin_system.apis import chat_api, person_api +from src.plugin_system.base.base_event import HandlerResult logger = get_logger(__name__) -from src.plugin_system import ( - EventType, - BaseEventHandler, - HandlerResult, -) + + +class ColdStartTask(AsyncTask): + """ + 冷启动任务,专门用于处理那些在白名单里,但从未与机器人发生过交互的用户。 + 它的核心职责是“破冰”,主动创建聊天流并发起第一次问候。 + """ + + def __init__(self): + super().__init__(task_name="ColdStartTask") + self.chat_manager = get_chat_manager() + + async def run(self): + """任务主循环,周期性地检查是否有需要“破冰”的新用户。""" + logger.info("冷启动任务已启动,将周期性检查白名单中的新朋友。") + # 初始等待一段时间,确保其他服务(如数据库)完全启动 + await asyncio.sleep(20) + + while True: + 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 + + # 遍历白名单中的每一个用户 + 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 # 跳过已存在的用户 + + logger.info(f"【冷启动】发现白名单新用户 {chat_id},准备发起第一次问候。") + + # 【增强体验】尝试从关系数据库中获取该用户的昵称 + # 这样打招呼时可以更亲切,而不是只知道一个冷冰冰的ID + 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 接管 + await self.chat_manager.get_or_create_stream(platform, user_info) + + # TODO: 在这里调用LLM,生成一句自然的、符合人设的“破冰”问候语,并发送给用户。 + 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) + + # 完成一轮检查后,进入长时休眠 + await asyncio.sleep(3600) + + except asyncio.CancelledError: + logger.info("冷启动任务被正常取消。") + break + except Exception as e: + logger.error(f"【冷启动】任务出现严重错误,将在5分钟后重试: {e}", exc_info=True) + await asyncio.sleep(300) + + +class ProactiveThinkingTask(AsyncTask): + """ + 主动思考的后台任务(日常唤醒),负责在聊天“冷却”后重新活跃气氛。 + 它只处理已经存在的聊天流。 + """ + + def __init__(self): + super().__init__(task_name="ProactiveThinkingTask") + self.chat_manager = get_chat_manager() + + def _get_next_interval(self) -> float: + """ + 动态计算下一次执行的时间间隔,模拟人类行为的随机性。 + 结合了基础间隔、随机偏移和每日不同时段的活跃度调整。 + """ + # 从配置中读取基础间隔和随机范围 + base_interval = global_config.proactive_thinking.interval + sigma = global_config.proactive_thinking.interval_sigma + + # 1. 在 [base - sigma, base + sigma] 范围内随机取一个值 + interval = random.uniform(base_interval - sigma, base_interval + sigma) + + # 2. 根据当前时间,应用活跃度调整因子 + now = datetime.now() + current_time_str = now.strftime("%H:%M") + + adjust_rules = global_config.proactive_thinking.talk_frequency_adjust + if adjust_rules and adjust_rules[0]: + # 按时间对规则排序,确保能找到正确的时间段 + rules = sorted([rule.split(",") for rule in adjust_rules[0][1:]], key=lambda x: x[0]) + + factor = 1.0 + # 找到最后一个小于等于当前时间的规则 + for time_str, factor_str in rules: + if current_time_str >= time_str: + factor = float(factor_str) + else: + break # 后面的时间都比当前晚,无需再找 + # factor > 1 表示更活跃,所以用除法来缩短间隔 + interval /= factor + + # 保证最小间隔,防止过于频繁的骚扰 + return max(60.0, interval) + + async def run(self): + """任务主循环,周期性地检查所有已存在的聊天是否需要“唤醒”。""" + logger.info("日常唤醒任务已启动,将根据动态间隔检查聊天活跃度。") + await asyncio.sleep(15) # 初始等待 + + while True: + # 计算下一次检查前的休眠时间 + next_interval = self._get_next_interval() + try: + logger.debug(f"【日常唤醒】下一次检查将在 {next_interval:.2f} 秒后进行。") + await asyncio.sleep(next_interval) + + logger.info("【日常唤醒】开始检查不活跃的聊天...") + + # 加载白名单配置 + 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} 秒,触发主动对话。") + + # TODO: 在这里调用LLM,生成一句自然的、符合上下文的问候语,并发送。 + + # 【关键步骤】在触发后,立刻更新活跃时间并保存。 + # 这可以防止在同一个检查周期内,对同一个目标因为意外的延迟而发送多条消息。 + stream.update_active_time() + await self.chat_manager._save_stream(stream) + + except asyncio.CancelledError: + logger.info("日常唤醒任务被正常取消。") + break + except Exception as e: + logger.error(f"【日常唤醒】任务出现错误,将在60秒后重试: {e}", exc_info=True) + await asyncio.sleep(60) class ProactiveThinkerEventHandler(BaseEventHandler): - """主动思考需要的启动时触发的事件处理器""" + """主动思考插件的启动事件处理器,负责根据配置启动一个或两个后台任务。""" handler_name: str = "proactive_thinker_on_start" handler_description: str = "主动思考插件的启动事件处理器" init_subscribe: List[Union[EventType, str]] = [EventType.ON_START] async def execute(self, kwargs: dict | None) -> "HandlerResult": - """执行事件处理""" - logger.info("ProactiveThinkerPlugin on_start event triggered.") - # 返回 (是否执行成功, 是否需要继续处理, 可选的返回消息) + """在机器人启动时执行,根据配置决定是否启动后台任务。""" + logger.info("检测到插件启动事件,正在初始化【主动思考】插件...") + # 检查总开关 + if global_config.proactive_thinking.enable: + # 启动负责“日常唤醒”的核心任务 + logger.info("【主动思考】功能已启用,正在启动“日常唤醒”任务...") + proactive_task = ProactiveThinkingTask() + await async_task_manager.add_task(proactive_task) + + # 检查“冷启动”功能的独立开关 + if global_config.proactive_thinking.enable_cold_start: + logger.info("“冷启动”功能已启用,正在启动“破冰”任务...") + cold_start_task = ColdStartTask() + await async_task_manager.add_task(cold_start_task) + + else: + logger.info("【主动思考】功能未启用,所有任务均跳过启动。") return HandlerResult(success=True, continue_process=True, message=None) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 325728696..8869d2f82 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "7.1.3" +version = "7.1.4" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -133,54 +133,6 @@ dynamic_distribution_max_interval = 30.0 # 最大分发间隔(秒) dynamic_distribution_jitter_factor = 0.2 # 分发间隔随机扰动因子 max_concurrent_distributions = 10 # 最大并发处理的消息流数量,可以根据API性能和服务器负载调整 -talk_frequency_adjust = [ - ["", "8:00,1", "12:00,1.2", "18:00,1.5", "01:00,0.6"], - ["qq:114514:group", "12:20,1", "16:10,2", "20:10,1", "00:10,0.3"], - ["qq:1919810:private", "8:20,1", "12:10,2", "20:10,1.5", "00:10,0.2"] -] -# 基于聊天流的个性化活跃度配置 -# 格式:[["platform:chat_id:type", "HH:MM,frequency", "HH:MM,frequency", ...], ...] - -# 全局配置示例: -# [["", "8:00,1", "12:00,2", "18:00,1.5", "00:00,0.5"]] - -# 特定聊天流配置示例: -# [ -# ["", "8:00,1", "12:00,1.2", "18:00,1.5", "01:00,0.6"], # 全局默认配置 -# ["qq:1026294844:group", "12:20,1", "16:10,2", "20:10,1", "00:10,0.3"], # 特定群聊配置 -# ["qq:729957033:private", "8:20,1", "12:10,2", "20:10,1.5", "00:10,0.2"] # 特定私聊配置 -# ] - -# 说明: -# - 当第一个元素为空字符串""时,表示全局默认配置 -# - 当第一个元素为"platform:id:type"格式时,表示特定聊天流配置 -# - 后续元素是"时间,频率"格式,表示从该时间开始使用该活跃度,直到下一个时间点 -# - 优先级:特定聊天流配置 > 全局配置 > 默认 talk_frequency - -# 主动思考功能配置(仅在focus模式下生效) - -enable_proactive_thinking = false # 是否启用主动思考功能 -proactive_thinking_interval = 1500 # 主动思考触发间隔时间(秒),默认1500秒(25分钟) -# TIPS: -# 创意玩法:可以设置为0!设置为0时将基于delta_sigma生成纯随机间隔 -# 负数保险:如果出现了负数,会自动使用绝对值 - -proactive_thinking_in_private = true # 主动思考可以在私聊里面启用 -proactive_thinking_in_group = true # 主动思考可以在群聊里面启用 -# 主动思考启用范围配置 - 按平台和类型分别配置,建议平台配置为小写 -# 格式:["platform:user_id", "platform:user_id", ...] -# 示例:["qq:123456789", "telegram:user123", "bilibili:987654321"] -proactive_thinking_enable_in_private = [] # 启用主动思考的私聊范围,为空则不限制 -proactive_thinking_enable_in_groups = [] # 启用主动思考的群聊范围,为空则不限制 - -delta_sigma = 120 # 正态分布的标准差,控制时间间隔的随机程度 -# 特殊用法: -# - 设置为0:禁用正态分布,使用固定间隔 -# - 设置得很大(如6000):产生高度随机的间隔,即使基础间隔为0也能工作 -# - 负数会自动转换为正数,不用担心配置错误以及极端边界情况 -# 实验建议:试试 proactive_thinking_interval=0 + delta_sigma 非常大 的纯随机模式! -# 结果保证:生成的间隔永远为正数(负数会取绝对值),最小1秒,最大24小时 - [relationship] enable_relationship = true # 是否启用关系系统 @@ -570,4 +522,33 @@ relationship_weight = 0.3 # 人物关系分数权重 # 提及bot相关参数 mention_bot_adjustment_threshold = 0.3 # 提及bot后的调整阈值 mention_bot_interest_score = 0.6 # 提及bot的兴趣分 -base_relationship_score = 0.3 # 基础人物关系分 \ No newline at end of file +base_relationship_score = 0.3 # 基础人物关系分 + +[proactive_thinking] # 主动思考(主动发起对话)功能配置 +# --- 总开关 --- +enable = true # 是否启用主动发起对话功能 + +# --- 触发时机 --- +# 基础触发间隔(秒),AI会围绕这个时间点主动发起对话 +interval = 1500 # 默认25分钟 +# 间隔随机化标准差(秒),让触发时间更自然。设为0则为固定间隔。 +interval_sigma = 120 +# 每日活跃度调整,格式:[["", "HH:MM,factor", ...], ["stream_id", ...]] +# factor > 1.0 会缩短思考间隔,更活跃;factor < 1.0 会延长间隔。 +talk_frequency_adjust = [['', '8:00,1', '12:00,1.2', '18:00,1.5', '01:00,0.6']] + +# --- 作用范围 --- +enable_in_private = true # 是否允许在私聊中主动发起对话 +enable_in_group = true # 是否允许在群聊中主动发起对话 +# 私聊白名单,为空则对所有私聊生效 +# 格式: ["platform:user_id", ...] e.g., ["qq:123456"] +enabled_private_chats = [] +# 群聊白名单,为空则对所有群聊生效 +# 格式: ["platform:group_id", ...] e.g., ["qq:7891011"] +enabled_group_chats = [] + +# --- 冷启动配置 (针对私聊) --- +# 对于白名单中不活跃的私聊,是否允许进行一次“冷启动”问候 +enable_cold_start = true +# 冷启动后,该私聊的下一次主动思考需要等待的最小时间(秒) +cold_start_cooldown = 86400 # 默认24小时 \ No newline at end of file