diff --git a/src/plugins/built_in/affinity_flow_chatter/proactive/proactive_thinking_executor.py b/src/plugins/built_in/affinity_flow_chatter/proactive/proactive_thinking_executor.py index 6da21b8bd..6ec9eab04 100644 --- a/src/plugins/built_in/affinity_flow_chatter/proactive/proactive_thinking_executor.py +++ b/src/plugins/built_in/affinity_flow_chatter/proactive/proactive_thinking_executor.py @@ -699,6 +699,42 @@ async def execute_proactive_thinking(stream_id: str): try: # 0. 前置检查 + + # 0.-1 检查是否是私聊且 KFC 主动思考已启用(让 KFC 接管私聊主动思考) + try: + from src.chat.message_receive.chat_stream import get_chat_manager + chat_manager = get_chat_manager() + chat_stream = await chat_manager.get_stream(stream_id) + + # 判断是否是私聊(使用 chat_type 枚举或从 stream_id 判断) + is_private = False + if chat_stream: + try: + is_private = chat_stream.chat_type.name == "private" + except Exception: + # 回退:从 stream_id 判断(私聊通常不包含 "group") + is_private = "group" not in stream_id.lower() + + if is_private: + # 这是一个私聊,检查 KFC 是否启用且其主动思考是否启用 + try: + from src.config.config import global_config + kfc_config = getattr(global_config, 'kokoro_flow_chatter', None) + if kfc_config: + kfc_enabled = getattr(kfc_config, 'enable', False) + proactive_config = getattr(kfc_config, 'proactive_thinking', None) + proactive_enabled = getattr(proactive_config, 'enabled', False) if proactive_config else False + + if kfc_enabled and proactive_enabled: + logger.debug( + f"[主动思考] 私聊 {stream_id} 由 KFC 主动思考接管,跳过通用主动思考" + ) + return + except Exception as e: + logger.debug(f"检查 KFC 配置时出错,继续执行通用主动思考: {e}") + except Exception as e: + logger.warning(f"检查私聊/KFC 状态时出错: {e},继续执行") + # 0.0 检查聊天流是否正在处理消息(双重保护) try: from src.chat.message_receive.chat_stream import get_chat_manager diff --git a/src/plugins/built_in/kokoro_flow_chatter/chatter.py b/src/plugins/built_in/kokoro_flow_chatter/chatter.py index e19982718..0cc30a4fa 100644 --- a/src/plugins/built_in/kokoro_flow_chatter/chatter.py +++ b/src/plugins/built_in/kokoro_flow_chatter/chatter.py @@ -143,6 +143,12 @@ class KokoroFlowChatter(BaseChatter): self.scheduler.set_continuous_thinking_callback( self._on_continuous_thinking ) + + # 设置主动思考回调 + if self.enable_proactive: + self.scheduler.set_proactive_thinking_callback( + self._on_proactive_thinking + ) async def execute(self, context: StreamContext) -> dict: """ @@ -573,6 +579,106 @@ class KokoroFlowChatter(BaseChatter): # 简单模式:更新焦虑程度(已在scheduler中处理) # 这里可以添加额外的逻辑 + async def _on_proactive_thinking(self, session: KokoroSession, trigger_reason: str) -> None: + """ + 主动思考回调 + + 当长时间沉默后触发,让 LLM 决定是否主动联系用户。 + 这不是"必须发消息",而是"想一想要不要联系对方"。 + + Args: + session: 会话 + trigger_reason: 触发原因描述 + """ + logger.info(f"[KFC] 处理主动思考: user={session.user_id}, reason={trigger_reason}") + + try: + # 创建正确的 ActionExecutor(使用 session 的 stream_id) + from .action_executor import ActionExecutor + proactive_action_executor = ActionExecutor(session.stream_id) + + # 加载可用动作 + available_actions = await proactive_action_executor.load_actions() + + # 获取 chat_stream 用于构建上下文 + chat_stream = await self._get_chat_stream(session.stream_id) + + # 构建 S4U 上下文数据(包含全局关系信息) + context_data: dict[str, str] = {} + if chat_stream: + try: + from .context_builder import KFCContextBuilder + context_builder = KFCContextBuilder(chat_stream) + context_data = await context_builder.build_all_context( + sender_name=session.user_id, # 主动思考时用 user_id + target_message="", # 没有目标消息 + context=None, + ) + logger.debug(f"[KFC] 主动思考上下文构建完成: {list(context_data.keys())}") + except Exception as e: + logger.warning(f"[KFC] 主动思考构建S4U上下文失败: {e}") + + # 生成主动思考提示词(传入 context_data 以获取全局关系信息) + system_prompt, user_prompt = self.prompt_generator.generate_proactive_thinking_prompt( + session, + trigger_context=trigger_reason, + available_actions=available_actions, + context_data=context_data, + chat_stream=chat_stream, + ) + + # 调用 LLM + llm_response = await self._call_llm(system_prompt, user_prompt) + self.stats["llm_calls"] += 1 + + # 解析响应 + parsed_response = proactive_action_executor.parse_llm_response(llm_response) + + # 检查是否决定不打扰(do_nothing) + is_do_nothing = ( + len(parsed_response.actions) == 0 or + (len(parsed_response.actions) == 1 and parsed_response.actions[0].type == "do_nothing") + ) + + if is_do_nothing: + logger.info(f"[KFC] 主动思考决定不打扰: user={session.user_id}, thought={parsed_response.thought[:50]}...") + # 记录这次"决定不打扰"的思考 + entry = MentalLogEntry( + event_type=MentalLogEventType.PROACTIVE_THINKING, + timestamp=time.time(), + thought=parsed_response.thought, + content="决定不打扰", + emotional_snapshot=session.emotional_state.to_dict(), + metadata={"trigger_reason": trigger_reason, "action": "do_nothing"}, + ) + session.add_mental_log_entry(entry) + await self.session_manager.save_session(session.user_id) + return + + # 执行决定的动作 + execution_result = await proactive_action_executor.execute_actions( + parsed_response, + session, + chat_stream + ) + + logger.info(f"[KFC] 主动思考执行完成: user={session.user_id}, has_reply={execution_result.get('has_reply')}") + + # 如果发送了消息,进入等待状态 + if execution_result.get("has_reply"): + session.start_waiting( + expected_reaction=parsed_response.expected_user_reaction, + max_wait=parsed_response.max_wait_seconds + ) + + # 保存会话 + await self.session_manager.save_session(session.user_id) + + except Exception as e: + logger.error(f"[KFC] 主动思考处理失败: {e}") + import traceback + traceback.print_exc() + def _build_result( self, success: bool, diff --git a/src/plugins/built_in/kokoro_flow_chatter/kfc_scheduler_adapter.py b/src/plugins/built_in/kokoro_flow_chatter/kfc_scheduler_adapter.py index 1e73ebc4b..f512e8c67 100644 --- a/src/plugins/built_in/kokoro_flow_chatter/kfc_scheduler_adapter.py +++ b/src/plugins/built_in/kokoro_flow_chatter/kfc_scheduler_adapter.py @@ -5,16 +5,19 @@ Kokoro Flow Chatter 调度器适配器 不再自己创建后台循环,而是复用全局调度器的基础设施。 核心功能: -1. 会话等待超时检测 -2. 连续思考触发 -3. 与 UnifiedScheduler 的集成 +1. 会话等待超时检测(短期) +2. 连续思考触发(等待期间的内心活动) +3. 主动思考检测(长期沉默后主动发起对话) +4. 与 UnifiedScheduler 的集成 """ import asyncio import time +from datetime import datetime from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional from src.common.logger import get_logger +from src.config.config import global_config from src.plugin_system.apis.unified_scheduler import ( TriggerType, unified_scheduler, @@ -41,9 +44,10 @@ class KFCSchedulerAdapter: 使用 UnifiedScheduler 实现 KFC 的定时任务功能,不再自行管理后台循环。 核心功能: - 1. 定期检查处于 WAITING 状态的会话 - 2. 在特定时间点触发"连续思考" - 3. 处理等待超时并触发决策 + 1. 定期检查处于 WAITING 状态的会话(短期等待超时) + 2. 在特定时间点触发"连续思考"(等待期间内心活动) + 3. 定期检查长期沉默的会话,触发"主动思考"(长期主动发起) + 4. 处理等待超时并触发决策 """ # 连续思考触发点(等待进度的百分比) @@ -51,45 +55,86 @@ class KFCSchedulerAdapter: # 任务名称常量 TASK_NAME_WAITING_CHECK = "kfc_waiting_check" + TASK_NAME_PROACTIVE_CHECK = "kfc_proactive_check" + + # 主动思考检查间隔(5分钟) + PROACTIVE_CHECK_INTERVAL = 300.0 def __init__( self, check_interval: float = 10.0, on_timeout_callback: Optional[Callable[[KokoroSession], Coroutine[Any, Any, None]]] = None, on_continuous_thinking_callback: Optional[Callable[[KokoroSession], Coroutine[Any, Any, None]]] = None, + on_proactive_thinking_callback: Optional[Callable[[KokoroSession, str], Coroutine[Any, Any, None]]] = None, ): """ 初始化调度器适配器 Args: - check_interval: 检查间隔(秒) + check_interval: 等待检查间隔(秒) on_timeout_callback: 超时回调函数 on_continuous_thinking_callback: 连续思考回调函数 + on_proactive_thinking_callback: 主动思考回调函数,接收 (session, trigger_reason) """ self.check_interval = check_interval self.on_timeout_callback = on_timeout_callback self.on_continuous_thinking_callback = on_continuous_thinking_callback + self.on_proactive_thinking_callback = on_proactive_thinking_callback self._registered = False self._schedule_id: Optional[str] = None + self._proactive_schedule_id: Optional[str] = None + + # 加载主动思考配置 + self._load_proactive_config() # 统计信息 self._stats = { "total_checks": 0, "timeouts_triggered": 0, "continuous_thinking_triggered": 0, + "proactive_thinking_triggered": 0, + "proactive_checks": 0, "last_check_time": 0.0, } logger.info("KFCSchedulerAdapter 初始化完成") + def _load_proactive_config(self) -> None: + """加载主动思考相关配置""" + try: + if global_config and hasattr(global_config, 'kokoro_flow_chatter'): + proactive_cfg = global_config.kokoro_flow_chatter.proactive_thinking + self.proactive_enabled = proactive_cfg.enabled + self.silence_threshold = proactive_cfg.silence_threshold_seconds + self.min_interval = proactive_cfg.min_interval_between_proactive + self.min_affinity = getattr(proactive_cfg, 'min_affinity_for_proactive', 0.3) + self.quiet_hours_start = getattr(proactive_cfg, 'quiet_hours_start', "23:00") + self.quiet_hours_end = getattr(proactive_cfg, 'quiet_hours_end', "07:00") + else: + # 默认值 + self.proactive_enabled = True + self.silence_threshold = 7200 # 2小时 + self.min_interval = 1800 # 30分钟 + self.min_affinity = 0.3 + self.quiet_hours_start = "23:00" + self.quiet_hours_end = "07:00" + except Exception as e: + logger.warning(f"加载主动思考配置失败,使用默认值: {e}") + self.proactive_enabled = True + self.silence_threshold = 7200 + self.min_interval = 1800 + self.min_affinity = 0.3 + self.quiet_hours_start = "23:00" + self.quiet_hours_end = "07:00" + async def start(self) -> None: """启动调度器(注册到 UnifiedScheduler)""" if self._registered: logger.warning("KFC 调度器已在运行中") return - # 注册周期性检查任务 + # 注册周期性等待检查任务(每10秒) self._schedule_id = await unified_scheduler.create_schedule( callback=self._check_waiting_sessions, trigger_type=TriggerType.TIME, @@ -97,9 +142,22 @@ class KFCSchedulerAdapter: is_recurring=True, task_name=self.TASK_NAME_WAITING_CHECK, force_overwrite=True, - timeout=30.0, # 单次检查超时 30 秒 + timeout=30.0, ) + # 如果启用了主动思考,注册主动思考检查任务(每5分钟) + if self.proactive_enabled: + self._proactive_schedule_id = await unified_scheduler.create_schedule( + callback=self._check_proactive_sessions, + trigger_type=TriggerType.TIME, + trigger_config={"delay_seconds": self.PROACTIVE_CHECK_INTERVAL}, + is_recurring=True, + task_name=self.TASK_NAME_PROACTIVE_CHECK, + force_overwrite=True, + timeout=120.0, # 主动思考可能需要更长时间(涉及 LLM 调用) + ) + logger.info(f"KFC 主动思考调度已注册: schedule_id={self._proactive_schedule_id}") + self._registered = True logger.info(f"KFC 调度器已注册到 UnifiedScheduler: schedule_id={self._schedule_id}") @@ -111,12 +169,16 @@ class KFCSchedulerAdapter: try: if self._schedule_id: await unified_scheduler.remove_schedule(self._schedule_id) - logger.info(f"KFC 调度器已从 UnifiedScheduler 注销: schedule_id={self._schedule_id}") + logger.info(f"KFC 等待检查调度已注销: schedule_id={self._schedule_id}") + if self._proactive_schedule_id: + await unified_scheduler.remove_schedule(self._proactive_schedule_id) + logger.info(f"KFC 主动思考调度已注销: schedule_id={self._proactive_schedule_id}") except Exception as e: logger.error(f"停止 KFC 调度器时出错: {e}") finally: self._registered = False self._schedule_id = None + self._proactive_schedule_id = None async def _check_waiting_sessions(self) -> None: """检查所有等待中的会话(由 UnifiedScheduler 调用) @@ -344,6 +406,223 @@ class KFCSchedulerAdapter: return random.choice(thoughts) + # ======================================== + # 主动思考相关方法(长期沉默后主动发起对话) + # ======================================== + + async def _check_proactive_sessions(self) -> None: + """ + 检查所有会话是否需要触发主动思考(由 UnifiedScheduler 定期调用) + + 主动思考的触发条件: + 1. 会话处于 IDLE 状态(不在等待回复中) + 2. 距离上次活动超过 silence_threshold + 3. 距离上次主动思考超过 min_interval + 4. 不在勿扰时段 + 5. 与用户的关系亲密度足够 + """ + if not self.proactive_enabled: + return + + # 检查是否在勿扰时段 + if self._is_quiet_hours(): + logger.debug("[KFC] 当前处于勿扰时段,跳过主动思考检查") + return + + self._stats["proactive_checks"] += 1 + + session_manager = get_session_manager() + all_sessions = await session_manager.get_all_sessions() + + current_time = time.time() + + for session in all_sessions: + try: + # 检查是否满足主动思考条件(异步获取全局关系分数) + trigger_reason = await self._should_trigger_proactive(session, current_time) + if trigger_reason: + logger.info( + f"[KFC] 触发主动思考: user={session.user_id}, reason={trigger_reason}" + ) + await self._handle_proactive_thinking(session, trigger_reason) + except Exception as e: + logger.error(f"检查主动思考条件时出错 (user={session.user_id}): {e}") + + def _is_quiet_hours(self) -> bool: + """ + 检查当前是否处于勿扰时段 + + 支持跨午夜的时段(如 23:00 到 07:00) + """ + try: + now = datetime.now() + current_minutes = now.hour * 60 + now.minute + + # 解析开始时间 + start_parts = self.quiet_hours_start.split(":") + start_minutes = int(start_parts[0]) * 60 + int(start_parts[1]) + + # 解析结束时间 + end_parts = self.quiet_hours_end.split(":") + end_minutes = int(end_parts[0]) * 60 + int(end_parts[1]) + + # 处理跨午夜的情况 + if start_minutes <= end_minutes: + # 不跨午夜(如 09:00 到 17:00) + return start_minutes <= current_minutes < end_minutes + else: + # 跨午夜(如 23:00 到 07:00) + return current_minutes >= start_minutes or current_minutes < end_minutes + + except Exception as e: + logger.warning(f"解析勿扰时段配置失败: {e}") + return False + + async def _should_trigger_proactive( + self, + session: KokoroSession, + current_time: float + ) -> Optional[str]: + """ + 检查是否应该触发主动思考 + + 使用全局关系数据库中的关系分数(而不是 KFC 内部的 emotional_state) + + 概率机制:关系越亲密,触发概率越高 + - 亲密度 0.3 → 触发概率 10% + - 亲密度 0.5 → 触发概率 30% + - 亲密度 0.7 → 触发概率 55% + - 亲密度 1.0 → 触发概率 90% + + Args: + session: 会话 + current_time: 当前时间戳 + + Returns: + 触发原因字符串,如果不触发则返回 None + """ + import random + + # 条件1:必须处于 IDLE 状态 + if session.status != SessionStatus.IDLE: + return None + + # 条件2:距离上次活动超过沉默阈值 + silence_duration = current_time - session.last_activity_at + if silence_duration < self.silence_threshold: + return None + + # 条件3:距离上次主动思考超过最小间隔 + if session.last_proactive_at is not None: + time_since_last_proactive = current_time - session.last_proactive_at + if time_since_last_proactive < self.min_interval: + return None + + # 条件4:从数据库获取全局关系分数 + relationship_score = await self._get_global_relationship_score(session.user_id) + if relationship_score < self.min_affinity: + logger.debug( + f"主动思考跳过(关系分数不足): user={session.user_id}, " + f"score={relationship_score:.2f}, min={self.min_affinity:.2f}" + ) + return None + + # 条件5:基于关系分数的概率判断 + # 公式:probability = 0.1 + 0.8 * ((score - min_affinity) / (1.0 - min_affinity))^1.5 + # 这样分数从 min_affinity 到 1.0 映射到概率 10% 到 90% + # 使用1.5次幂让曲线更陡峭,高亲密度时概率增长更快 + normalized_score = (relationship_score - self.min_affinity) / (1.0 - self.min_affinity) + probability = 0.1 + 0.8 * (normalized_score ** 1.5) + probability = min(probability, 0.9) # 最高90%,永远不是100%确定 + + if random.random() > probability: + # 这次检查没触发,但记录一下(用于调试) + logger.debug( + f"主动思考概率检查未通过: user={session.user_id}, " + f"score={relationship_score:.2f}, probability={probability:.1%}" + ) + return None + + # 所有条件满足,生成触发原因 + silence_hours = silence_duration / 3600 + logger.info( + f"主动思考触发: user={session.user_id}, " + f"silence={silence_hours:.1f}h, score={relationship_score:.2f}, prob={probability:.1%}" + ) + return f"沉默了{silence_hours:.1f}小时,想主动关心一下对方" + + async def _get_global_relationship_score(self, user_id: str) -> float: + """ + 从全局关系数据库获取关系分数 + + Args: + user_id: 用户ID + + Returns: + 关系分数 (0.0-1.0),如果没有记录返回默认值 0.3 + """ + try: + from src.common.database.api.specialized import get_user_relationship + + # 从 user_id 解析 platform(格式通常是 "platform_userid") + # 这里假设 user_id 中包含 platform 信息,需要根据实际情况调整 + # 先尝试直接查询,如果失败再用默认值 + relationship = await get_user_relationship( + platform="qq", # TODO: 从 session 或 stream_id 获取真实 platform + user_id=user_id, + target_id="bot", + ) + + if relationship and hasattr(relationship, 'relationship_score'): + return relationship.relationship_score + + # 没有找到关系记录,返回默认值 + return 0.3 + + except Exception as e: + logger.warning(f"获取全局关系分数失败 (user={user_id}): {e}") + return 0.3 # 出错时返回较低的默认值 + + async def _handle_proactive_thinking( + self, + session: KokoroSession, + trigger_reason: str + ) -> None: + """ + 处理主动思考 + + Args: + session: 会话 + trigger_reason: 触发原因 + """ + self._stats["proactive_thinking_triggered"] += 1 + + # 更新会话状态 + session.last_proactive_at = time.time() + session.proactive_count += 1 + + # 添加主动思考日志 + proactive_entry = MentalLogEntry( + event_type=MentalLogEventType.PROACTIVE_THINKING, + timestamp=time.time(), + thought=trigger_reason, + content="主动思考触发", + emotional_snapshot=session.emotional_state.to_dict(), + metadata={"trigger_reason": trigger_reason}, + ) + session.add_mental_log_entry(proactive_entry) + + # 保存会话状态 + session_manager = get_session_manager() + await session_manager.save_session(session.user_id) + + # 调用主动思考回调(由 chatter 处理实际的 LLM 调用和动作执行) + if self.on_proactive_thinking_callback: + try: + await self.on_proactive_thinking_callback(session, trigger_reason) + except Exception as e: + logger.error(f"执行主动思考回调时出错 (user={session.user_id}): {e}") + def set_timeout_callback( self, callback: Callable[[KokoroSession], Coroutine[Any, Any, None]], @@ -358,6 +637,13 @@ class KFCSchedulerAdapter: """设置连续思考回调函数""" self.on_continuous_thinking_callback = callback + def set_proactive_thinking_callback( + self, + callback: Callable[[KokoroSession, str], Coroutine[Any, Any, None]], + ) -> None: + """设置主动思考回调函数""" + self.on_proactive_thinking_callback = callback + def get_stats(self) -> dict[str, Any]: """获取统计信息""" return { @@ -388,6 +674,7 @@ async def initialize_scheduler( check_interval: float = 10.0, on_timeout_callback: Optional[Callable[[KokoroSession], Coroutine[Any, Any, None]]] = None, on_continuous_thinking_callback: Optional[Callable[[KokoroSession], Coroutine[Any, Any, None]]] = None, + on_proactive_thinking_callback: Optional[Callable[[KokoroSession, str], Coroutine[Any, Any, None]]] = None, ) -> KFCSchedulerAdapter: """ 初始化并启动调度器 @@ -396,6 +683,7 @@ async def initialize_scheduler( check_interval: 检查间隔 on_timeout_callback: 超时回调 on_continuous_thinking_callback: 连续思考回调 + on_proactive_thinking_callback: 主动思考回调 Returns: KFCSchedulerAdapter: 调度器适配器实例 @@ -405,6 +693,7 @@ async def initialize_scheduler( check_interval=check_interval, on_timeout_callback=on_timeout_callback, on_continuous_thinking_callback=on_continuous_thinking_callback, + on_proactive_thinking_callback=on_proactive_thinking_callback, ) await _scheduler_adapter.start() return _scheduler_adapter diff --git a/src/plugins/built_in/kokoro_flow_chatter/models.py b/src/plugins/built_in/kokoro_flow_chatter/models.py index 19c5e49f2..dfa07becd 100644 --- a/src/plugins/built_in/kokoro_flow_chatter/models.py +++ b/src/plugins/built_in/kokoro_flow_chatter/models.py @@ -47,6 +47,7 @@ class MentalLogEventType(Enum): TIMEOUT_DECISION = "timeout_decision" # 超时决策事件 STATE_CHANGE = "state_change" # 状态变更事件 CONTINUOUS_THINKING = "continuous_thinking" # 连续思考事件 + PROACTIVE_THINKING = "proactive_thinking" # 主动思考事件(长期沉默后主动发起) def __str__(self) -> str: return self.value @@ -222,6 +223,10 @@ class KokoroSession: continuous_thinking_count: int = 0 last_continuous_thinking_at: Optional[float] = None + # 主动思考相关(长期沉默后主动发起对话) + last_proactive_at: Optional[float] = None # 上次主动思考的时间 + proactive_count: int = 0 # 主动思考的次数(累计) + def add_mental_log_entry(self, entry: MentalLogEntry, max_log_size: int = 100) -> None: """ 添加心理活动日志条目 @@ -284,6 +289,8 @@ class KokoroSession: "total_interactions": self.total_interactions, "continuous_thinking_count": self.continuous_thinking_count, "last_continuous_thinking_at": self.last_continuous_thinking_at, + "last_proactive_at": self.last_proactive_at, + "proactive_count": self.proactive_count, } @classmethod @@ -320,6 +327,8 @@ class KokoroSession: total_interactions=data.get("total_interactions", 0), continuous_thinking_count=data.get("continuous_thinking_count", 0), last_continuous_thinking_at=data.get("last_continuous_thinking_at"), + last_proactive_at=data.get("last_proactive_at"), + proactive_count=data.get("proactive_count", 0), ) diff --git a/src/plugins/built_in/kokoro_flow_chatter/prompt_generator.py b/src/plugins/built_in/kokoro_flow_chatter/prompt_generator.py index 4e8a18a2c..fdda5f90c 100644 --- a/src/plugins/built_in/kokoro_flow_chatter/prompt_generator.py +++ b/src/plugins/built_in/kokoro_flow_chatter/prompt_generator.py @@ -205,23 +205,31 @@ class PromptGenerator: {narrative_history} ## 当前情况 +现在是 {current_time}。 +距离你们上次对话已经过去了 {silence_duration}。 + +{relation_block} + {trigger_context} -## 触发类型 -{trigger_type} - ## 你的任务 -这是一次"主动思考"触发。你不是因为收到消息才行动,而是因为内心的某种驱动力。 -现在你需要: -1. 感受一下现在的心情和想法 -2. 思考是否需要主动联系对方 -3. 如果决定主动,想好要说什么或做什么 -4. 如果决定不主动,也要有明确的理由 +这是一次"主动思考"——你突然想起了对方,想了想要不要联系ta。 -注意:主动联系应该是自然的、符合你们关系的。不要显得过于依赖或强迫。 -你可以选择发消息、发图片、唱首歌、或者只是在心里想想然后什么都不做。 +请像真人一样思考: +1. **感受一下此刻的心情**:你真的想联系对方吗?还是只是习惯性地想起? +2. **考虑对方的情况**:现在这个时间,对方可能在干嘛?方便被打扰吗? +3. **回忆上次对话**:你们聊了什么?对话是怎么结束的? +4. **做出决定**: + - 如果决定联系:想好说什么,要自然,不要刻意 + - 如果决定不联系:这也完全ok,不打扰也是一种温柔 -请以JSON格式输出你的完整心理活动和决策。""" +**重要提醒**: +- 你不是必须发消息的,"算了,不打扰了"是完全合理的选择 +- 如果决定联系,内容要自然——可以是分享、关心、延续话题,但不要生硬 +- 避免机械式的问候(如固定的"早安""晚安"),除非你们的关系真的会这样打招呼 + +请以JSON格式输出你的完整心理活动和决策。 +如果决定不打扰,actions 里放一个 `{{"type": "do_nothing"}}` 就好。""" def __init__(self, persona_description: str = ""): """ @@ -663,34 +671,72 @@ class PromptGenerator: def generate_proactive_thinking_prompt( self, session: KokoroSession, - trigger_type: str, trigger_context: str, available_actions: Optional[dict[str, ActionInfo]] = None, + context_data: Optional[dict[str, str]] = None, + chat_stream: Optional["ChatStream"] = None, ) -> tuple[str, str]: """ 生成主动思考场景的提示词 这是私聊专属的功能,用于实现"主动找话题、主动关心用户"。 + 主动思考不是"必须发消息",而是"想一想要不要联系对方"。 Args: session: 当前会话 - trigger_type: 触发类型(如 silence_timeout, memory_event 等) - trigger_context: 触发上下文描述 + trigger_context: 触发上下文描述(如"沉默了2小时") available_actions: 可用动作字典 + context_data: S4U上下文数据(包含全局关系信息) + chat_stream: 聊天流 Returns: tuple[str, str]: (系统提示词, 用户提示词) """ - system_prompt = self.generate_system_prompt(session, available_actions) + from datetime import datetime + import time + + # 生成系统提示词(使用 context_data 获取完整的关系和记忆信息) + system_prompt = self.generate_system_prompt( + session, + available_actions, + context_data=context_data, + chat_stream=chat_stream, + ) narrative_history = self._format_narrative_history( session.mental_log, max_entries=10, # 主动思考时使用较少的历史 ) + # 计算沉默时长 + silence_seconds = time.time() - session.last_activity_at + if silence_seconds < 3600: + silence_duration = f"{silence_seconds / 60:.0f}分钟" + else: + silence_duration = f"{silence_seconds / 3600:.1f}小时" + + # 当前时间 + current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M") + + # 从 context_data 获取全局关系信息(这是正确的来源) + relation_block = "" + if context_data: + relation_info = context_data.get("relation_info", "") + if relation_info: + relation_block = f"### 你与对方的关系\n{relation_info}" + + if not relation_block: + # 回退:使用 session 的情感状态(不太准确但有总比没有好) + es = session.emotional_state + relation_block = f"""### 你与对方的关系 +- 当前心情:{es.mood} +- 对对方的印象:{es.impression_of_user or "还在慢慢了解中"}""" + user_prompt = self.PROACTIVE_THINKING_USER_PROMPT_TEMPLATE.format( narrative_history=narrative_history, - trigger_type=trigger_type, + current_time=current_time, + silence_duration=silence_duration, + relation_block=relation_block, trigger_context=trigger_context, ) diff --git a/src/plugins/built_in/kokoro_flow_chatter/session_manager.py b/src/plugins/built_in/kokoro_flow_chatter/session_manager.py index 7884f6b6a..9661a0247 100644 --- a/src/plugins/built_in/kokoro_flow_chatter/session_manager.py +++ b/src/plugins/built_in/kokoro_flow_chatter/session_manager.py @@ -421,6 +421,17 @@ class SessionManager: return waiting_sessions + async def get_all_sessions(self) -> list[KokoroSession]: + """ + 获取所有内存中的会话 + + 用于主动思考检查等需要遍历所有会话的场景 + + Returns: + list[KokoroSession]: 所有会话列表 + """ + return list(self._sessions.values()) + async def get_session_statistics(self) -> dict: """ 获取会话统计信息 diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index d0ea7e5e4..88115087d 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "7.9.2" +version = "7.9.3" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -655,6 +655,8 @@ enable_continuous_thinking = true # 是否在等待期间启用心理活动更 # --- 私聊专属主动思考配置 --- # 注意:这是KFC专属的主动思考配置,只有当KFC启用时才生效。 # 它旨在模拟更真实、情感驱动的互动,而非简单的定时任务。 +# 「主动思考」是「想一想要不要联系对方」,不是「到时间就发消息」。 +# 她可能决定说些什么,也可能决定「算了,不打扰了」。 [kokoro_flow_chatter.proactive_thinking] enabled = true # 是否启用KFC的私聊主动思考。 @@ -667,6 +669,7 @@ min_affinity_for_proactive = 0.3 # 需要达到最低好感度,她才会开始 # 3. 频率呼吸:为了避免打扰,她的关心总是有间隔的。 min_interval_between_proactive = 1800 # 两次主动思考之间的最小间隔(秒,默认30分钟)。 -# 4. 自然问候:在特定的时间,她会像朋友一样送上问候。 -enable_morning_greeting = true # 是否启用早安问候 (例如: 8:00 - 9:00)。 -enable_night_greeting = true # 是否启用晚安问候 (例如: 22:00 - 23:00)。 +# 4. 勿扰时段:在这个时间范围内,不会触发主动思考(避免深夜打扰用户)。 +# 格式为 "HH:MM",使用24小时制。如果 start > end,表示跨越午夜(如 23:00 到 07:00)。 +quiet_hours_start = "23:00" # 勿扰开始时间 +quiet_hours_end = "07:00" # 勿扰结束时间