diff --git a/src/plugins/built_in/kokoro_flow_chatter/proactive_thinker.py b/src/plugins/built_in/kokoro_flow_chatter/proactive_thinker.py index 9d7e7438a..fbd95e13b 100644 --- a/src/plugins/built_in/kokoro_flow_chatter/proactive_thinker.py +++ b/src/plugins/built_in/kokoro_flow_chatter/proactive_thinker.py @@ -22,7 +22,7 @@ from src.plugin_system.apis.unified_scheduler import TriggerType, unified_schedu from .models import EventType, SessionStatus from .planner import generate_plan -from .replyer import generate_reply_text +from .replyer import _clean_reply_text, generate_reply_text from .session import KokoroSession, get_session_manager if TYPE_CHECKING: @@ -211,13 +211,16 @@ class ProactiveThinker: """处理连续思考""" self._stats["continuous_thinking_triggered"] += 1 - # 生成等待中的想法 - thought = self._generate_waiting_thought(session, progress) + # 获取用户名 + user_name = await self._get_user_name(session.user_id, session.stream_id) + + # 调用 LLM 生成等待中的想法 + thought = await self._generate_waiting_thought(session, user_name, progress) # 记录到 mental_log session.add_waiting_update( waiting_thought=thought, - mood="", # 可以根据进度设置心情 + mood="", # 心情已融入 thought 中 ) # 更新思考计数 @@ -232,10 +235,104 @@ class ProactiveThinker: f"progress={progress:.1%}, thought={thought[:30]}..." ) - def _generate_waiting_thought(self, session: KokoroSession, progress: float) -> str: - """生成等待中的想法""" - elapsed_minutes = session.waiting_config.get_elapsed_minutes() - + async def _generate_waiting_thought( + self, + session: KokoroSession, + user_name: str, + progress: float, + ) -> str: + """调用 LLM 生成等待中的想法""" + try: + from src.chat.utils.prompt import global_prompt_manager + from src.plugin_system.apis import llm_api + + from .prompt.builder import get_prompt_builder + from .prompt.prompts import PROMPT_NAMES + + # 使用 PromptBuilder 构建人设块 + prompt_builder = get_prompt_builder() + persona_block = prompt_builder._build_persona_block() + + # 获取关系信息 + relation_block = f"你与 {user_name} 还不太熟悉。" + try: + from src.person_info.relationship_manager import relationship_manager + + person_info_manager = await self._get_person_info_manager() + if person_info_manager: + platform = global_config.bot.platform if global_config else "qq" + person_id = person_info_manager.get_person_id(platform, session.user_id) + relationship = await relationship_manager.get_relationship(person_id) + if relationship: + relation_block = f"你与 {user_name} 的亲密度是 {relationship.intimacy}。{relationship.description or ''}" + except Exception as e: + logger.debug(f"获取关系信息失败: {e}") + + # 获取上次发送的消息 + last_bot_message = "(未知)" + for entry in reversed(session.mental_log): + if entry.event_type == EventType.BOT_PLANNING and entry.actions: + for action in entry.actions: + if action.get("type") == "kfc_reply": + content = action.get("content", "") + if content: + last_bot_message = content[:100] + ("..." if len(content) > 100 else "") + break + if last_bot_message != "(未知)": + break + + # 构建提示词 + elapsed_minutes = session.waiting_config.get_elapsed_minutes() + max_wait_minutes = session.waiting_config.max_wait_seconds / 60 + expected_reaction = session.waiting_config.expected_reaction or "对方能回复点什么" + + prompt = await global_prompt_manager.format_prompt( + PROMPT_NAMES["waiting_thought"], + persona_block=persona_block, + user_name=user_name, + relation_block=relation_block, + last_bot_message=last_bot_message, + expected_reaction=expected_reaction, + elapsed_minutes=elapsed_minutes, + max_wait_minutes=max_wait_minutes, + progress_percent=int(progress * 100), + ) + + # 调用情绪模型 + models = llm_api.get_available_models() + emotion_config = models.get("emotion") or models.get("replyer") + + if not emotion_config: + logger.warning("[ProactiveThinker] 未找到 emotion/replyer 模型配置,使用默认想法") + return self._get_fallback_thought(elapsed_minutes, progress) + + success, raw_response, _, model_name = await llm_api.generate_with_model( + prompt=prompt, + model_config=emotion_config, + request_type="kokoro_flow_chatter.waiting_thought", + ) + + if not success or not raw_response: + logger.warning(f"[ProactiveThinker] LLM 调用失败: {raw_response}") + return self._get_fallback_thought(elapsed_minutes, progress) + + # 使用统一的文本清理函数 + thought = _clean_reply_text(raw_response) + + logger.debug(f"[ProactiveThinker] LLM 生成等待想法 (model={model_name}): {thought[:50]}...") + return thought + + except Exception as e: + logger.error(f"[ProactiveThinker] 生成等待想法失败: {e}") + import traceback + traceback.print_exc() + return self._get_fallback_thought( + session.waiting_config.get_elapsed_minutes(), + progress + ) + + def _get_fallback_thought(self, elapsed_minutes: float, progress: float) -> str: + """获取备用的等待想法(当 LLM 调用失败时使用)""" if progress < 0.4: thoughts = [ f"已经等了 {elapsed_minutes:.0f} 分钟了,对方可能在忙吧...", @@ -254,9 +351,16 @@ class ProactiveThinker: "要不要主动说点什么呢...", "快到时间了,对方还是没回", ] - return random.choice(thoughts) + async def _get_person_info_manager(self): + """获取 person_info_manager""" + try: + from src.person_info.person_info import get_person_info_manager + return get_person_info_manager() + except Exception: + return None + async def _handle_timeout(self, session: KokoroSession) -> None: """处理等待超时""" self._stats["timeout_decisions"] += 1 @@ -577,10 +681,7 @@ class ProactiveThinker: from src.person_info.person_info import get_person_info_manager person_info_manager = get_person_info_manager() - # 从 stream_id 提取 platform(格式通常是 platform_xxx) - platform = "qq" # 默认平台 - if "_" in stream_id: - platform = stream_id.split("_")[0] + platform = global_config.bot.platform if global_config else "qq" person_id = person_info_manager.get_person_id(platform, user_id) person_name = await person_info_manager.get_value(person_id, "person_name") diff --git a/src/plugins/built_in/kokoro_flow_chatter/prompt/builder.py b/src/plugins/built_in/kokoro_flow_chatter/prompt/builder.py index ad90aca76..94ed55263 100644 --- a/src/plugins/built_in/kokoro_flow_chatter/prompt/builder.py +++ b/src/plugins/built_in/kokoro_flow_chatter/prompt/builder.py @@ -149,8 +149,8 @@ class PromptBuilder: # 3. 构建活动流 activity_stream = await self._build_activity_stream(session, user_name) - # 4. 构建当前情况(简化版,不需要那么详细) - current_situation = await self._build_current_situation( + # 4. 构建当前情况(回复器专用,简化版,不包含决策语言) + current_situation = await self._build_replyer_situation( session, user_name, situation_type, extra_context ) @@ -315,9 +315,15 @@ class PromptBuilder: if not history_messages: return "(暂无聊天记录)" + # 过滤非文本消息(如戳一戳、禁言等系统通知) + text_messages = self._filter_text_messages(history_messages) + + if not text_messages: + return "(暂无聊天记录)" + # 构建可读消息 chat_content, _ = await build_readable_messages_with_id( - messages=[msg.flatten() for msg in history_messages[-30:]], # 最多30条 + messages=[msg.flatten() for msg in text_messages[-30:]], # 最多30条 timestamp_mode="normal_no_YMD", truncate=False, show_actions=False, @@ -329,6 +335,33 @@ class PromptBuilder: logger.warning(f"构建聊天历史块失败: {e}") return "(获取聊天记录失败)" + def _filter_text_messages(self, messages: list) -> list: + """ + 过滤非文本消息 + + 移除系统通知消息(如戳一戳、禁言等),只保留正常的文本聊天消息 + + Args: + messages: 消息列表(DatabaseMessages 对象) + + Returns: + 过滤后的消息列表 + """ + filtered = [] + for msg in messages: + # 跳过系统通知消息(戳一戳、禁言等) + if getattr(msg, "is_notify", False): + continue + + # 跳过没有实际文本内容的消息 + content = getattr(msg, "processed_plain_text", "") or getattr(msg, "display_message", "") + if not content or not content.strip(): + continue + + filtered.append(msg) + + return filtered + async def _build_activity_stream( self, session: KokoroSession, @@ -668,6 +701,66 @@ class PromptBuilder: 注意:kfc_reply 动作不需要填写 content 字段,回复内容会单独生成。""" + async def _build_replyer_situation( + self, + session: KokoroSession, + user_name: str, + situation_type: str, + extra_context: dict, + ) -> str: + """ + 构建回复器专用的当前情况描述 + + 与 Planner 的 _build_current_situation 不同,这里不包含决策性语言, + 只描述当前的情景背景 + """ + from datetime import datetime + current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M") + + if situation_type == "new_message": + return f"现在是 {current_time}。{user_name} 刚给你发了消息。" + + elif situation_type == "reply_in_time": + elapsed = session.waiting_config.get_elapsed_seconds() + max_wait = session.waiting_config.max_wait_seconds + return ( + f"现在是 {current_time}。\n" + f"你之前发了消息后在等 {user_name} 的回复。" + f"等了大约 {elapsed / 60:.1f} 分钟(你原本打算最多等 {max_wait / 60:.1f} 分钟)。" + f"现在 {user_name} 回复了!" + ) + + elif situation_type == "reply_late": + elapsed = session.waiting_config.get_elapsed_seconds() + max_wait = session.waiting_config.max_wait_seconds + return ( + f"现在是 {current_time}。\n" + f"你之前发了消息后在等 {user_name} 的回复。" + f"你原本打算最多等 {max_wait / 60:.1f} 分钟,但实际等了 {elapsed / 60:.1f} 分钟才收到回复。" + f"虽然有点迟,但 {user_name} 终于回复了。" + ) + + elif situation_type == "timeout": + elapsed = session.waiting_config.get_elapsed_seconds() + max_wait = session.waiting_config.max_wait_seconds + return ( + f"现在是 {current_time}。\n" + f"你之前发了消息后一直在等 {user_name} 的回复。" + f"你原本打算最多等 {max_wait / 60:.1f} 分钟,现在已经等了 {elapsed / 60:.1f} 分钟了,对方还是没回。" + f"你决定主动说点什么。" + ) + + elif situation_type == "proactive": + silence = extra_context.get("silence_duration", "一段时间") + return ( + f"现在是 {current_time}。\n" + f"你和 {user_name} 已经有一段时间没聊天了(沉默了 {silence})。" + f"你决定主动找 {user_name} 聊点什么。" + ) + + # 默认 + return f"现在是 {current_time}。" + async def _build_reply_context( self, session: KokoroSession, diff --git a/src/plugins/built_in/kokoro_flow_chatter/prompt/prompts.py b/src/plugins/built_in/kokoro_flow_chatter/prompt/prompts.py index 1445360b8..d089888b6 100644 --- a/src/plugins/built_in/kokoro_flow_chatter/prompt/prompts.py +++ b/src/plugins/built_in/kokoro_flow_chatter/prompt/prompts.py @@ -337,6 +337,45 @@ kfc_REPLYER_CONTEXT_PROACTIVE = Prompt( 你决定主动打破沉默,找 {user_name} 聊点什么。想一个自然的开场白,不要太突兀。""", ) +# ================================================================================================= +# 等待思考提示词模板(用于生成等待中的心理活动) +# ================================================================================================= + +kfc_WAITING_THOUGHT = Prompt( + name="kfc_waiting_thought", + template="""# 等待中的心理活动 + +## 你是谁 +{persona_block} + +## 你与 {user_name} 的关系 +{relation_block} + +## 当前情况 +你刚才给 {user_name} 发了消息,现在正在等待对方回复。 + +**你发的消息**:{last_bot_message} +**你期待的反应**:{expected_reaction} +**已等待时间**:{elapsed_minutes:.1f} 分钟 +**计划最多等待**:{max_wait_minutes:.1f} 分钟 +**等待进度**:{progress_percent}% + +## 任务 +请描述你此刻等待时的内心想法。这是你私下的心理活动,不是要发送的消息。 + +**要求**: +- 用第一人称描述你的感受和想法 +- 要符合你的性格和你们的关系 +- 根据等待进度自然表达情绪变化: + - 初期(0-40%):可能比较平静,稍微期待 + - 中期(40-70%):可能开始有点在意,但还好 + - 后期(70-100%):可能有点焦虑、担心,或者想主动做点什么 +- 不要太长,1-2句话即可 +- 不要输出 JSON,直接输出你的想法 + +现在,请直接输出你等待时的内心想法:""", +) + # 导出所有模板名称,方便外部引用 PROMPT_NAMES = { "main": "kfc_main", @@ -347,6 +386,7 @@ PROMPT_NAMES = { "replyer_context_in_time": "kfc_replyer_context_in_time", "replyer_context_late": "kfc_replyer_context_late", "replyer_context_proactive": "kfc_replyer_context_proactive", + "waiting_thought": "kfc_waiting_thought", "situation_new_message": "kfc_situation_new_message", "situation_reply_in_time": "kfc_situation_reply_in_time", "situation_reply_late": "kfc_situation_reply_late", diff --git a/src/plugins/built_in/social_toolkit_plugin/plugin.py b/src/plugins/built_in/social_toolkit_plugin/plugin.py index 90f75b934..ab69ce7c3 100644 --- a/src/plugins/built_in/social_toolkit_plugin/plugin.py +++ b/src/plugins/built_in/social_toolkit_plugin/plugin.py @@ -195,7 +195,7 @@ class PokeAction(BaseAction): for i in range(times): logger.info(f"正在向 {display_name} ({user_id}) 发送第 {i + 1}/{times} 次戳一戳...") await self.send_command( - "SEND_POKE", args=poke_args, display_message=f"戳了戳 {display_name} ({i + 1}/{times})" + "SEND_POKE", args=poke_args, display_message=f"(系统动作)戳了戳 {display_name} ({i + 1}/{times})" ) # 添加一个延迟,避免因发送过快导致后续戳一戳失败 await asyncio.sleep(1.5)