From c68bf4ad4fc95813cb4a5896662ce08283ec7c33 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sun, 30 Nov 2025 13:40:59 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84Kokoro=20Flow=20Chatter?= =?UTF-8?q?=EF=BC=9A=E7=A7=BB=E9=99=A4=E5=B7=B2=E5=BC=83=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E5=90=8E=E5=A4=84=E7=90=86=E5=99=A8=E5=92=8C?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E7=AE=A1=E7=90=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除了`response_post_processor.py`和`session_manager.py`,因为它们已不再需要。 - 更新了`__init__.py`文件,移除了对`ActionExecutor`的引用。 - 删除了`action_executor.py`,并将动作执行直接集成到`chatter.py`和`proactive_thinker.py`中。 - 在`KokoroFlowChatterV2`中重构了动作执行逻辑,以直接使用`ChatterActionManager`。 - 增强了主动思考逻辑,以简化操作执行,而无需依赖已移除的`ActionExecutor`。 --- src/chat/planner_actions/action_manager.py | 169 +--- src/chat/planner_actions/action_modifier.py | 37 +- src/plugin_system/base/base_action.py | 1 + src/plugin_system/base/component_types.py | 3 + .../affinity_flow_chatter/planner/planner.py | 2 +- src/plugins/built_in/core_actions/reply.py | 220 ++++- .../built_in/kokoro_flow_chatter/__init__.py | 26 - .../kokoro_flow_chatter/action_executor.py | 711 -------------- .../built_in/kokoro_flow_chatter/chatter.py | 877 ------------------ .../built_in/kokoro_flow_chatter/config.py | 251 ----- .../kokoro_flow_chatter/context_builder.py | 528 ----------- .../kfc_scheduler_adapter.py | 707 -------------- .../built_in/kokoro_flow_chatter/models.py | 459 --------- .../built_in/kokoro_flow_chatter/plugin.py | 218 ----- .../kokoro_flow_chatter/proactive_thinking.py | 528 ----------- .../kokoro_flow_chatter/prompt_generator.py | 807 ---------------- .../kokoro_flow_chatter/prompt_modules.py | 369 -------- .../response_post_processor.py | 169 ---- .../kokoro_flow_chatter/session_manager.py | 561 ----------- .../kokoro_flow_chatter_v2/__init__.py | 2 - .../kokoro_flow_chatter_v2/action_executor.py | 228 ----- .../kokoro_flow_chatter_v2/chatter.py | 44 +- .../proactive_thinker.py | 36 +- 23 files changed, 319 insertions(+), 6634 deletions(-) delete mode 100644 src/plugins/built_in/kokoro_flow_chatter/__init__.py delete mode 100644 src/plugins/built_in/kokoro_flow_chatter/action_executor.py delete mode 100644 src/plugins/built_in/kokoro_flow_chatter/chatter.py delete mode 100644 src/plugins/built_in/kokoro_flow_chatter/config.py delete mode 100644 src/plugins/built_in/kokoro_flow_chatter/context_builder.py delete mode 100644 src/plugins/built_in/kokoro_flow_chatter/kfc_scheduler_adapter.py delete mode 100644 src/plugins/built_in/kokoro_flow_chatter/models.py delete mode 100644 src/plugins/built_in/kokoro_flow_chatter/plugin.py delete mode 100644 src/plugins/built_in/kokoro_flow_chatter/proactive_thinking.py delete mode 100644 src/plugins/built_in/kokoro_flow_chatter/prompt_generator.py delete mode 100644 src/plugins/built_in/kokoro_flow_chatter/prompt_modules.py delete mode 100644 src/plugins/built_in/kokoro_flow_chatter/response_post_processor.py delete mode 100644 src/plugins/built_in/kokoro_flow_chatter/session_manager.py delete mode 100644 src/plugins/built_in/kokoro_flow_chatter_v2/action_executor.py diff --git a/src/chat/planner_actions/action_manager.py b/src/chat/planner_actions/action_manager.py index ef8b24657..4c4f30e30 100644 --- a/src/chat/planner_actions/action_manager.py +++ b/src/chat/planner_actions/action_manager.py @@ -4,12 +4,10 @@ import traceback from typing import Any, TYPE_CHECKING from src.chat.message_receive.chat_stream import get_chat_manager -from src.chat.utils.timer_calculator import Timer from src.common.data_models.database_data_model import DatabaseMessages from src.common.logger import get_logger -from src.config.config import global_config from src.person_info.person_info import get_person_info_manager -from src.plugin_system.apis import database_api, generator_api, message_api, send_api +from src.plugin_system.apis import database_api from src.plugin_system.base.base_action import BaseAction from src.plugin_system.base.component_types import ActionInfo, ComponentType from src.plugin_system.core.component_registry import component_registry @@ -160,6 +158,8 @@ class ChatterActionManager: ) -> Any: """ 执行单个动作的通用函数 + + 所有动作(包括 reply/respond)都通过 BaseAction.execute() 执行 Args: action_name: 动作名称 @@ -169,14 +169,13 @@ class ChatterActionManager: action_data: 动作数据 thinking_id: 思考ID log_prefix: 日志前缀 + clear_unread_messages: 是否清除未读消息 Returns: 执行结果 """ - chat_stream = None try: - logger.debug(f"🎯 [ActionManager] execute_action接收到 target_message: {target_message}") # 通过chat_id获取chat_stream chat_manager = get_chat_manager() chat_stream = await chat_manager.get_stream(chat_id) @@ -193,149 +192,33 @@ class ChatterActionManager: # 设置正在回复的状态 chat_stream.context.is_replying = True + # no_action 特殊处理 if action_name == "no_action": return {"action_type": "no_action", "success": True, "reply_text": "", "command": ""} - if action_name == "no_reply": - # 直接处理no_reply逻辑,不再通过动作系统 - reason = reasoning or "选择不回复" - logger.info(f"{log_prefix} 选择不回复,原因: {reason}") + # 统一通过 _handle_action 执行所有动作 + success, reply_text, command = await self._handle_action( + chat_stream, + action_name, + reasoning, + action_data or {}, + {}, # cycle_timers + thinking_id, + target_message, + ) - # 存储no_reply信息到数据库(支持批量存储) - if self._batch_storage_enabled: - self.add_action_to_batch( - action_name="no_reply", - action_data={"reason": reason}, - thinking_id=thinking_id or "", - action_done=True, - action_build_into_prompt=False, - action_prompt_display=reason, - ) - else: - asyncio.create_task(database_api.store_action_info( - chat_stream=chat_stream, - action_build_into_prompt=False, - action_prompt_display=reason, - action_done=True, - thinking_id=thinking_id or "", - action_data={"reason": reason}, - action_name="no_reply", - )) + # 记录执行的动作到目标消息 + if success: + asyncio.create_task(self._record_action_to_message(chat_stream, action_name, target_message, action_data)) + # 重置打断计数 + await self._reset_interruption_count_after_action(chat_stream.stream_id) - return {"action_type": "no_reply", "success": True, "reply_text": "", "command": ""} - - elif action_name != "reply" and action_name != "respond" and action_name != "no_action": - # 执行普通动作 - success, reply_text, command = await self._handle_action( - chat_stream, - action_name, - reasoning, - action_data or {}, - {}, # cycle_timers - thinking_id, - target_message, - ) - - # 记录执行的动作到目标消息 - if success: - asyncio.create_task(self._record_action_to_message(chat_stream, action_name, target_message, action_data)) - # 重置打断计数 - await self._reset_interruption_count_after_action(chat_stream.stream_id) - - return { - "action_type": action_name, - "success": success, - "reply_text": reply_text, - "command": command, - } - else: - # 检查目标消息是否为表情包消息以及配置是否允许回复表情包 - if target_message and getattr(target_message, "is_emoji", False): - # 如果是表情包消息且配置不允许回复表情包,则跳过回复 - if not getattr(global_config.chat, "allow_reply_to_emoji", True): - logger.info(f"{log_prefix} 目标消息为表情包且配置不允许回复表情包,跳过回复") - return {"action_type": action_name, "success": True, "reply_text": "", "skip_reason": "emoji_not_allowed"} - - # 生成回复 (reply 或 respond) - # reply: 针对单条消息的回复,使用 s4u 模板 - # respond: 对未读消息的统一回应,使用 normal 模板 - try: - # 根据动作类型确定提示词模式 - prompt_mode = "s4u" if action_name == "reply" else "normal" - - # 将prompt_mode传递给generate_reply - action_data_with_mode = (action_data or {}).copy() - action_data_with_mode["prompt_mode"] = prompt_mode - - # 只传递当前正在执行的动作,而不是所有可用动作 - # 这样可以让LLM明确知道"已决定执行X动作",而不是"有这些动作可用" - current_action_info = self._using_actions.get(action_name) - current_actions: dict[str, Any] = {action_name: current_action_info} if current_action_info else {} - - # 附加目标消息信息(如果存在) - if target_message: - # 提取目标消息的关键信息 - target_msg_info = { - "message_id": getattr(target_message, "message_id", ""), - "sender": getattr(target_message.user_info, "user_nickname", "") if hasattr(target_message, "user_info") else "", - "content": getattr(target_message, "processed_plain_text", ""), - "time": getattr(target_message, "time", 0), - } - current_actions["_target_message"] = target_msg_info - - success, response_set, _ = await generator_api.generate_reply( - chat_stream=chat_stream, - reply_message=target_message, - action_data=action_data_with_mode, - available_actions=current_actions, # type: ignore - enable_tool=global_config.tool.enable_tool, - request_type="chat.replyer", - from_plugin=False, - ) - if not success or not response_set: - # 安全地获取 processed_plain_text - if target_message: - msg_text = target_message.processed_plain_text or "未知消息" - else: - msg_text = "未知消息" - - logger.info(f"对 {msg_text} 的回复生成失败") - return {"action_type": action_name, "success": False, "reply_text": "", "loop_info": None} - except asyncio.CancelledError: - logger.debug(f"{log_prefix} 并行执行:回复生成任务已被取消") - return {"action_type": action_name, "success": False, "reply_text": "", "loop_info": None} - - # 从action_data中提取should_quote_reply参数 - should_quote_reply = None - if action_data and isinstance(action_data, dict): - should_quote_reply = action_data.get("should_quote_reply", None) - - # respond动作默认不引用回复,保持对话流畅 - if action_name == "respond" and should_quote_reply is None: - should_quote_reply = False - - async def _after_reply(): - # 发送并存储回复 - reply_text, cycle_timers_reply = await self._send_and_store_reply( - chat_stream, - response_set, - asyncio.get_event_loop().time(), - target_message, - {}, # cycle_timers - thinking_id, - [], # actions - should_quote_reply, # 传递should_quote_reply参数 - ) - - # 记录回复动作到目标消息 - await self._record_action_to_message(chat_stream, action_name, target_message, action_data) - - # 回复成功,重置打断计数 - await self._reset_interruption_count_after_action(chat_stream.stream_id) - - return reply_text - asyncio.create_task(_after_reply()) - return {"action_type": action_name, "success": True} + return { + "action_type": action_name, + "success": success, + "reply_text": reply_text, + "command": command, + } except Exception as e: logger.error(f"{log_prefix} 执行动作时出错: {e}") diff --git a/src/chat/planner_actions/action_modifier.py b/src/chat/planner_actions/action_modifier.py index ca057d8e7..a0197d7a5 100644 --- a/src/chat/planner_actions/action_modifier.py +++ b/src/chat/planner_actions/action_modifier.py @@ -57,6 +57,7 @@ class ActionModifier: async def modify_actions( self, message_content: str = "", + chatter_name: str = "", ): # sourcery skip: use-named-expression """ 动作修改流程,整合传统观察处理和新的激活类型判定 @@ -66,6 +67,10 @@ class ActionModifier: 2. 基于激活类型的智能动作判定,最终确定可用动作集 处理后,ActionManager 将包含最终的可用动作集,供规划器直接使用 + + Args: + message_content: 消息内容 + chatter_name: 当前使用的 Chatter 名称,用于过滤只允许特定 Chatter 使用的动作 """ # 初始化log_prefix await self._initialize_log_prefix() @@ -82,13 +87,14 @@ class ActionModifier: logger.debug(f"{self.log_prefix}开始完整动作修改流程") + removals_s0: list[tuple[str, str]] = [] # 第0阶段:聊天类型和Chatter过滤 removals_s1: list[tuple[str, str]] = [] removals_s2: list[tuple[str, str]] = [] removals_s3: list[tuple[str, str]] = [] all_actions = self.action_manager.get_using_actions() - # === 第0阶段:根据聊天类型过滤动作 === + # === 第0阶段:根据聊天类型和Chatter过滤动作 === from src.chat.utils.utils import get_chat_type_and_target_info from src.plugin_system.base.component_types import ChatType, ComponentType from src.plugin_system.core.component_registry import component_registry @@ -97,26 +103,35 @@ class ActionModifier: is_group_chat, _ = await get_chat_type_and_target_info(self.chat_id) all_registered_actions = component_registry.get_components_by_type(ComponentType.ACTION) - chat_type_removals = [] for action_name in list(all_actions.keys()): if action_name in all_registered_actions: action_info = all_registered_actions[action_name] + + # 检查聊天类型限制 chat_type_allow = getattr(action_info, "chat_type_allow", ChatType.ALL) - - # 检查是否符合聊天类型限制 - should_keep = ( + should_keep_chat_type = ( chat_type_allow == ChatType.ALL or (chat_type_allow == ChatType.GROUP and is_group_chat) or (chat_type_allow == ChatType.PRIVATE and not is_group_chat) ) - - if not should_keep: - chat_type_removals.append((action_name, f"不支持{'群聊' if is_group_chat else '私聊'}")) + + if not should_keep_chat_type: + removals_s0.append((action_name, f"不支持{'群聊' if is_group_chat else '私聊'}")) self.action_manager.remove_action_from_using(action_name) + continue + + # 检查 Chatter 限制 + chatter_allow = getattr(action_info, "chatter_allow", []) + if chatter_allow and chatter_name: + # 如果设置了 chatter_allow 且提供了 chatter_name,则检查是否匹配 + if chatter_name not in chatter_allow: + removals_s0.append((action_name, f"仅限 {', '.join(chatter_allow)} 使用")) + self.action_manager.remove_action_from_using(action_name) + continue - if chat_type_removals: - logger.info(f"{self.log_prefix} 第0阶段:根据聊天类型过滤 - 移除了 {len(chat_type_removals)} 个动作") - for action_name, reason in chat_type_removals: + if removals_s0: + logger.info(f"{self.log_prefix} 第0阶段:类型/Chatter过滤 - 移除了 {len(removals_s0)} 个动作") + for action_name, reason in removals_s0: logger.debug(f"{self.log_prefix} - 移除 {action_name}: {reason}") message_list_before_now_half = await get_raw_msg_before_timestamp_with_chat( diff --git a/src/plugin_system/base/base_action.py b/src/plugin_system/base/base_action.py index 27e877ff5..44ef212ce 100644 --- a/src/plugin_system/base/base_action.py +++ b/src/plugin_system/base/base_action.py @@ -559,6 +559,7 @@ class BaseAction(ABC): action_require=getattr(cls, "action_require", []).copy(), associated_types=getattr(cls, "associated_types", []).copy(), chat_type_allow=getattr(cls, "chat_type_allow", ChatType.ALL), + chatter_allow=getattr(cls, "chatter_allow", []).copy(), # 二步Action相关属性 is_two_step_action=getattr(cls, "is_two_step_action", False), step_one_description=getattr(cls, "step_one_description", ""), diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index 55fa28284..aa3147785 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -209,6 +209,7 @@ class ActionInfo(ComponentInfo): mode_enable: ChatMode = ChatMode.ALL parallel_action: bool = False chat_type_allow: ChatType = ChatType.ALL # 允许的聊天类型 + chatter_allow: list[str] = field(default_factory=list) # 允许的 Chatter 列表,空则允许所有 # 二步Action相关属性 is_two_step_action: bool = False # 是否为二步Action step_one_description: str = "" # 第一步的描述 @@ -226,6 +227,8 @@ class ActionInfo(ComponentInfo): self.associated_types = [] if self.sub_actions is None: self.sub_actions = [] + if self.chatter_allow is None: + self.chatter_allow = [] self.component_type = ComponentType.ACTION diff --git a/src/plugins/built_in/affinity_flow_chatter/planner/planner.py b/src/plugins/built_in/affinity_flow_chatter/planner/planner.py index c12bf71ab..703265624 100644 --- a/src/plugins/built_in/affinity_flow_chatter/planner/planner.py +++ b/src/plugins/built_in/affinity_flow_chatter/planner/planner.py @@ -258,7 +258,7 @@ class ChatterActionPlanner: # 3. 在规划前,先进行动作修改 from src.chat.planner_actions.action_modifier import ActionModifier action_modifier = ActionModifier(self.action_manager, self.chat_id) - await action_modifier.modify_actions() + await action_modifier.modify_actions(chatter_name="AffinityFlowChatter") # 4. 生成初始计划 initial_plan = await self.generator.generate(ChatMode.FOCUS) diff --git a/src/plugins/built_in/core_actions/reply.py b/src/plugins/built_in/core_actions/reply.py index 9a90f7e33..08d77c2ce 100644 --- a/src/plugins/built_in/core_actions/reply.py +++ b/src/plugins/built_in/core_actions/reply.py @@ -1,15 +1,19 @@ """ 回复动作模块 -定义了两种回复动作: +定义了三种回复相关动作: - reply: 针对单条消息的深度回复(使用 s4u 模板) - respond: 对未读消息的统一回应(使用 normal 模板) +- no_reply: 选择不回复 """ +import asyncio from typing import ClassVar from src.common.logger import get_logger +from src.config.config import global_config from src.plugin_system import ActionActivationType, BaseAction, ChatMode +from src.plugin_system.apis import database_api, generator_api, send_api logger = get_logger("reply_actions") @@ -21,6 +25,7 @@ class ReplyAction(BaseAction): - 使用 s4u (Speak for You) 模板 - 专注于理解和回应单条消息的具体内容 - 适合 Focus 模式下的精准回复 + - 仅限 AffinityFlowChatter 使用 """ # 动作基本信息 @@ -31,6 +36,9 @@ class ReplyAction(BaseAction): activation_type = ActionActivationType.ALWAYS # 回复动作总是可用 mode_enable = ChatMode.ALL # 在所有模式下都可用 parallel_action = False # 回复动作不能与其他动作并行 + + # Chatter 限制:仅允许 AffinityFlowChatter 使用 + chatter_allow: ClassVar[list[str]] = ["AffinityFlowChatter"] # 动作参数定义 action_parameters: ClassVar = { @@ -53,13 +61,116 @@ class ReplyAction(BaseAction): associated_types: ClassVar[list[str]] = ["text"] async def execute(self) -> tuple[bool, str]: - """执行reply动作 + """执行reply动作 - 完整的回复流程""" + try: + # 检查目标消息是否为表情包 + if self.action_message and getattr(self.action_message, "is_emoji", False): + if not getattr(global_config.chat, "allow_reply_to_emoji", True): + logger.info(f"{self.log_prefix} 目标消息为表情包且配置不允许回复,跳过") + return True, "" + + # 准备 action_data + action_data = self.action_data.copy() + action_data["prompt_mode"] = "s4u" + + # 生成回复 + success, response_set, _ = await generator_api.generate_reply( + chat_stream=self.chat_stream, + reply_message=self.action_message, + action_data=action_data, + available_actions={self.action_name: None}, + enable_tool=global_config.tool.enable_tool, + request_type="chat.replyer", + from_plugin=False, + ) + + if not success or not response_set: + logger.warning(f"{self.log_prefix} 回复生成失败") + return False, "" + + # 发送回复 + reply_text = await self._send_response(response_set) + + # 存储动作信息 + await self._store_action_info(reply_text) + + logger.info(f"{self.log_prefix} reply 动作执行成功") + return True, reply_text + + except asyncio.CancelledError: + logger.debug(f"{self.log_prefix} 回复任务被取消") + return False, "" + except Exception as e: + logger.error(f"{self.log_prefix} reply 动作执行失败: {e}") + import traceback + traceback.print_exc() + return False, "" + + async def _send_response(self, response_set) -> str: + """发送回复内容""" + reply_text = "" + should_quote = self.action_data.get("should_quote_reply", False) + first_sent = False - 注意:实际的回复生成由 action_manager 统一处理 - 这里只是标记使用 reply 动作(s4u 模板) - """ - logger.info(f"{self.log_prefix} 使用 reply 动作(s4u 模板)") - return True, "" + for reply_seg in response_set: + # 处理元组格式 + if isinstance(reply_seg, tuple) and len(reply_seg) >= 2: + _, data = reply_seg + else: + data = str(reply_seg) + + if isinstance(data, list): + data = "".join(map(str, data)) + + reply_text += data + + # 发送消息 + if not first_sent: + await send_api.text_to_stream( + text=data, + stream_id=self.chat_stream.stream_id, + reply_to_message=self.action_message, + set_reply=should_quote and bool(self.action_message), + typing=False, + ) + first_sent = True + else: + await send_api.text_to_stream( + text=data, + stream_id=self.chat_stream.stream_id, + reply_to_message=None, + set_reply=False, + typing=True, + ) + + return reply_text + + async def _store_action_info(self, reply_text: str): + """存储动作信息到数据库""" + from src.person_info.person_info import get_person_info_manager + + person_info_manager = get_person_info_manager() + + if self.action_message: + platform = self.action_message.chat_info.platform + user_id = self.action_message.user_info.user_id + else: + platform = getattr(self.chat_stream, "platform", "unknown") + user_id = "" + + person_id = person_info_manager.get_person_id(platform, user_id) + person_name = await person_info_manager.get_value(person_id, "person_name") + action_prompt_display = f"你对{person_name}进行了回复:{reply_text}" + + await database_api.store_action_info( + chat_stream=self.chat_stream, + action_build_into_prompt=False, + action_prompt_display=action_prompt_display, + action_done=True, + thinking_id=self.thinking_id, + action_data={"reply_text": reply_text}, + action_name="reply", + ) class RespondAction(BaseAction): @@ -69,6 +180,7 @@ class RespondAction(BaseAction): - 关注整体对话动态和未读消息的统一回应 - 适合对于群聊消息下的宏观回应 - 避免与单一用户深度对话而忽略其他用户的消息 + - 仅限 AffinityFlowChatter 使用 """ # 动作基本信息 @@ -79,6 +191,9 @@ class RespondAction(BaseAction): activation_type = ActionActivationType.ALWAYS # 回应动作总是可用 mode_enable = ChatMode.ALL # 在所有模式下都可用 parallel_action = False # 回应动作不能与其他动作并行 + + # Chatter 限制:仅允许 AffinityFlowChatter 使用 + chatter_allow: ClassVar[list[str]] = ["AffinityFlowChatter"] # 动作参数定义 action_parameters: ClassVar = { @@ -99,10 +214,89 @@ class RespondAction(BaseAction): associated_types: ClassVar[list[str]] = ["text"] async def execute(self) -> tuple[bool, str]: - """执行respond动作 + """执行respond动作 - 完整的回复流程""" + try: + # 准备 action_data + action_data = self.action_data.copy() + action_data["prompt_mode"] = "normal" + + # 生成回复 + success, response_set, _ = await generator_api.generate_reply( + chat_stream=self.chat_stream, + reply_message=self.action_message, + action_data=action_data, + available_actions={self.action_name: None}, + enable_tool=global_config.tool.enable_tool, + request_type="chat.replyer", + from_plugin=False, + ) + + if not success or not response_set: + logger.warning(f"{self.log_prefix} 回复生成失败") + return False, "" + + # 发送回复(respond 默认不引用) + reply_text = await self._send_response(response_set) + + # 存储动作信息 + await self._store_action_info(reply_text) + + logger.info(f"{self.log_prefix} respond 动作执行成功") + return True, reply_text + + except asyncio.CancelledError: + logger.debug(f"{self.log_prefix} 回复任务被取消") + return False, "" + except Exception as e: + logger.error(f"{self.log_prefix} respond 动作执行失败: {e}") + import traceback + traceback.print_exc() + return False, "" + + async def _send_response(self, response_set) -> str: + """发送回复内容(不引用原消息)""" + reply_text = "" + first_sent = False - 注意:实际的回复生成由 action_manager 统一处理 - 这里只是标记使用 respond 动作(normal 模板) - """ - logger.info(f"{self.log_prefix} 使用 respond 动作(normal 模板)") - return True, "" + for reply_seg in response_set: + if isinstance(reply_seg, tuple) and len(reply_seg) >= 2: + _, data = reply_seg + else: + data = str(reply_seg) + + if isinstance(data, list): + data = "".join(map(str, data)) + + reply_text += data + + if not first_sent: + await send_api.text_to_stream( + text=data, + stream_id=self.chat_stream.stream_id, + reply_to_message=None, + set_reply=False, + typing=False, + ) + first_sent = True + else: + await send_api.text_to_stream( + text=data, + stream_id=self.chat_stream.stream_id, + reply_to_message=None, + set_reply=False, + typing=True, + ) + + return reply_text + + async def _store_action_info(self, reply_text: str): + """存储动作信息到数据库""" + await database_api.store_action_info( + chat_stream=self.chat_stream, + action_build_into_prompt=False, + action_prompt_display=f"统一回应:{reply_text}", + action_done=True, + thinking_id=self.thinking_id, + action_data={"reply_text": reply_text}, + action_name="respond", + ) diff --git a/src/plugins/built_in/kokoro_flow_chatter/__init__.py b/src/plugins/built_in/kokoro_flow_chatter/__init__.py deleted file mode 100644 index 440ea0f49..000000000 --- a/src/plugins/built_in/kokoro_flow_chatter/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Kokoro Flow Chatter (心流聊天器) 插件 - -一个专为私聊场景设计的AI聊天插件,实现从"消息响应者"到"对话体验者"的转变。 -核心特点: -- 心理状态驱动的交互模型 -- 连续的时间观念和等待体验 -- 深度情感连接和长期关系维护 -""" - -from src.plugin_system.base.plugin_metadata import PluginMetadata - -from .plugin import KokoroFlowChatterPlugin - -__plugin_meta__ = PluginMetadata( - name="Kokoro Flow Chatter", - description="专为私聊设计的深度情感交互处理器,实现心理状态驱动的对话体验", - usage="在私聊场景中自动启用,可通过 [kokoro_flow_chatter].enable 配置开关", - version="3.0.0", - author="MoFox", - keywords=["chatter", "kokoro", "private", "emotional", "narrative"], - categories=["Chat", "AI", "Emotional"], - extra={"is_built_in": True, "chat_type": "private"}, -) - -__all__ = ["KokoroFlowChatterPlugin", "__plugin_meta__"] diff --git a/src/plugins/built_in/kokoro_flow_chatter/action_executor.py b/src/plugins/built_in/kokoro_flow_chatter/action_executor.py deleted file mode 100644 index 8ad88b87d..000000000 --- a/src/plugins/built_in/kokoro_flow_chatter/action_executor.py +++ /dev/null @@ -1,711 +0,0 @@ -""" -Kokoro Flow Chatter 动作执行器 (V2) - -融合AFC的动态动作发现机制,支持所有注册的Action组件。 -负责解析LLM返回的动作列表并通过ChatterActionManager执行。 - -V2升级要点: -1. 动态动作支持 - 使用ActionManager发现所有可用动作 -2. 统一执行接口 - 通过ChatterActionManager.execute_action()执行所有动作 -3. 保留KFC特有功能 - 内部状态更新、心理日志等 -4. 支持复合动作 - 如 sing_a_song + image_sender + tts_voice_action - -V5升级要点: -1. 动态情感更新 - 根据thought字段的情感倾向微调EmotionalState -""" - -import asyncio -import time -from typing import TYPE_CHECKING, Any, Optional - -import orjson - -from src.chat.planner_actions.action_manager import ChatterActionManager -from src.common.logger import get_logger -from src.plugin_system.base.component_types import ActionInfo -from src.utils.json_parser import extract_and_parse_json - -from .models import ( - ActionModel, - EmotionalState, - KokoroSession, - LLMResponseModel, - MentalLogEntry, - MentalLogEventType, -) - -if TYPE_CHECKING: - from src.chat.message_receive.chat_stream import ChatStream - -logger = get_logger("kokoro_action_executor") - - -class ActionExecutor: - """ - Kokoro Flow Chatter 动作执行器 (V2) - - 职责: - 1. 解析LLM返回的JSON响应 - 2. 动态验证动作格式和参数(基于ActionManager的动作注册) - 3. 通过ChatterActionManager执行各类动作 - 4. 处理KFC特有的内部状态更新 - 5. 记录执行结果到心理日志 - - V2特性: - - 支持所有通过插件系统注册的Action - - 自动从ActionManager获取可用动作列表 - - 支持复合动作组合执行 - - 区分"回复类动作"和"其他动作"的执行顺序 - """ - - # KFC内置的特殊动作(不通过ActionManager执行) - INTERNAL_ACTIONS = { - "update_internal_state": { - "required": [], - "optional": ["mood", "mood_intensity", "relationship_warmth", "impression_of_user", "anxiety_level", "engagement_level"] - }, - "do_nothing": {"required": [], "optional": []}, - } - - def __init__(self, stream_id: str): - """ - 初始化动作执行器 - - Args: - stream_id: 聊天流ID - """ - self.stream_id = stream_id - self._action_manager = ChatterActionManager() - self._available_actions: dict[str, ActionInfo] = {} - self._execution_stats = { - "total_executed": 0, - "successful": 0, - "failed": 0, - "by_type": {}, - } - - async def load_actions(self) -> dict[str, ActionInfo]: - """ - 加载当前可用的动作列表 - - Returns: - dict[str, ActionInfo]: 可用动作字典 - """ - await self._action_manager.load_actions(self.stream_id) - self._available_actions = self._action_manager.get_using_actions() - logger.debug(f"KFC ActionExecutor 加载了 {len(self._available_actions)} 个可用动作: {list(self._available_actions.keys())}") - return self._available_actions - - def get_available_actions(self) -> dict[str, ActionInfo]: - """获取当前可用的动作列表""" - return self._available_actions.copy() - - def is_action_available(self, action_type: str) -> bool: - """ - 检查动作是否可用 - - Args: - action_type: 动作类型名称 - - Returns: - bool: 动作是否可用 - """ - # 内置动作总是可用 - if action_type in self.INTERNAL_ACTIONS: - return True - # 检查动态注册的动作 - return action_type in self._available_actions - - def parse_llm_response(self, response_text: str) -> LLMResponseModel: - """ - 解析LLM的JSON响应 - - 使用统一的json_parser工具进行解析,自动处理: - - Markdown代码块标记 - - 格式错误的JSON修复(json_repair) - - 多种包装格式 - - Args: - response_text: LLM返回的原始文本 - - Returns: - LLMResponseModel: 解析后的响应模型 - """ - # 使用统一的json_parser工具解析 - data = extract_and_parse_json(response_text, strict=False) - - if not data or not isinstance(data, dict): - logger.warning(f"无法从LLM响应中提取有效JSON: {response_text[:200]}...") - return LLMResponseModel.create_error_response("无法解析响应格式") - - return self._validate_and_create_response(data) - - def _validate_and_create_response(self, data: dict[str, Any]) -> LLMResponseModel: - """ - 验证并创建响应模型(V2:支持动态动作验证) - - Args: - data: 解析后的字典数据 - - Returns: - LLMResponseModel: 验证后的响应模型 - """ - # 验证必需字段 - if "thought" not in data: - data["thought"] = "" - logger.warning("LLM响应缺少'thought'字段") - - if "expected_user_reaction" not in data: - data["expected_user_reaction"] = "" - logger.warning("LLM响应缺少'expected_user_reaction'字段") - - if "max_wait_seconds" not in data: - data["max_wait_seconds"] = 300 - logger.warning("LLM响应缺少'max_wait_seconds'字段,使用默认值300") - else: - # 确保在合理范围内:0-900秒 - # 0 表示不等待(话题结束/用户说再见等) - try: - wait_seconds = int(data["max_wait_seconds"]) - data["max_wait_seconds"] = max(0, min(wait_seconds, 900)) - except (ValueError, TypeError): - data["max_wait_seconds"] = 300 - - if "actions" not in data or not data["actions"]: - data["actions"] = [{"type": "do_nothing"}] - logger.warning("LLM响应缺少'actions'字段,添加默认的do_nothing动作") - - # 验证每个动作(V2:使用动态验证) - validated_actions = [] - for action_data in data["actions"]: - if not isinstance(action_data, dict): - logger.warning(f"无效的动作格式: {action_data}") - continue - - action_type = action_data.get("type", "") - - # 检查是否是已注册的动作 - if not self.is_action_available(action_type): - logger.warning(f"不支持的动作类型: {action_type},可用动作: {list(self._available_actions.keys()) + list(self.INTERNAL_ACTIONS.keys())}") - continue - - # 对于内置动作,验证参数 - if action_type in self.INTERNAL_ACTIONS: - required_params = self.INTERNAL_ACTIONS[action_type]["required"] - missing_params = [p for p in required_params if p not in action_data] - if missing_params: - logger.warning(f"动作 '{action_type}' 缺少必需参数: {missing_params}") - continue - - # 对于动态注册的动作,仅记录参数信息(不强制验证) - # 注意:action_require 是"使用场景描述",不是必需参数! - # 必需参数应该在 action_parameters 中定义 - elif action_type in self._available_actions: - action_info = self._available_actions[action_type] - # 仅记录调试信息,不阻止执行 - if action_info.action_parameters: - provided_params = set(action_data.keys()) - {"type", "reason"} - expected_params = set(action_info.action_parameters.keys()) - if expected_params and not provided_params.intersection(expected_params): - logger.debug(f"动作 '{action_type}' 期望参数: {list(expected_params)},实际提供: {list(provided_params)}") - - validated_actions.append(action_data) - - if not validated_actions: - validated_actions = [{"type": "do_nothing"}] - - data["actions"] = validated_actions - - return LLMResponseModel.from_dict(data) - - async def execute_actions( - self, - response: LLMResponseModel, - session: KokoroSession, - chat_stream: Optional["ChatStream"] = None, - ) -> dict[str, Any]: - """ - 执行动作列表(V2:通过ActionManager执行动态动作) - - 执行策略(参考AFC的plan_executor): - 1. 先执行所有"回复类"动作(reply, respond等) - 2. 再执行"其他"动作(send_reaction, sing_a_song等) - 3. 内部动作(update_internal_state, do_nothing)由KFC直接处理 - - Args: - response: LLM响应模型 - session: 当前会话 - chat_stream: 聊天流对象(用于发送消息) - - Returns: - dict: 执行结果 - """ - results = [] - has_reply = False - reply_content = "" - - # INFO日志:打印所有解析出的动作(可观测性增强) - for action in response.actions: - logger.info( - f"Parsed action for execution: type={action.type}, params={action.params}" - ) - - # 分类动作:回复类 vs 其他类 vs 内部类 - reply_actions = [] # reply, respond - other_actions = [] # 其他注册的动作 - internal_actions = [] # update_internal_state, do_nothing - - for action in response.actions: - action_type = action.type - if action_type in self.INTERNAL_ACTIONS: - internal_actions.append(action) - elif action_type in ("reply", "respond"): - reply_actions.append(action) - else: - other_actions.append(action) - - # 第1步:执行回复类动作 - for action in reply_actions: - try: - result = await self._execute_via_action_manager( - action, session, chat_stream - ) - results.append(result) - - if result.get("success"): - self._execution_stats["successful"] += 1 - has_reply = True - reply_content = action.params.get("content", "") or result.get("reply_text", "") - else: - self._execution_stats["failed"] += 1 - - except Exception as e: - logger.error(f"执行回复动作 '{action.type}' 失败: {e}") - results.append({ - "action_type": action.type, - "success": False, - "error": str(e), - }) - self._execution_stats["failed"] += 1 - - self._update_stats(action.type) - - # 第2步:并行执行其他动作(参考AFC的_execute_other_actions) - if other_actions: - other_tasks = [] - for action in other_actions: - task = asyncio.create_task( - self._execute_via_action_manager(action, session, chat_stream) - ) - other_tasks.append((action, task)) - - for action, task in other_tasks: - try: - result = await task - results.append(result) - if result.get("success"): - self._execution_stats["successful"] += 1 - else: - self._execution_stats["failed"] += 1 - except Exception as e: - logger.error(f"执行动作 '{action.type}' 失败: {e}") - results.append({ - "action_type": action.type, - "success": False, - "error": str(e), - }) - self._execution_stats["failed"] += 1 - - self._update_stats(action.type) - - # 第3步:执行内部动作 - for action in internal_actions: - try: - result = await self._execute_internal_action(action, session) - results.append(result) - self._execution_stats["successful"] += 1 - except Exception as e: - logger.error(f"执行内部动作 '{action.type}' 失败: {e}") - results.append({ - "action_type": action.type, - "success": False, - "error": str(e), - }) - self._execution_stats["failed"] += 1 - - self._update_stats(action.type) - - # 添加Bot行动日志 - if has_reply or other_actions: - entry = MentalLogEntry( - event_type=MentalLogEventType.BOT_ACTION, - timestamp=time.time(), - thought=response.thought, - content=reply_content or f"执行了 {len(other_actions)} 个动作", - emotional_snapshot=session.emotional_state.to_dict(), - metadata={ - "actions": [a.to_dict() for a in response.actions], - "results_summary": { - "total": len(results), - "successful": sum(1 for r in results if r.get("success")), - }, - }, - ) - session.add_mental_log_entry(entry) - if reply_content: - session.last_bot_message = reply_content - - # V5:动态情感更新 - 根据thought分析情感倾向并微调EmotionalState - await self._update_emotional_state_from_thought(response.thought, session) - - return { - "success": all(r.get("success", False) for r in results), - "results": results, - "has_reply": has_reply, - "reply_content": reply_content, - "thought": response.thought, - "expected_user_reaction": response.expected_user_reaction, - "max_wait_seconds": response.max_wait_seconds, - } - - def _update_stats(self, action_type: str) -> None: - """更新执行统计""" - self._execution_stats["total_executed"] += 1 - if action_type not in self._execution_stats["by_type"]: - self._execution_stats["by_type"][action_type] = 0 - self._execution_stats["by_type"][action_type] += 1 - - async def _execute_via_action_manager( - self, - action: ActionModel, - session: KokoroSession, - chat_stream: Optional["ChatStream"], - ) -> dict[str, Any]: - """ - 通过ActionManager执行动作 - - Args: - action: 动作模型 - session: 当前会话 - chat_stream: 聊天流对象 - - Returns: - dict: 执行结果 - """ - action_type = action.type - params = action.params - - logger.debug(f"通过ActionManager执行动作: {action_type}, 参数: {params}") - - if not chat_stream: - return { - "action_type": action_type, - "success": False, - "error": "无法获取聊天流", - } - - try: - # 准备动作数据 - action_data = params.copy() - - # 对于reply动作,需要处理content字段 - if action_type in ("reply", "respond") and "content" in action_data: - # ActionManager的reply期望的是生成回复而不是直接内容 - # 但KFC已经决定了内容,所以我们直接发送 - return await self._execute_reply_directly(action_data, chat_stream) - - # 使用ActionManager执行其他动作 - result = await self._action_manager.execute_action( - action_name=action_type, - chat_id=self.stream_id, - target_message=None, # KFC模式不需要target_message - reasoning=f"KFC决策: {action_type}", - action_data=action_data, - thinking_id=None, - log_prefix="[KFC]", - ) - - return { - "action_type": action_type, - "success": result.get("success", False), - "reply_text": result.get("reply_text", ""), - "result": result, - } - - except Exception as e: - logger.error(f"ActionManager执行失败: {action_type}, 错误: {e}") - import traceback - logger.error(traceback.format_exc()) - return { - "action_type": action_type, - "success": False, - "error": str(e), - } - - async def _execute_reply_directly( - self, - params: dict[str, Any], - chat_stream: "ChatStream", - ) -> dict[str, Any]: - """ - 直接执行回复动作(KFC决定的内容直接发送) - - V4升级:集成全局后处理流程(错别字、消息分割) - - Args: - params: 动作参数,包含content - chat_stream: 聊天流对象 - - Returns: - dict: 执行结果 - """ - from src.plugin_system.apis import send_api - from .response_post_processor import process_reply_content - - content = params.get("content", "") - reply_to = params.get("reply_to") - should_quote = params.get("should_quote_reply", False) - - if not content: - return { - "action_type": "reply", - "success": False, - "error": "回复内容为空", - } - - try: - # 【关键步骤】调用全局后处理器(错别字生成、消息分割) - processed_messages = await process_reply_content(content) - logger.info(f"[KFC] 后处理完成,原始内容长度={len(content)},分割为 {len(processed_messages)} 条消息") - - all_success = True - first_message = True - - for msg in processed_messages: - success = await send_api.text_to_stream( - text=msg, - stream_id=self.stream_id, - reply_to_message=reply_to if first_message else None, - set_reply=should_quote if first_message else False, - typing=True, - ) - if not success: - all_success = False - first_message = False - - return { - "action_type": "reply", - "success": all_success, - "reply_text": content, - "processed_messages": processed_messages, - } - - except Exception as e: - logger.error(f"直接发送回复失败: {e}") - import traceback - logger.error(traceback.format_exc()) - return { - "action_type": "reply", - "success": False, - "error": str(e), - } - - async def _execute_internal_action( - self, - action: ActionModel, - session: KokoroSession, - ) -> dict[str, Any]: - """ - 执行KFC内部动作 - - Args: - action: 动作模型 - session: 当前会话 - - Returns: - dict: 执行结果 - """ - action_type = action.type - params = action.params - - if action_type == "update_internal_state": - return await self._execute_update_state(params, session) - - elif action_type == "do_nothing": - return await self._execute_do_nothing() - - else: - return { - "action_type": action_type, - "success": False, - "error": f"未知的内部动作类型: {action_type}", - } - - async def _execute_update_state( - self, - params: dict[str, Any], - session: KokoroSession, - ) -> dict[str, Any]: - """ - 执行内部状态更新动作 - - V7重构:情绪变化必须合理 - - 禁止 LLM 直接设置负面情绪(低落、沮丧、难过等) - - 情绪变化必须渐进,不能突然跳变 - - 情绪强度变化限制在 ±0.3 以内 - """ - updated_fields = [] - emotional_state = session.emotional_state - blocked_fields = [] - - if "mood" in params: - new_mood = str(params["mood"]) - # V7: 检查是否是负面情绪 - negative_moods = [ - "低落", "沮丧", "难过", "伤心", "失落", "郁闷", "烦躁", "焦虑", - "担忧", "害怕", "恐惧", "愤怒", "生气", "不安", "忧郁", "悲伤", - "sad", "depressed", "anxious", "angry", "upset", "worried" - ] - is_negative = any(neg in new_mood.lower() for neg in negative_moods) - - if is_negative: - # 负面情绪需要检查是否有合理理由(通过检查上下文) - # 如果当前情绪是平静/正面的,不允许突然变成负面 - current_mood = emotional_state.mood.lower() - positive_indicators = ["平静", "开心", "愉快", "高兴", "满足", "期待", "好奇", "neutral"] - - if any(pos in current_mood for pos in positive_indicators): - # 从正面情绪直接跳到负面情绪,阻止这种变化 - logger.warning( - f"[KFC] 阻止无厘头负面情绪变化: {emotional_state.mood} -> {new_mood}," - f"情绪变化必须有聊天上下文支撑" - ) - blocked_fields.append("mood") - else: - # 已经是非正面情绪,允许变化但记录警告 - emotional_state.mood = new_mood - updated_fields.append("mood") - logger.info(f"[KFC] 情绪变化: {emotional_state.mood} -> {new_mood}") - else: - # 非负面情绪,允许更新 - emotional_state.mood = new_mood - updated_fields.append("mood") - - if "mood_intensity" in params: - try: - new_intensity = float(params["mood_intensity"]) - new_intensity = max(0.0, min(1.0, new_intensity)) - old_intensity = emotional_state.mood_intensity - - # V7: 限制情绪强度变化幅度(最多 ±0.3) - max_change = 0.3 - if abs(new_intensity - old_intensity) > max_change: - # 限制变化幅度 - if new_intensity > old_intensity: - new_intensity = min(old_intensity + max_change, 1.0) - else: - new_intensity = max(old_intensity - max_change, 0.0) - logger.info( - f"[KFC] 限制情绪强度变化: {old_intensity:.2f} -> {new_intensity:.2f} " - f"(原请求: {params['mood_intensity']})" - ) - - emotional_state.mood_intensity = new_intensity - updated_fields.append("mood_intensity") - except (ValueError, TypeError): - pass - - # relationship_warmth 不再由 LLM 更新,应该从全局关系系统读取 - if "relationship_warmth" in params: - logger.debug("[KFC] 忽略 relationship_warmth 更新,应从全局关系系统读取") - blocked_fields.append("relationship_warmth") - - if "impression_of_user" in params: - emotional_state.impression_of_user = str(params["impression_of_user"]) - updated_fields.append("impression_of_user") - - if "anxiety_level" in params: - try: - anxiety = float(params["anxiety_level"]) - emotional_state.anxiety_level = max(0.0, min(1.0, anxiety)) - updated_fields.append("anxiety_level") - except (ValueError, TypeError): - pass - - if "engagement_level" in params: - try: - engagement = float(params["engagement_level"]) - emotional_state.engagement_level = max(0.0, min(1.0, engagement)) - updated_fields.append("engagement_level") - except (ValueError, TypeError): - pass - - emotional_state.last_update_time = time.time() - - if blocked_fields: - logger.debug(f"更新情感状态: 更新={updated_fields}, 阻止={blocked_fields}") - else: - logger.debug(f"更新情感状态: {updated_fields}") - - return { - "action_type": "update_internal_state", - "success": True, - "updated_fields": updated_fields, - "blocked_fields": blocked_fields, - } - - async def _execute_do_nothing(self) -> dict[str, Any]: - """执行"什么都不做"动作""" - logger.debug("执行 do_nothing 动作") - return { - "action_type": "do_nothing", - "success": True, - } - - def get_execution_stats(self) -> dict[str, Any]: - """获取执行统计信息""" - return self._execution_stats.copy() - - def reset_stats(self) -> None: - """重置统计信息""" - self._execution_stats = { - "total_executed": 0, - "successful": 0, - "failed": 0, - "by_type": {}, - } - - async def _update_emotional_state_from_thought( - self, - thought: str, - session: KokoroSession, - ) -> None: - """ - 根据thought字段更新EmotionalState - - V6重构: - - 移除基于关键词的情感分析(诡异且不准确) - - 情感状态现在主要通过LLM输出的update_internal_state动作更新 - - 关系温度应该从person_info/relationship_manager的好感度系统读取 - - 此方法仅做简单的engagement_level更新 - - Args: - thought: LLM返回的内心独白 - session: 当前会话 - """ - if not thought: - return - - emotional_state = session.emotional_state - - # 简单的engagement_level更新:有内容的thought表示高投入 - if len(thought) > 50: - old_engagement = emotional_state.engagement_level - new_engagement = old_engagement + 0.025 # 微调 - emotional_state.engagement_level = max(0.0, min(1.0, new_engagement)) - - emotional_state.last_update_time = time.time() - - # 注意:关系温度(relationship_warmth)应该从全局的好感度系统读取 - # 参考 src/person_info/relationship_manager.py 和 src/plugin_system/apis/person_api.py - # 当前实现中,这个值主要通过 LLM 的 update_internal_state 动作来更新 diff --git a/src/plugins/built_in/kokoro_flow_chatter/chatter.py b/src/plugins/built_in/kokoro_flow_chatter/chatter.py deleted file mode 100644 index d6dd7f139..000000000 --- a/src/plugins/built_in/kokoro_flow_chatter/chatter.py +++ /dev/null @@ -1,877 +0,0 @@ -""" -Kokoro Flow Chatter (心流聊天器) 主类 - -核心聊天处理器,协调所有组件完成"体验-决策-行动"的交互循环。 -实现从"消息响应者"到"对话体验者"的核心转变。 -""" - -import asyncio -import time -import traceback -from typing import TYPE_CHECKING, Any, ClassVar, Optional - -from src.chat.planner_actions.action_manager import ChatterActionManager -from src.chat.planner_actions.action_modifier import ActionModifier # V6: 动作筛选器 -from src.common.data_models.message_manager_data_model import StreamContext -from src.common.logger import get_logger -from src.config.config import global_config, model_config -from src.llm_models.utils_model import LLMRequest -from src.plugin_system.base.base_chatter import BaseChatter -from src.plugin_system.base.component_types import ChatType - -from .action_executor import ActionExecutor -from .context_builder import KFCContextBuilder -from .models import ( - KokoroSession, - LLMResponseModel, - MentalLogEntry, - MentalLogEventType, - SessionStatus, -) -from .prompt_generator import PromptGenerator, get_prompt_generator -from .kfc_scheduler_adapter import KFCSchedulerAdapter, get_scheduler -from .session_manager import SessionManager, get_session_manager - -if TYPE_CHECKING: - from src.common.data_models.database_data_model import DatabaseMessages - -logger = get_logger("kokoro_flow_chatter") - -# 控制台颜色 -SOFT_PURPLE = "\033[38;5;183m" -RESET_COLOR = "\033[0m" - - -class KokoroFlowChatter(BaseChatter): - """ - 心流聊天器 (Kokoro Flow Chatter) - - 专为私聊场景设计的AI聊天处理器,核心特点: - - 心理状态驱动的交互模型 - - 连续的时间观念和等待体验 - - 深度情感连接和长期关系维护 - - 状态机: - IDLE -> RESPONDING -> WAITING -> (收到消息) -> RESPONDING - -> (超时) -> FOLLOW_UP_PENDING -> RESPONDING/IDLE - """ - - chatter_name: str = "KokoroFlowChatter" - chatter_description: str = "心流聊天器 - 专为私聊设计的深度情感交互处理器" - chat_types: ClassVar[list[ChatType]] = [ChatType.PRIVATE] # 仅支持私聊 - - def __init__( - self, - stream_id: str, - action_manager: ChatterActionManager, - plugin_config: dict | None = None, - ): - """ - 初始化心流聊天器 - - Args: - stream_id: 聊天流ID - action_manager: 动作管理器 - plugin_config: 插件配置 - """ - super().__init__(stream_id, action_manager, plugin_config) - - # 核心组件 - self.session_manager: SessionManager = get_session_manager() - self.prompt_generator: PromptGenerator = get_prompt_generator() - self.scheduler: KFCSchedulerAdapter = get_scheduler() - self.action_executor: ActionExecutor = ActionExecutor(stream_id) - - # 配置 - self._load_config() - - # 并发控制 - self._lock = asyncio.Lock() - - # V7: 打断机制(类似S4U的已读/未读,这里是已处理/未处理) - self._current_task: Optional[asyncio.Task] = None # 当前正在执行的任务 - self._interrupt_requested: bool = False # 是否请求打断 - self._interrupt_wait_seconds: float = 3.0 # 被打断后等待新消息的时间 - self._last_interrupt_time: float = 0.0 # 上次被打断的时间 - self._pending_message_ids: set[str] = set() # 未处理的消息ID集合(被打断时保留) - self._current_processing_message_id: Optional[str] = None # 当前正在处理的消息ID - - # 统计信息 - self.stats = { - "messages_processed": 0, - "llm_calls": 0, - "successful_responses": 0, - "failed_responses": 0, - "timeout_decisions": 0, - "interrupts": 0, # V7: 打断次数统计 - } - self.last_activity_time = time.time() - - # 设置调度器回调 - self._setup_scheduler_callbacks() - - logger.info(f"{SOFT_PURPLE}[KFC]{RESET_COLOR} 初始化完成: stream_id={stream_id}") - - def _load_config(self) -> None: - """ - 加载配置(从 global_config.kokoro_flow_chatter 读取) - - 设计理念:KFC不是独立人格,它复用全局的人设、情感框架和回复模型, - 只保留最少的行为控制开关。 - """ - # 获取 KFC 配置 - if global_config and hasattr(global_config, 'kokoro_flow_chatter'): - kfc_config = global_config.kokoro_flow_chatter - - # 核心行为配置 - self.max_wait_seconds_default: int = kfc_config.max_wait_seconds_default - self.enable_continuous_thinking: bool = kfc_config.enable_continuous_thinking - - # 主动思考子配置(V3: 人性化驱动,无机械限制) - proactive_cfg = kfc_config.proactive_thinking - self.enable_proactive: bool = proactive_cfg.enabled - self.silence_threshold_seconds: int = proactive_cfg.silence_threshold_seconds - self.min_interval_between_proactive: int = proactive_cfg.min_interval_between_proactive - - logger.debug("[KFC] 已从 global_config.kokoro_flow_chatter 加载配置") - else: - # 回退到默认值 - self.max_wait_seconds_default = 300 - self.enable_continuous_thinking = True - self.enable_proactive = True - self.silence_threshold_seconds = 7200 - self.min_interval_between_proactive = 1800 - - logger.debug("[KFC] 使用默认配置") - - def _setup_scheduler_callbacks(self) -> None: - """设置调度器回调""" - self.scheduler.set_timeout_callback(self._on_session_timeout) - - if self.enable_continuous_thinking: - 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: - """ - 执行聊天处理逻辑(BaseChatter接口实现) - - V7升级:实现打断机制(类似S4U的已读/未读机制) - - 如果当前有任务在执行,新消息会请求打断 - - 被打断时,当前处理的消息会被标记为"未处理"(pending) - - 下次处理时,会合并所有pending消息 + 新消息一起处理 - - 这样被打断的消息不会丢失,上下文关联性得以保持 - - Args: - context: StreamContext对象,包含聊天上下文信息 - - Returns: - 处理结果字典 - """ - # V7: 检查是否需要打断当前任务 - if self._current_task and not self._current_task.done(): - logger.info(f"[KFC] 收到新消息,请求打断当前任务: {self.stream_id}") - self._interrupt_requested = True - self.stats["interrupts"] += 1 - - # 返回一个特殊结果表示请求打断 - # 注意:当前正在处理的消息会在被打断时自动加入 pending 列表 - return self._build_result( - success=True, - message="interrupt_requested", - interrupted=True - ) - - # V7: 检查是否需要等待(刚被打断过,等待用户可能的连续输入) - time_since_interrupt = time.time() - self._last_interrupt_time - if time_since_interrupt < self._interrupt_wait_seconds and self._last_interrupt_time > 0: - wait_remaining = self._interrupt_wait_seconds - time_since_interrupt - logger.info(f"[KFC] 刚被打断,等待 {wait_remaining:.1f}s 收集更多消息: {self.stream_id}") - await asyncio.sleep(wait_remaining) - - async with self._lock: - try: - self.last_activity_time = time.time() - self._interrupt_requested = False - - # 创建任务以便可以被打断 - self._current_task = asyncio.current_task() - - # V7: 获取所有未读消息 - # 注意:被打断的消息不会被标记为已读,所以仍然在 unread 列表中 - unread_messages = context.get_unread_messages() - - if not unread_messages: - logger.debug(f"[KFC] 没有未读消息: {self.stream_id}") - return self._build_result(success=True, message="no_unread_messages") - - # V7: 记录是否有 pending 消息(被打断时遗留的) - pending_count = len(self._pending_message_ids) - if pending_count > 0: - # 日志:显示有多少消息是被打断后重新处理的 - new_count = sum(1 for msg in unread_messages - if str(msg.message_id) not in self._pending_message_ids) - logger.info( - f"[KFC] 打断恢复: 正在处理 {len(unread_messages)} 条消息 " - f"({pending_count} 条pending + {new_count} 条新消息): {self.stream_id}" - ) - - # 以最后一条消息为主消息(用于动作筛选和主要响应) - target_message = unread_messages[-1] - - # 记录当前正在处理的消息ID(用于被打断时标记为pending) - self._current_processing_message_id = str(target_message.message_id) - - message_content = self._extract_message_content(target_message) - - # V2: 加载可用动作(动态动作发现) - await self.action_executor.load_actions() - raw_action_count = len(self.action_executor.get_available_actions()) - logger.debug(f"[KFC] 原始加载 {raw_action_count} 个动作") - - # V7: 在动作筛选前检查是否被打断 - if self._interrupt_requested: - logger.info(f"[KFC] 动作筛选前被打断: {self.stream_id}") - # 将当前处理的消息加入pending列表,下次一起处理 - if self._current_processing_message_id: - self._pending_message_ids.add(self._current_processing_message_id) - logger.info(f"[KFC] 消息 {self._current_processing_message_id} 加入pending列表") - self._last_interrupt_time = time.time() - self._current_processing_message_id = None - return self._build_result(success=True, message="interrupted") - - # V6: 使用ActionModifier筛选动作(复用AFC的三阶段筛选逻辑) - # 阶段0: 聊天类型过滤(私聊/群聊) - # 阶段2: 关联类型匹配(适配器能力检查) - # 阶段3: 激活判定(go_activate + LLM判断) - action_modifier = ActionModifier( - action_manager=self.action_executor._action_manager, - chat_id=self.stream_id, - ) - await action_modifier.modify_actions(message_content=message_content) - - # 获取筛选后的动作 - available_actions = self.action_executor._action_manager.get_using_actions() - logger.info( - f"[KFC] 动作筛选: {raw_action_count} -> {len(available_actions)} " - f"(筛除 {raw_action_count - len(available_actions)} 个)" - ) - - # 执行核心处理流程(传递筛选后的动作,V7: 传递所有未读消息) - result = await self._handle_message( - target_message, - context, - available_actions, - all_unread_messages=unread_messages, # V7: 传递所有未读消息 - ) - - # 更新统计 - self.stats["messages_processed"] += 1 - - return result - - except asyncio.CancelledError: - logger.info(f"[KFC] 处理被取消: {self.stream_id}") - self.stats["failed_responses"] += 1 - raise - except Exception as e: - logger.error(f"[KFC] 处理出错: {e}\n{traceback.format_exc()}") - self.stats["failed_responses"] += 1 - return self._build_result( - success=False, - message=str(e), - error=True - ) - finally: - self._current_task = None - - async def _handle_message( - self, - message: "DatabaseMessages", - context: StreamContext, - available_actions: dict | None = None, - all_unread_messages: list | None = None, # V7: 所有未读消息(包含pending的) - ) -> dict: - """ - 处理单条消息的核心逻辑 - - 实现"体验 -> 决策 -> 行动"的交互模式 - V5超融合:集成S4U所有上下文模块 - V7升级:支持处理多条消息(打断机制合并pending消息) - - Args: - message: 要处理的主消息(最新的那条) - context: 聊天上下文 - available_actions: 可用动作字典(V2新增) - all_unread_messages: 所有未读消息列表(V7新增,包含pending消息) - - Returns: - 处理结果字典 - """ - # 1. 获取或创建会话 - user_id = str(message.user_info.user_id) - session = await self.session_manager.get_session(user_id, self.stream_id) - - # 2. 记录收到消息的事件 - await self._record_user_message(session, message) - - # 3. 更新会话状态为RESPONDING - old_status = session.status - session.status = SessionStatus.RESPONDING - - # 4. 如果之前在等待,结束等待状态 - if old_status == SessionStatus.WAITING: - session.end_waiting() - # V7: 用户回复了,重置连续追问计数 - session.consecutive_followup_count = 0 - logger.debug(f"[KFC] 收到消息,结束等待,重置追问计数: user={user_id}") - - # 5. V5超融合:构建S4U上下文数据 - chat_stream = await self._get_chat_stream() - context_data = {} - - if chat_stream: - try: - context_builder = KFCContextBuilder(chat_stream) - sender_name = message.user_info.user_nickname or user_id - target_message = self._extract_message_content(message) - - context_data = await context_builder.build_all_context( - sender_name=sender_name, - target_message=target_message, - context=context, - ) - logger.info(f"[KFC] 超融合上下文构建完成: {list(context_data.keys())}") - except Exception as e: - logger.warning(f"[KFC] 构建S4U上下文失败,使用基础模式: {e}") - - # 6. 生成提示词(V3: 从共享数据源读取历史, V5: 传递S4U上下文, V7: 支持多条消息) - system_prompt, user_prompt = self.prompt_generator.generate_responding_prompt( - session=session, - message_content=self._extract_message_content(message), - sender_name=message.user_info.user_nickname or user_id, - sender_id=user_id, - message_time=message.time, - available_actions=available_actions, - context=context, # V3: 传递StreamContext以读取共享历史 - context_data=context_data, # V5: S4U上下文数据 - chat_stream=chat_stream, # V5: 聊天流用于场景判断 - all_unread_messages=all_unread_messages, # V7: 传递所有未读消息 - ) - - # 7. 调用LLM - llm_response = await self._call_llm(system_prompt, user_prompt) - self.stats["llm_calls"] += 1 - - # V7: LLM调用后检查是否被打断 - if self._interrupt_requested: - logger.info(f"[KFC] LLM调用后被打断: {self.stream_id}") - # 将当前处理的消息加入pending列表 - if self._current_processing_message_id: - self._pending_message_ids.add(self._current_processing_message_id) - logger.info(f"[KFC] 消息 {self._current_processing_message_id} 加入pending列表") - self._last_interrupt_time = time.time() - self._current_processing_message_id = None - return self._build_result(success=True, message="interrupted_after_llm") - - # 8. 解析响应 - parsed_response = self.action_executor.parse_llm_response(llm_response) - - # 9. 执行动作 - execution_result = await self.action_executor.execute_actions( - parsed_response, - session, - chat_stream - ) - - # 10. 处理执行结果 - if execution_result["has_reply"]: - # 如果发送了回复,检查是否需要进入等待状态 - max_wait = parsed_response.max_wait_seconds - - if max_wait > 0: - # 正常等待状态 - session.start_waiting( - expected_reaction=parsed_response.expected_user_reaction, - max_wait=max_wait - ) - logger.debug( - f"[KFC] 进入等待状态: user={user_id}, " - f"max_wait={max_wait}s" - ) - else: - # max_wait=0 表示不等待(话题结束/用户说再见等) - session.status = SessionStatus.IDLE - session.end_waiting() - logger.info( - f"[KFC] 话题结束,不等待用户回复: user={user_id} " - f"(max_wait_seconds=0)" - ) - - session.total_interactions += 1 - self.stats["successful_responses"] += 1 - else: - # 没有发送回复,返回空闲状态 - session.status = SessionStatus.IDLE - logger.debug(f"[KFC] 无回复动作,返回空闲: user={user_id}") - - # 11. 保存会话 - await self.session_manager.save_session(user_id) - - # 12. V7: 标记当前消息为已读 - context.mark_message_as_read(str(message.message_id)) - - # 13. V7: 清除pending状态(所有消息都已成功处理) - processed_count = len(self._pending_message_ids) - if self._pending_message_ids: - # 标记所有pending消息为已读 - for msg_id in self._pending_message_ids: - context.mark_message_as_read(msg_id) - logger.info(f"[KFC] 清除 {processed_count} 条pending消息: {self.stream_id}") - self._pending_message_ids.clear() - - # 清除当前处理的消息ID - self._current_processing_message_id = None - - return self._build_result( - success=True, - message="processed", - has_reply=execution_result["has_reply"], - thought=parsed_response.thought, - pending_messages_processed=processed_count, # V7: 返回处理了多少条pending消息 - ) - - async def _record_user_message( - self, - session: KokoroSession, - message: "DatabaseMessages", - ) -> None: - """记录用户消息到会话历史""" - content = self._extract_message_content(message) - session.last_user_message = content - - entry = MentalLogEntry( - event_type=MentalLogEventType.USER_MESSAGE, - timestamp=message.time or time.time(), - thought="", # 用户消息不需要内心独白 - content=content, - metadata={ - "message_id": str(message.message_id), - "user_id": str(message.user_info.user_id), - "user_name": message.user_info.user_nickname, - }, - ) - session.add_mental_log_entry(entry) - - def _extract_message_content(self, message: "DatabaseMessages") -> str: - """提取消息内容""" - return ( - message.processed_plain_text - or message.display_message - or "" - ) - - async def _call_llm( - self, - system_prompt: str, - user_prompt: str, - ) -> str: - """ - 调用LLM生成响应 - - Args: - system_prompt: 系统提示词 - user_prompt: 用户提示词 - - Returns: - LLM的响应文本 - """ - try: - # 获取模型配置 - # 使用 replyer 任务的模型配置(KFC 生成回复,必须使用回复专用模型) - if model_config is None: - raise RuntimeError("model_config 未初始化") - task_config = model_config.model_task_config.replyer - - llm_request = LLMRequest( - model_set=task_config, - request_type="kokoro_flow_chatter", - ) - - # 构建完整的提示词(将系统提示词和用户提示词合并) - full_prompt = f"{system_prompt}\n\n{user_prompt}" - - # INFO日志:打印完整的KFC提示词(可观测性增强) - logger.info( - f"Final KFC prompt constructed for stream {self.stream_id}:\n" - f"--- PROMPT START ---\n" - f"[SYSTEM]\n{system_prompt}\n\n[USER]\n{user_prompt}\n" - f"--- PROMPT END ---" - ) - - # 生成响应 - response, _ = await llm_request.generate_response_async( - prompt=full_prompt, - ) - - # INFO日志:打印原始JSON响应(可观测性增强) - logger.info( - f"Raw JSON response from LLM for stream {self.stream_id}:\n" - f"--- JSON START ---\n" - f"{response}\n" - f"--- JSON END ---" - ) - - logger.info(f"[KFC] LLM响应长度: {len(response)}") - return response - - except Exception as e: - logger.error(f"[KFC] 调用LLM失败: {e}") - # 返回一个默认的JSON响应 - return '{"thought": "出现了技术问题", "expected_user_reaction": "", "max_wait_seconds": 60, "actions": [{"type": "do_nothing"}]}' - - async def _get_chat_stream(self, stream_id: Optional[str] = None): - """ - 获取聊天流对象 - - Args: - stream_id: 可选的stream_id,若不提供则使用self.stream_id - 在超时回调中应使用session.stream_id以避免发送到错误的用户 - """ - target_stream_id = stream_id or self.stream_id - try: - from src.chat.message_receive.chat_stream import get_chat_manager - - chat_manager = get_chat_manager() - if chat_manager: - return await chat_manager.get_stream(target_stream_id) - except Exception as e: - logger.warning(f"[KFC] 获取chat_stream失败 (stream_id={target_stream_id}): {e}") - return None - - async def _on_session_timeout(self, session: KokoroSession) -> None: - """ - 会话超时回调(V7:增加连续追问限制) - - 当等待超时时,触发后续决策流程 - - 注意:此回调由全局调度器触发,可能会在任意Chatter实例上执行。 - 因此必须使用session.stream_id而非self.stream_id来确保消息发送给正确的用户。 - - Args: - session: 超时的会话 - """ - logger.info(f"[KFC] 处理超时决策: user={session.user_id}, stream_id={session.stream_id}, followup_count={session.consecutive_followup_count}") - self.stats["timeout_decisions"] += 1 - - try: - # V7: 检查是否超过最大连续追问次数 - if session.consecutive_followup_count >= session.max_consecutive_followups: - logger.info( - f"[KFC] 已达到最大连续追问次数 ({session.max_consecutive_followups})," - f"自动返回IDLE状态: user={session.user_id}" - ) - session.status = SessionStatus.IDLE - session.end_waiting() - # 重置连续追问计数(下次用户回复后会重新开始) - session.consecutive_followup_count = 0 - await self.session_manager.save_session(session.user_id) - return - - # 关键修复:使用 session 的 stream_id 创建正确的 ActionExecutor - # 因为全局调度器的回调可能在任意 Chatter 实例上执行 - from .action_executor import ActionExecutor - timeout_action_executor = ActionExecutor(session.stream_id) - - # V2: 加载可用动作 - available_actions = await timeout_action_executor.load_actions() - - # 生成超时决策提示词(V2: 传递可用动作,V7: 传递连续追问信息) - system_prompt, user_prompt = self.prompt_generator.generate_timeout_decision_prompt( - session, - available_actions=available_actions, - ) - - # 调用LLM - llm_response = await self._call_llm(system_prompt, user_prompt) - self.stats["llm_calls"] += 1 - - # 解析响应 - parsed_response = timeout_action_executor.parse_llm_response(llm_response) - - # 关键修复:使用 session.stream_id 获取正确的 chat_stream - chat_stream = await self._get_chat_stream(session.stream_id) - execution_result = await timeout_action_executor.execute_actions( - parsed_response, - session, - chat_stream - ) - - # 更新会话状态 - if execution_result["has_reply"]: - # V7: 发送了后续消息,增加连续追问计数 - session.consecutive_followup_count += 1 - logger.info(f"[KFC] 发送追问消息,当前连续追问次数: {session.consecutive_followup_count}") - - # 如果发送了后续消息,重新进入等待 - session.start_waiting( - expected_reaction=parsed_response.expected_user_reaction, - max_wait=parsed_response.max_wait_seconds - ) - else: - # V7重构:do_nothing 的两种情况 - # 1. max_wait_seconds > 0: "看了一眼手机,决定再等等" → 继续等待,不算追问 - # 2. max_wait_seconds = 0: "算了,不等了" → 进入 IDLE - if parsed_response.max_wait_seconds > 0: - # 继续等待,不增加追问计数 - logger.info( - f"[KFC] 决定继续等待 {parsed_response.max_wait_seconds}s," - f"不算追问: user={session.user_id}" - ) - session.start_waiting( - expected_reaction=parsed_response.expected_user_reaction or session.expected_user_reaction, - max_wait=parsed_response.max_wait_seconds - ) - else: - # 不再等待,进入 IDLE - logger.info(f"[KFC] 决定不再等待,返回IDLE: user={session.user_id}") - session.status = SessionStatus.IDLE - session.end_waiting() - - # 保存会话 - await self.session_manager.save_session(session.user_id) - - except Exception as e: - logger.error(f"[KFC] 超时决策处理失败: {e}") - # 发生错误时返回空闲状态 - session.status = SessionStatus.IDLE - session.end_waiting() - await self.session_manager.save_session(session.user_id) - - async def _on_continuous_thinking(self, session: KokoroSession) -> None: - """ - 连续思考回调(V2升级版) - - 在等待期间更新心理状态,可选择调用LLM生成更自然的想法 - V2: 支持通过配置启用LLM驱动的连续思考 - - Args: - session: 会话 - """ - logger.debug(f"[KFC] 连续思考触发: user={session.user_id}") - - # 检查是否启用LLM驱动的连续思考 - use_llm_thinking = self.get_config( - "behavior.use_llm_continuous_thinking", - default=False - ) - - if use_llm_thinking and isinstance(use_llm_thinking, bool) and use_llm_thinking: - try: - # V2: 加载可用动作 - available_actions = await self.action_executor.load_actions() - - # 生成连续思考提示词 - system_prompt, user_prompt = self.prompt_generator.generate_continuous_thinking_prompt( - session, - available_actions=available_actions, - ) - - # 调用LLM - llm_response = await self._call_llm(system_prompt, user_prompt) - self.stats["llm_calls"] += 1 - - # 解析并执行(可能会更新内部状态) - parsed_response = self.action_executor.parse_llm_response(llm_response) - - # 只执行内部动作,不执行外部动作 - for action in parsed_response.actions: - if action.type == "update_internal_state": - await self.action_executor._execute_internal_action(action, session) - - # 记录思考内容 - entry = MentalLogEntry( - event_type=MentalLogEventType.CONTINUOUS_THINKING, - timestamp=time.time(), - thought=parsed_response.thought, - content="", - emotional_snapshot=session.emotional_state.to_dict(), - ) - session.add_mental_log_entry(entry) - - # 保存会话 - await self.session_manager.save_session(session.user_id) - - except Exception as e: - logger.warning(f"[KFC] LLM连续思考失败: {e}") - - # 简单模式:更新焦虑程度(已在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, - message: str = "", - error: bool = False, - **kwargs, - ) -> dict: - """构建返回结果""" - result = { - "success": success, - "stream_id": self.stream_id, - "message": message, - "error": error, - "timestamp": time.time(), - } - result.update(kwargs) - return result - - def get_stats(self) -> dict[str, Any]: - """获取统计信息""" - return { - **self.stats, - "last_activity_time": self.last_activity_time, - "action_executor_stats": self.action_executor.get_execution_stats(), - } - - def reset_stats(self) -> None: - """重置统计信息""" - self.stats = { - "messages_processed": 0, - "llm_calls": 0, - "successful_responses": 0, - "failed_responses": 0, - "timeout_decisions": 0, - "interrupts": 0, # V7: 打断次数统计 - } - self.action_executor.reset_stats() - - async def get_session_info(self) -> Optional[dict]: - """获取当前会话信息(用于调试)""" - try: - # 尝试获取当前用户的会话 - sessions = await self.session_manager.get_all_waiting_sessions() - for session in sessions: - if session.stream_id == self.stream_id: - return session.to_dict() - except Exception as e: - logger.error(f"获取会话信息失败: {e}") - return None - - def __str__(self) -> str: - """字符串表示""" - return f"KokoroFlowChatter(stream_id={self.stream_id})" - - def __repr__(self) -> str: - """详细字符串表示""" - return ( - f"KokoroFlowChatter(stream_id={self.stream_id}, " - f"messages_processed={self.stats['messages_processed']})" - ) diff --git a/src/plugins/built_in/kokoro_flow_chatter/config.py b/src/plugins/built_in/kokoro_flow_chatter/config.py deleted file mode 100644 index 0fc3bc30b..000000000 --- a/src/plugins/built_in/kokoro_flow_chatter/config.py +++ /dev/null @@ -1,251 +0,0 @@ -""" -Kokoro Flow Chatter 插件配置 - -定义插件的配置项和默认值 -""" - -from dataclasses import dataclass, field -from typing import Any - - -@dataclass -class PersonaConfig: - """人设配置""" - description: str = """你是一个温暖、真诚的对话伙伴。 -你有自己的情感和想法,会真实地表达内心的感受。 -你关心与用户的关系,但也保持健康的边界。 -你可以感到开心、好奇、担心、或者稍微沮丧,这些都是正常的。""" - - name: str = "心流助手" - """AI的名字""" - - -@dataclass -class BehaviorConfig: - """行为配置""" - max_wait_seconds_default: int = 300 - """默认最大等待秒数""" - - min_wait_seconds: int = 60 - """最小等待秒数""" - - max_wait_seconds_limit: int = 600 - """最大等待秒数上限""" - - enable_continuous_thinking: bool = True - """是否启用连续思考""" - - continuous_thinking_triggers: list[float] = field( - default_factory=lambda: [0.3, 0.6, 0.85] - ) - """连续思考触发点(等待进度百分比)""" - - scheduler_check_interval: float = 10.0 - """调度器检查间隔(秒)""" - - -@dataclass -class SessionConfig: - """会话配置""" - data_dir: str = "data/kokoro_flow_chatter/sessions" - """会话数据存储目录""" - - max_session_age_days: int = 30 - """会话最大保留天数""" - - auto_save_interval: int = 300 - """自动保存间隔(秒)""" - - max_mental_log_size: int = 100 - """心理日志最大条目数""" - - -@dataclass -class LLMConfig: - """LLM配置""" - model_name: str = "" - """使用的模型名称,留空则使用默认主模型""" - - max_tokens: int = 2048 - """最大生成token数""" - - temperature: float = 0.8 - """生成温度""" - - -@dataclass -class EmotionalConfig: - """情感系统配置""" - initial_mood: str = "neutral" - """初始心情""" - - initial_mood_intensity: float = 0.5 - """初始心情强度""" - - initial_relationship_warmth: float = 0.5 - """初始关系热度""" - - anxiety_increase_rate: float = 0.5 - """焦虑增长率(平方根系数)""" - - -@dataclass -class KokoroFlowChatterConfig: - """心流聊天器完整配置""" - enabled: bool = True - """是否启用插件""" - - persona: PersonaConfig = field(default_factory=PersonaConfig) - """人设配置""" - - behavior: BehaviorConfig = field(default_factory=BehaviorConfig) - """行为配置""" - - session: SessionConfig = field(default_factory=SessionConfig) - """会话配置""" - - llm: LLMConfig = field(default_factory=LLMConfig) - """LLM配置""" - - emotional: EmotionalConfig = field(default_factory=EmotionalConfig) - """情感系统配置""" - - def to_dict(self) -> dict[str, Any]: - """转换为字典""" - return { - "enabled": self.enabled, - "persona": { - "description": self.persona.description, - "name": self.persona.name, - }, - "behavior": { - "max_wait_seconds_default": self.behavior.max_wait_seconds_default, - "min_wait_seconds": self.behavior.min_wait_seconds, - "max_wait_seconds_limit": self.behavior.max_wait_seconds_limit, - "enable_continuous_thinking": self.behavior.enable_continuous_thinking, - "continuous_thinking_triggers": self.behavior.continuous_thinking_triggers, - "scheduler_check_interval": self.behavior.scheduler_check_interval, - }, - "session": { - "data_dir": self.session.data_dir, - "max_session_age_days": self.session.max_session_age_days, - "auto_save_interval": self.session.auto_save_interval, - "max_mental_log_size": self.session.max_mental_log_size, - }, - "llm": { - "model_name": self.llm.model_name, - "max_tokens": self.llm.max_tokens, - "temperature": self.llm.temperature, - }, - "emotional": { - "initial_mood": self.emotional.initial_mood, - "initial_mood_intensity": self.emotional.initial_mood_intensity, - "initial_relationship_warmth": self.emotional.initial_relationship_warmth, - "anxiety_increase_rate": self.emotional.anxiety_increase_rate, - }, - } - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> "KokoroFlowChatterConfig": - """从字典创建配置""" - config = cls() - - if "enabled" in data: - config.enabled = data["enabled"] - - if "persona" in data: - persona_data = data["persona"] - config.persona.description = persona_data.get( - "description", - config.persona.description - ) - config.persona.name = persona_data.get( - "name", - config.persona.name - ) - - if "behavior" in data: - behavior_data = data["behavior"] - config.behavior.max_wait_seconds_default = behavior_data.get( - "max_wait_seconds_default", - config.behavior.max_wait_seconds_default - ) - config.behavior.min_wait_seconds = behavior_data.get( - "min_wait_seconds", - config.behavior.min_wait_seconds - ) - config.behavior.max_wait_seconds_limit = behavior_data.get( - "max_wait_seconds_limit", - config.behavior.max_wait_seconds_limit - ) - config.behavior.enable_continuous_thinking = behavior_data.get( - "enable_continuous_thinking", - config.behavior.enable_continuous_thinking - ) - config.behavior.continuous_thinking_triggers = behavior_data.get( - "continuous_thinking_triggers", - config.behavior.continuous_thinking_triggers - ) - config.behavior.scheduler_check_interval = behavior_data.get( - "scheduler_check_interval", - config.behavior.scheduler_check_interval - ) - - if "session" in data: - session_data = data["session"] - config.session.data_dir = session_data.get( - "data_dir", - config.session.data_dir - ) - config.session.max_session_age_days = session_data.get( - "max_session_age_days", - config.session.max_session_age_days - ) - config.session.auto_save_interval = session_data.get( - "auto_save_interval", - config.session.auto_save_interval - ) - config.session.max_mental_log_size = session_data.get( - "max_mental_log_size", - config.session.max_mental_log_size - ) - - if "llm" in data: - llm_data = data["llm"] - config.llm.model_name = llm_data.get( - "model_name", - config.llm.model_name - ) - config.llm.max_tokens = llm_data.get( - "max_tokens", - config.llm.max_tokens - ) - config.llm.temperature = llm_data.get( - "temperature", - config.llm.temperature - ) - - if "emotional" in data: - emotional_data = data["emotional"] - config.emotional.initial_mood = emotional_data.get( - "initial_mood", - config.emotional.initial_mood - ) - config.emotional.initial_mood_intensity = emotional_data.get( - "initial_mood_intensity", - config.emotional.initial_mood_intensity - ) - config.emotional.initial_relationship_warmth = emotional_data.get( - "initial_relationship_warmth", - config.emotional.initial_relationship_warmth - ) - config.emotional.anxiety_increase_rate = emotional_data.get( - "anxiety_increase_rate", - config.emotional.anxiety_increase_rate - ) - - return config - - -# 默认配置实例 -default_config = KokoroFlowChatterConfig() diff --git a/src/plugins/built_in/kokoro_flow_chatter/context_builder.py b/src/plugins/built_in/kokoro_flow_chatter/context_builder.py deleted file mode 100644 index 26180c067..000000000 --- a/src/plugins/built_in/kokoro_flow_chatter/context_builder.py +++ /dev/null @@ -1,528 +0,0 @@ -""" -Kokoro Flow Chatter 上下文构建器 - -该模块负责从 S4U 移植的所有上下文模块,为 KFC 提供"全知"Prompt所需的完整情境感知能力。 -包含: -- 关系信息 (relation_info) -- 记忆块 (memory_block) -- 表达习惯 (expression_habits) -- 知识库 (knowledge) -- 跨上下文 (cross_context) -- 日程信息 (schedule) -- 通知块 (notice) -- 历史消息构建 (history) -""" - -import asyncio -import time -from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any, Optional - -from src.common.logger import get_logger -from src.config.config import global_config -from src.person_info.person_info import get_person_info_manager, PersonInfoManager - -if TYPE_CHECKING: - from src.chat.message_receive.chat_stream import ChatStream - from src.common.data_models.message_manager_data_model import StreamContext - from src.config.config import BotConfig # 用于类型提示 - -logger = get_logger("kfc_context_builder") - - -# 类型断言辅助函数 -def _get_config(): - """获取全局配置(带类型断言)""" - assert global_config is not None, "global_config 未初始化" - return global_config - - -class KFCContextBuilder: - """ - KFC 上下文构建器 - - 从 S4U 的 DefaultReplyer 移植所有上下文构建能力, - 为 KFC 的"超融合"Prompt 提供完整的情境感知数据。 - """ - - def __init__(self, chat_stream: "ChatStream"): - """ - 初始化上下文构建器 - - Args: - chat_stream: 当前聊天流 - """ - self.chat_stream = chat_stream - self.chat_id = chat_stream.stream_id - self.platform = chat_stream.platform - self.is_group_chat = bool(chat_stream.group_info) - - # 延迟初始化的组件 - self._tool_executor: Any = None - self._expression_selector: Any = None - - @property - def tool_executor(self) -> Any: - """延迟初始化工具执行器""" - if self._tool_executor is None: - from src.plugin_system.core.tool_use import ToolExecutor - self._tool_executor = ToolExecutor(chat_id=self.chat_id) - return self._tool_executor - - async def build_all_context( - self, - sender_name: str, - target_message: str, - context: Optional["StreamContext"] = None, - ) -> dict[str, str]: - """ - 并行构建所有上下文模块 - - Args: - sender_name: 发送者名称 - target_message: 目标消息内容 - context: 聊天流上下文(可选) - - Returns: - dict: 包含所有上下文块的字典 - """ - # 获取历史消息用于构建各种上下文 - chat_history = await self._get_chat_history_text(context) - - # 并行执行所有上下文构建任务 - tasks = { - "relation_info": self._build_relation_info(sender_name, target_message), - "memory_block": self._build_memory_block(chat_history, target_message), - "expression_habits": self._build_expression_habits(chat_history, target_message), - "schedule": self._build_schedule_block(), - "time": self._build_time_block(), - } - - results = {} - try: - task_results = await asyncio.gather( - *[self._wrap_task(name, coro) for name, coro in tasks.items()], - return_exceptions=True - ) - - for result in task_results: - if isinstance(result, tuple): - name, value = result - results[name] = value - else: - logger.warning(f"上下文构建任务异常: {result}") - except Exception as e: - logger.error(f"并行构建上下文失败: {e}") - - return results - - async def _wrap_task(self, name: str, coro) -> tuple[str, str]: - """包装任务以返回名称和结果""" - try: - result = await coro - return (name, result or "") - except Exception as e: - logger.error(f"构建 {name} 失败: {e}") - return (name, "") - - async def _get_chat_history_text( - self, - context: Optional["StreamContext"] = None, - limit: int = 20, - ) -> str: - """ - 获取聊天历史文本 - - Args: - context: 聊天流上下文 - limit: 最大消息数量 - - Returns: - str: 格式化的聊天历史 - """ - if context is None: - return "" - - try: - from src.chat.utils.chat_message_builder import build_readable_messages - - messages = context.get_messages(limit=limit, include_unread=True) - if not messages: - return "" - - # 转换为字典格式 - msg_dicts = [msg.flatten() for msg in messages] - - return await build_readable_messages( - msg_dicts, - replace_bot_name=True, - timestamp_mode="relative", - truncate=True, - ) - except Exception as e: - logger.error(f"获取聊天历史失败: {e}") - return "" - - async def _build_relation_info(self, sender_name: str, target_message: str) -> str: - """ - 构建关系信息块 - - 从 S4U 的 build_relation_info 移植 - - Args: - sender_name: 发送者名称 - target_message: 目标消息 - - Returns: - str: 格式化的关系信息 - """ - config = _get_config() - - # 检查是否是Bot自己的消息 - if sender_name == f"{config.bot.nickname}(你)": - return "你将要回复的是你自己发送的消息。" - - person_info_manager = get_person_info_manager() - person_id = await person_info_manager.get_person_id_by_person_name(sender_name) - - if not person_id: - logger.debug(f"未找到用户 {sender_name} 的ID") - return f"你完全不认识{sender_name},这是你们的第一次互动。" - - try: - from src.person_info.relationship_fetcher import relationship_fetcher_manager - - relationship_fetcher = relationship_fetcher_manager.get_fetcher(self.chat_id) - - # 构建用户关系信息(包含别名、偏好关键词等字段) - user_relation_info = await relationship_fetcher.build_relation_info(person_id, points_num=5) - - # 构建聊天流印象信息(群聊/私聊的整体印象) - stream_impression = await relationship_fetcher.build_chat_stream_impression(self.chat_id) - - # 组合信息 - parts = [] - if user_relation_info: - parts.append(f"### 你与 {sender_name} 的关系\n{user_relation_info}") - if stream_impression: - scene_type = "这个群" if self.is_group_chat else "你们的私聊" - parts.append(f"### 你对{scene_type}的印象\n{stream_impression}") - - if parts: - return "\n\n".join(parts) - else: - return f"你与{sender_name}还没有建立深厚的关系,这是早期的互动阶段。" - - except Exception as e: - logger.error(f"获取关系信息失败: {e}") - return self._build_fallback_relation_info(sender_name, person_id) - - def _build_fallback_relation_info(self, sender_name: str, person_id: str) -> str: - """降级的关系信息构建""" - return f"你与{sender_name}是普通朋友关系。" - - async def _build_memory_block(self, chat_history: str, target_message: str) -> str: - """ - 构建记忆块 - - 从 S4U 的 build_memory_block 移植,使用三层记忆系统 - - Args: - chat_history: 聊天历史 - target_message: 目标消息 - - Returns: - str: 格式化的记忆信息 - """ - config = _get_config() - - if not (config.memory and config.memory.enable): - return "" - - try: - from src.memory_graph.manager_singleton import get_unified_memory_manager - from src.memory_graph.utils.three_tier_formatter import memory_formatter - - unified_manager = get_unified_memory_manager() - if not unified_manager: - logger.debug("[三层记忆] 管理器未初始化") - return "" - - # 使用统一管理器的智能检索 - search_result = await unified_manager.search_memories( - query_text=target_message, - use_judge=True, - recent_chat_history=chat_history, - ) - - if not search_result: - return "" - - # 分类记忆块 - perceptual_blocks = search_result.get("perceptual_blocks", []) - short_term_memories = search_result.get("short_term_memories", []) - long_term_memories = search_result.get("long_term_memories", []) - - # 使用三级记忆格式化器 - formatted_memories = await memory_formatter.format_all_tiers( - perceptual_blocks=perceptual_blocks, - short_term_memories=short_term_memories, - long_term_memories=long_term_memories - ) - - total_count = len(perceptual_blocks) + len(short_term_memories) + len(long_term_memories) - if total_count > 0 and formatted_memories.strip(): - logger.info( - f"[三层记忆] 检索到 {total_count} 条记忆 " - f"(感知:{len(perceptual_blocks)}, 短期:{len(short_term_memories)}, 长期:{len(long_term_memories)})" - ) - return f"### 🧠 相关记忆\n\n{formatted_memories}" - - return "" - - except Exception as e: - logger.error(f"[三层记忆] 检索失败: {e}") - return "" - - async def _build_expression_habits(self, chat_history: str, target_message: str) -> str: - """ - 构建表达习惯块 - - 从 S4U 的 build_expression_habits 移植 - - Args: - chat_history: 聊天历史 - target_message: 目标消息 - - Returns: - str: 格式化的表达习惯 - """ - config = _get_config() - - # 检查是否允许使用表达 - use_expression, _, _ = config.expression.get_expression_config_for_chat(self.chat_id) - if not use_expression: - return "" - - try: - from src.chat.express.expression_selector import expression_selector - - style_habits = [] - grammar_habits = [] - - # 使用统一的表达方式选择 - selected_expressions = await expression_selector.select_suitable_expressions( - chat_id=self.chat_id, - chat_history=chat_history, - target_message=target_message, - max_num=8, - min_num=2 - ) - - if selected_expressions: - for expr in selected_expressions: - if isinstance(expr, dict) and "situation" in expr and "style" in expr: - expr_type = expr.get("type", "style") - habit_str = f"当{expr['situation']}时,使用 {expr['style']}" - if expr_type == "grammar": - grammar_habits.append(habit_str) - else: - style_habits.append(habit_str) - - # 构建表达习惯块 - parts = [] - if style_habits: - parts.append("**语言风格习惯**:\n" + "\n".join(f"- {h}" for h in style_habits)) - if grammar_habits: - parts.append("**句法习惯**:\n" + "\n".join(f"- {h}" for h in grammar_habits)) - - if parts: - return "### 💬 你的表达习惯\n\n" + "\n\n".join(parts) - - return "" - - except Exception as e: - logger.error(f"构建表达习惯失败: {e}") - return "" - - async def _build_schedule_block(self) -> str: - """ - 构建日程信息块 - - 从 S4U 移植 - - Returns: - str: 格式化的日程信息 - """ - config = _get_config() - - if not config.planning_system.schedule_enable: - return "" - - try: - from src.schedule.schedule_manager import schedule_manager - - activity_info = schedule_manager.get_current_activity() - if not activity_info: - return "" - - activity = activity_info.get("activity") - time_range = activity_info.get("time_range") - now = datetime.now() - - if time_range: - try: - start_str, end_str = time_range.split("-") - start_time = datetime.strptime(start_str.strip(), "%H:%M").replace( - year=now.year, month=now.month, day=now.day - ) - end_time = datetime.strptime(end_str.strip(), "%H:%M").replace( - year=now.year, month=now.month, day=now.day - ) - - if end_time < start_time: - end_time += timedelta(days=1) - if now < start_time: - now += timedelta(days=1) - - duration_minutes = (now - start_time).total_seconds() / 60 - remaining_minutes = (end_time - now).total_seconds() / 60 - - return ( - f"你当前正在「{activity}」," - f"从{start_time.strftime('%H:%M')}开始,预计{end_time.strftime('%H:%M')}结束," - f"已进行{duration_minutes:.0f}分钟,还剩约{remaining_minutes:.0f}分钟。" - ) - except (ValueError, AttributeError): - pass - - return f"你当前正在「{activity}」" - - except Exception as e: - logger.error(f"构建日程块失败: {e}") - return "" - - async def _build_time_block(self) -> str: - """构建时间信息块""" - now = datetime.now() - weekdays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] - weekday = weekdays[now.weekday()] - - return f"{now.strftime('%Y年%m月%d日')} {weekday} {now.strftime('%H:%M:%S')}" - - async def build_s4u_style_history( - self, - context: "StreamContext", - max_read: int = 10, - max_unread: int = 10, - ) -> tuple[str, str]: - """ - 构建 S4U 风格的已读/未读历史消息 - - 从 S4U 的 build_s4u_chat_history_prompts 移植 - - Args: - context: 聊天流上下文 - max_read: 最大已读消息数 - max_unread: 最大未读消息数 - - Returns: - tuple[str, str]: (已读历史, 未读历史) - """ - try: - from src.chat.utils.chat_message_builder import build_readable_messages, replace_user_references_async - - # 确保历史消息已初始化 - await context.ensure_history_initialized() - - read_messages = context.history_messages - unread_messages = context.get_unread_messages() - - # 构建已读历史 - read_history = "" - if read_messages: - read_dicts = [msg.flatten() for msg in read_messages[-max_read:]] - read_content = await build_readable_messages( - read_dicts, - replace_bot_name=True, - timestamp_mode="normal_no_YMD", - truncate=True, - ) - read_history = f"### 📜 已读历史消息\n{read_content}" - - # 构建未读历史 - unread_history = "" - if unread_messages: - unread_lines = [] - for msg in unread_messages[-max_unread:]: - msg_time = time.strftime("%H:%M:%S", time.localtime(msg.time)) - msg_content = msg.processed_plain_text or "" - - # 获取发送者名称 - sender_name = await self._get_sender_name(msg) - - # 处理消息内容中的用户引用 - if msg_content: - msg_content = await replace_user_references_async( - msg_content, - self.platform, - replace_bot_name=True - ) - - unread_lines.append(f"{msg_time} {sender_name}: {msg_content}") - - unread_history = f"### 📬 未读历史消息\n" + "\n".join(unread_lines) - - return read_history, unread_history - - except Exception as e: - logger.error(f"构建S4U风格历史失败: {e}") - return "", "" - - async def _get_sender_name(self, msg) -> str: - """获取消息发送者名称""" - config = _get_config() - - try: - user_info = getattr(msg, "user_info", {}) - platform = getattr(user_info, "platform", "") or getattr(msg, "platform", "") - user_id = getattr(user_info, "user_id", "") or getattr(msg, "user_id", "") - - if not (platform and user_id): - return "未知用户" - - person_id = PersonInfoManager.get_person_id(platform, user_id) - person_info_manager = get_person_info_manager() - sender_name = await person_info_manager.get_value(person_id, "person_name") or "未知用户" - - # 如果是Bot自己,标记为(你) - if user_id == str(config.bot.qq_account): - sender_name = f"{config.bot.nickname}(你)" - - return sender_name - - except Exception: - return "未知用户" - - -# 模块级便捷函数 -async def build_kfc_context( - chat_stream: "ChatStream", - sender_name: str, - target_message: str, - context: Optional["StreamContext"] = None, -) -> dict[str, str]: - """ - 便捷函数:构建KFC所需的所有上下文 - - Args: - chat_stream: 聊天流 - sender_name: 发送者名称 - target_message: 目标消息 - context: 聊天流上下文 - - Returns: - dict: 包含所有上下文块的字典 - """ - builder = KFCContextBuilder(chat_stream) - return await builder.build_all_context(sender_name, target_message, context) 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 deleted file mode 100644 index f512e8c67..000000000 --- a/src/plugins/built_in/kokoro_flow_chatter/kfc_scheduler_adapter.py +++ /dev/null @@ -1,707 +0,0 @@ -""" -Kokoro Flow Chatter 调度器适配器 - -基于项目统一的 UnifiedScheduler 实现 KFC 的定时任务功能。 -不再自己创建后台循环,而是复用全局调度器的基础设施。 - -核心功能: -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, -) - -from .models import ( - KokoroSession, - MentalLogEntry, - MentalLogEventType, - SessionStatus, -) -from .session_manager import get_session_manager - -if TYPE_CHECKING: - from .chatter import KokoroFlowChatter - -logger = get_logger("kokoro_scheduler_adapter") - - -class KFCSchedulerAdapter: - """ - KFC 调度器适配器 - - 使用 UnifiedScheduler 实现 KFC 的定时任务功能,不再自行管理后台循环。 - - 核心功能: - 1. 定期检查处于 WAITING 状态的会话(短期等待超时) - 2. 在特定时间点触发"连续思考"(等待期间内心活动) - 3. 定期检查长期沉默的会话,触发"主动思考"(长期主动发起) - 4. 处理等待超时并触发决策 - """ - - # 连续思考触发点(等待进度的百分比) - CONTINUOUS_THINKING_TRIGGERS = [0.3, 0.6, 0.85] - - # 任务名称常量 - 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: 等待检查间隔(秒) - 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, - trigger_config={"delay_seconds": self.check_interval}, - is_recurring=True, - task_name=self.TASK_NAME_WAITING_CHECK, - force_overwrite=True, - 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}") - - async def stop(self) -> None: - """停止调度器(从 UnifiedScheduler 注销)""" - if not self._registered: - return - - try: - if self._schedule_id: - await unified_scheduler.remove_schedule(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 调用) - - 优化:使用 asyncio.create_task 并行处理多个会话,避免顺序阻塞 - """ - session_manager = get_session_manager() - waiting_sessions = await session_manager.get_all_waiting_sessions() - - self._stats["total_checks"] += 1 - self._stats["last_check_time"] = time.time() - - if not waiting_sessions: - return - - # 并行处理所有等待中的会话,避免一个会话阻塞其他会话 - tasks = [] - for session in waiting_sessions: - task = asyncio.create_task( - self._safe_process_waiting_session(session), - name=f"kfc_session_check_{session.user_id}" - ) - tasks.append(task) - - # 等待所有任务完成,但每个任务都有独立的异常处理 - if tasks: - await asyncio.gather(*tasks, return_exceptions=True) - - async def _safe_process_waiting_session(self, session: KokoroSession) -> None: - """安全地处理等待会话,带有超时保护""" - try: - # 给每个会话处理设置 60 秒超时(LLM 调用可能需要较长时间) - await asyncio.wait_for( - self._process_waiting_session(session), - timeout=60.0 - ) - except asyncio.TimeoutError: - logger.error(f"处理等待会话 {session.user_id} 超时(60秒)") - except Exception as e: - logger.error(f"处理等待会话 {session.user_id} 时出错: {e}") - - async def _process_waiting_session(self, session: KokoroSession) -> None: - """ - 处理单个等待中的会话 - - Args: - session: 等待中的会话 - """ - if session.status != SessionStatus.WAITING: - return - - if session.waiting_since is None: - return - - wait_duration = session.get_waiting_duration() - max_wait = session.max_wait_seconds - - # max_wait_seconds = 0 表示不等待,直接返回 IDLE - if max_wait <= 0: - logger.info(f"会话 {session.user_id} 设置为不等待 (max_wait=0),返回空闲状态") - session.status = SessionStatus.IDLE - session.end_waiting() - session_manager = get_session_manager() - await session_manager.save_session(session.user_id) - return - - # 检查是否超时 - if session.is_wait_timeout(): - logger.info(f"会话 {session.user_id} 等待超时,触发决策") - await self._handle_timeout(session) - return - - # 检查是否需要触发连续思考 - wait_progress = wait_duration / max_wait if max_wait > 0 else 0 - - for trigger_point in self.CONTINUOUS_THINKING_TRIGGERS: - if self._should_trigger_continuous_thinking(session, wait_progress, trigger_point): - logger.debug( - f"会话 {session.user_id} 触发连续思考 " - f"(进度: {wait_progress:.1%}, 触发点: {trigger_point:.1%})" - ) - await self._handle_continuous_thinking(session, wait_progress) - break - - def _should_trigger_continuous_thinking( - self, - session: KokoroSession, - current_progress: float, - trigger_point: float, - ) -> bool: - """ - 判断是否应该触发连续思考 - """ - if current_progress < trigger_point: - return False - - expected_count = sum( - 1 for tp in self.CONTINUOUS_THINKING_TRIGGERS - if current_progress >= tp - ) - - if session.continuous_thinking_count < expected_count: - if session.last_continuous_thinking_at is None: - return True - - time_since_last = time.time() - session.last_continuous_thinking_at - return time_since_last >= 30.0 - - return False - - async def _handle_timeout(self, session: KokoroSession) -> None: - """ - 处理等待超时 - - Args: - session: 超时的会话 - """ - self._stats["timeouts_triggered"] += 1 - - # 更新会话状态 - session.status = SessionStatus.FOLLOW_UP_PENDING - session.emotional_state.anxiety_level = 0.8 - - # 添加超时日志 - timeout_entry = MentalLogEntry( - event_type=MentalLogEventType.TIMEOUT_DECISION, - timestamp=time.time(), - thought=f"等了{session.max_wait_seconds}秒了,对方还是没有回复...", - content="等待超时", - emotional_snapshot=session.emotional_state.to_dict(), - ) - session.add_mental_log_entry(timeout_entry) - - # 保存会话状态 - session_manager = get_session_manager() - await session_manager.save_session(session.user_id) - - # 调用超时回调 - if self.on_timeout_callback: - try: - await self.on_timeout_callback(session) - except Exception as e: - logger.error(f"执行超时回调时出错 (user={session.user_id}): {e}") - - async def _handle_continuous_thinking( - self, - session: KokoroSession, - wait_progress: float, - ) -> None: - """ - 处理连续思考 - - Args: - session: 会话 - wait_progress: 等待进度 - """ - self._stats["continuous_thinking_triggered"] += 1 - - # 更新焦虑程度 - session.emotional_state.update_anxiety_over_time( - session.get_waiting_duration(), - session.max_wait_seconds - ) - - # 更新连续思考计数 - session.continuous_thinking_count += 1 - session.last_continuous_thinking_at = time.time() - - # 生成基于进度的内心想法 - thought = self._generate_waiting_thought(session, wait_progress) - - # 添加连续思考日志 - thinking_entry = MentalLogEntry( - event_type=MentalLogEventType.CONTINUOUS_THINKING, - timestamp=time.time(), - thought=thought, - content="", - emotional_snapshot=session.emotional_state.to_dict(), - metadata={"wait_progress": wait_progress}, - ) - session.add_mental_log_entry(thinking_entry) - - # 保存会话状态 - session_manager = get_session_manager() - await session_manager.save_session(session.user_id) - - # 调用连续思考回调 - if self.on_continuous_thinking_callback: - try: - await self.on_continuous_thinking_callback(session) - except Exception as e: - logger.error(f"执行连续思考回调时出错 (user={session.user_id}): {e}") - - def _generate_waiting_thought( - self, - session: KokoroSession, - wait_progress: float, - ) -> str: - """ - 生成等待中的内心想法(简单版本,不调用LLM) - """ - import random - - wait_seconds = session.get_waiting_duration() - wait_minutes = wait_seconds / 60 - - if wait_progress < 0.4: - thoughts = [ - f"已经等了{wait_minutes:.1f}分钟了,对方可能在忙吧...", - f"嗯...{wait_minutes:.1f}分钟过去了,不知道对方在做什么", - "对方好像还没看到消息,再等等吧", - ] - elif wait_progress < 0.7: - thoughts = [ - f"等了{wait_minutes:.1f}分钟了,有点担心对方是不是不想回了", - f"{wait_minutes:.1f}分钟了,对方可能真的很忙?", - "时间过得好慢啊...不知道对方什么时候会回复", - ] - else: - thoughts = [ - f"已经等了{wait_minutes:.1f}分钟了,感觉有点焦虑...", - f"快{wait_minutes:.0f}分钟了,对方是不是忘记回复了?", - "等了这么久,要不要主动说点什么呢...", - ] - - 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]], - ) -> None: - """设置超时回调函数""" - self.on_timeout_callback = callback - - def set_continuous_thinking_callback( - self, - callback: Callable[[KokoroSession], Coroutine[Any, Any, None]], - ) -> None: - """设置连续思考回调函数""" - 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 { - **self._stats, - "is_running": self._registered, - "check_interval": self.check_interval, - } - - @property - def is_running(self) -> bool: - """调度器是否正在运行""" - return self._registered - - -# 全局适配器实例 -_scheduler_adapter: Optional[KFCSchedulerAdapter] = None - - -def get_scheduler() -> KFCSchedulerAdapter: - """获取全局调度器适配器实例""" - global _scheduler_adapter - if _scheduler_adapter is None: - _scheduler_adapter = KFCSchedulerAdapter() - return _scheduler_adapter - - -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: - """ - 初始化并启动调度器 - - Args: - check_interval: 检查间隔 - on_timeout_callback: 超时回调 - on_continuous_thinking_callback: 连续思考回调 - on_proactive_thinking_callback: 主动思考回调 - - Returns: - KFCSchedulerAdapter: 调度器适配器实例 - """ - global _scheduler_adapter - _scheduler_adapter = KFCSchedulerAdapter( - 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 - - -async def shutdown_scheduler() -> None: - """关闭调度器""" - global _scheduler_adapter - if _scheduler_adapter: - await _scheduler_adapter.stop() - _scheduler_adapter = None diff --git a/src/plugins/built_in/kokoro_flow_chatter/models.py b/src/plugins/built_in/kokoro_flow_chatter/models.py deleted file mode 100644 index 277dfe846..000000000 --- a/src/plugins/built_in/kokoro_flow_chatter/models.py +++ /dev/null @@ -1,459 +0,0 @@ -""" -Kokoro Flow Chatter 数据模型 - -定义心流聊天器的核心数据结构,包括: -- SessionStatus: 会话状态枚举 -- EmotionalState: 情感状态模型 -- MentalLogEntry: 心理活动日志条目 -- KokoroSession: 完整的会话模型 -- LLMResponseModel: LLM响应结构 -- ActionModel: 动作模型 -""" - -from dataclasses import dataclass, field -from enum import Enum -from typing import Any, Optional -import time - - -class SessionStatus(Enum): - """ - 会话状态枚举 - - 状态机核心,定义了KFC系统的四个基本状态: - - IDLE: 空闲态,会话的起点和终点 - - RESPONDING: 响应中,正在处理消息和生成决策 - - WAITING: 等待态,已发送回复,等待用户回应 - - FOLLOW_UP_PENDING: 决策态,等待超时后进行后续决策 - """ - IDLE = "idle" - RESPONDING = "responding" - WAITING = "waiting" - FOLLOW_UP_PENDING = "follow_up_pending" - - def __str__(self) -> str: - return self.value - - -class MentalLogEventType(Enum): - """ - 心理活动日志事件类型 - - 用于标记线性叙事历史中不同类型的事件 - """ - USER_MESSAGE = "user_message" # 用户消息事件 - BOT_ACTION = "bot_action" # Bot行动事件 - WAITING_UPDATE = "waiting_update" # 等待期间的心理更新 - TIMEOUT_DECISION = "timeout_decision" # 超时决策事件 - STATE_CHANGE = "state_change" # 状态变更事件 - CONTINUOUS_THINKING = "continuous_thinking" # 连续思考事件 - PROACTIVE_THINKING = "proactive_thinking" # 主动思考事件(长期沉默后主动发起) - - def __str__(self) -> str: - return self.value - - -@dataclass -class EmotionalState: - """ - 动态情感状态模型 - - 记录和跟踪AI的情感参数,用于驱动个性化的交互行为 - - Attributes: - mood: 当前心情标签(如:开心、好奇、疲惫、沮丧) - mood_intensity: 心情强度,0.0-1.0 - relationship_warmth: 关系热度,代表与用户的亲密度,0.0-1.0 - impression_of_user: 对用户的动态印象描述 - anxiety_level: 焦虑程度,0.0-1.0,在等待时会变化 - engagement_level: 投入程度,0.0-1.0,表示对当前对话的关注度 - last_update_time: 最后更新时间戳 - """ - mood: str = "平静" # V7: 改为中文"平静",更自然 - mood_intensity: float = 0.3 # V7: 默认低强度,避免无厘头的强烈情绪 - relationship_warmth: float = 0.5 - impression_of_user: str = "" - anxiety_level: float = 0.0 - engagement_level: float = 0.5 - last_update_time: float = field(default_factory=time.time) - - def to_dict(self) -> dict[str, Any]: - """转换为字典格式""" - return { - "mood": self.mood, - "mood_intensity": self.mood_intensity, - "relationship_warmth": self.relationship_warmth, - "impression_of_user": self.impression_of_user, - "anxiety_level": self.anxiety_level, - "engagement_level": self.engagement_level, - "last_update_time": self.last_update_time, - } - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> "EmotionalState": - """从字典创建实例""" - return cls( - mood=data.get("mood", "neutral"), - mood_intensity=data.get("mood_intensity", 0.5), - relationship_warmth=data.get("relationship_warmth", 0.5), - impression_of_user=data.get("impression_of_user", ""), - anxiety_level=data.get("anxiety_level", 0.0), - engagement_level=data.get("engagement_level", 0.5), - last_update_time=data.get("last_update_time", time.time()), - ) - - def update_anxiety_over_time(self, elapsed_seconds: float, max_wait_seconds: float) -> None: - """ - 根据等待时间更新焦虑程度 - - Args: - elapsed_seconds: 已等待的秒数 - max_wait_seconds: 最大等待秒数 - """ - if max_wait_seconds <= 0: - return - - # 焦虑程度随时间流逝增加,使用平方根函数使增长趋于平缓 - wait_ratio = min(elapsed_seconds / max_wait_seconds, 1.0) - self.anxiety_level = min(wait_ratio ** 0.5, 1.0) - self.last_update_time = time.time() - - -@dataclass -class MentalLogEntry: - """ - 心理活动日志条目 - - 记录线性叙事历史中的每一个事件节点, - 是实现"连续主观体验"的核心数据结构 - - Attributes: - event_type: 事件类型 - timestamp: 事件发生时间戳 - thought: 内心独白 - content: 事件内容(如用户消息、Bot回复等) - emotional_snapshot: 事件发生时的情感状态快照 - metadata: 额外元数据 - """ - event_type: MentalLogEventType - timestamp: float - thought: str = "" - content: str = "" - emotional_snapshot: Optional[dict[str, Any]] = None - metadata: dict[str, Any] = field(default_factory=dict) - - def to_dict(self) -> dict[str, Any]: - """转换为字典格式""" - return { - "event_type": str(self.event_type), - "timestamp": self.timestamp, - "thought": self.thought, - "content": self.content, - "emotional_snapshot": self.emotional_snapshot, - "metadata": self.metadata, - } - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> "MentalLogEntry": - """从字典创建实例""" - event_type_str = data.get("event_type", "state_change") - try: - event_type = MentalLogEventType(event_type_str) - except ValueError: - event_type = MentalLogEventType.STATE_CHANGE - - return cls( - event_type=event_type, - timestamp=data.get("timestamp", time.time()), - thought=data.get("thought", ""), - content=data.get("content", ""), - emotional_snapshot=data.get("emotional_snapshot"), - metadata=data.get("metadata", {}), - ) - - -@dataclass -class KokoroSession: - """ - Kokoro Flow Chatter 会话模型 - - 为每个私聊用户维护一个独立的会话,包含: - - 基本会话信息 - - 当前状态 - - 情感状态 - - 线性叙事历史(心理活动日志) - - 等待相关的状态 - - Attributes: - user_id: 用户唯一标识 - stream_id: 聊天流ID - status: 当前会话状态 - emotional_state: 动态情感状态 - mental_log: 线性叙事历史 - expected_user_reaction: 对用户回应的预期 - max_wait_seconds: 最大等待秒数 - waiting_since: 开始等待的时间戳 - last_bot_message: 最后一条Bot消息 - last_user_message: 最后一条用户消息 - created_at: 会话创建时间 - last_activity_at: 最后活动时间 - total_interactions: 总交互次数 - """ - user_id: str - stream_id: str - status: SessionStatus = SessionStatus.IDLE - emotional_state: EmotionalState = field(default_factory=EmotionalState) - mental_log: list[MentalLogEntry] = field(default_factory=list) - - # 等待状态相关 - expected_user_reaction: str = "" - max_wait_seconds: int = 300 - waiting_since: Optional[float] = None - - # 消息记录 - last_bot_message: str = "" - last_user_message: str = "" - - # 统计信息 - created_at: float = field(default_factory=time.time) - last_activity_at: float = field(default_factory=time.time) - total_interactions: int = 0 - - # 连续思考相关 - continuous_thinking_count: int = 0 - last_continuous_thinking_at: Optional[float] = None - - # 主动思考相关(长期沉默后主动发起对话) - last_proactive_at: Optional[float] = None # 上次主动思考的时间 - proactive_count: int = 0 # 主动思考的次数(累计) - - # V7: 连续等待追问限制(防止用户不回复时连续追问) - consecutive_followup_count: int = 0 # 用户没回复时连续追问的次数 - max_consecutive_followups: int = 2 # 最多允许连续追问2次 - - def add_mental_log_entry(self, entry: MentalLogEntry, max_log_size: int = 100) -> None: - """ - 添加心理活动日志条目 - - Args: - entry: 日志条目 - max_log_size: 日志最大保留条数 - """ - self.mental_log.append(entry) - self.last_activity_at = time.time() - - # 保持日志在合理大小 - if len(self.mental_log) > max_log_size: - # 保留最近的日志 - self.mental_log = self.mental_log[-max_log_size:] - - def get_recent_mental_log(self, limit: int = 20) -> list[MentalLogEntry]: - """获取最近的心理活动日志""" - return self.mental_log[-limit:] if self.mental_log else [] - - def get_waiting_duration(self) -> float: - """获取当前等待时长(秒)""" - if self.waiting_since is None: - return 0.0 - return time.time() - self.waiting_since - - def is_wait_timeout(self) -> bool: - """检查是否等待超时""" - return self.get_waiting_duration() >= self.max_wait_seconds - - def start_waiting(self, expected_reaction: str, max_wait: int) -> None: - """开始等待状态""" - self.status = SessionStatus.WAITING - self.expected_user_reaction = expected_reaction - self.max_wait_seconds = max_wait - self.waiting_since = time.time() - self.continuous_thinking_count = 0 - - def end_waiting(self) -> None: - """结束等待状态""" - self.waiting_since = None - self.expected_user_reaction = "" - self.continuous_thinking_count = 0 - - def to_dict(self) -> dict[str, Any]: - """转换为可序列化的字典格式""" - return { - "user_id": self.user_id, - "stream_id": self.stream_id, - "status": str(self.status), - "emotional_state": self.emotional_state.to_dict(), - "mental_log": [entry.to_dict() for entry in self.mental_log], - "expected_user_reaction": self.expected_user_reaction, - "max_wait_seconds": self.max_wait_seconds, - "waiting_since": self.waiting_since, - "last_bot_message": self.last_bot_message, - "last_user_message": self.last_user_message, - "created_at": self.created_at, - "last_activity_at": self.last_activity_at, - "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, - "consecutive_followup_count": self.consecutive_followup_count, - "max_consecutive_followups": self.max_consecutive_followups, - } - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> "KokoroSession": - """从字典创建会话实例""" - status_str = data.get("status", "idle") - try: - status = SessionStatus(status_str) - except ValueError: - status = SessionStatus.IDLE - - emotional_state = EmotionalState.from_dict( - data.get("emotional_state", {}) - ) - - mental_log = [ - MentalLogEntry.from_dict(entry) - for entry in data.get("mental_log", []) - ] - - return cls( - user_id=data.get("user_id", ""), - stream_id=data.get("stream_id", ""), - status=status, - emotional_state=emotional_state, - mental_log=mental_log, - expected_user_reaction=data.get("expected_user_reaction", ""), - max_wait_seconds=data.get("max_wait_seconds", 300), - waiting_since=data.get("waiting_since"), - last_bot_message=data.get("last_bot_message", ""), - last_user_message=data.get("last_user_message", ""), - created_at=data.get("created_at", time.time()), - last_activity_at=data.get("last_activity_at", time.time()), - 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), - consecutive_followup_count=data.get("consecutive_followup_count", 0), - max_consecutive_followups=data.get("max_consecutive_followups", 2), - ) - - -@dataclass -class ActionModel: - """ - 动作模型 - - 表示LLM决策的单个动作 - - Attributes: - type: 动作类型(reply, poke_user, send_reaction, update_internal_state, do_nothing) - params: 动作参数 - """ - type: str - params: dict[str, Any] = field(default_factory=dict) - - def to_dict(self) -> dict[str, Any]: - """转换为字典格式""" - return { - "type": self.type, - **self.params - } - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> "ActionModel": - """从字典创建实例""" - action_type = data.get("type", "do_nothing") - params = {k: v for k, v in data.items() if k != "type"} - return cls(type=action_type, params=params) - - -@dataclass -class LLMResponseModel: - """ - LLM响应模型 - - 定义LLM输出的结构化JSON格式 - - Attributes: - thought: 内心独白(必须) - expected_user_reaction: 用户回应预期(必须) - max_wait_seconds: 最长等待秒数(必须) - actions: 行动列表(必须) - plan: 行动意图(可选) - emotional_updates: 情感状态更新(可选) - """ - thought: str - expected_user_reaction: str - max_wait_seconds: int - actions: list[ActionModel] - plan: str = "" - emotional_updates: Optional[dict[str, Any]] = None - - def to_dict(self) -> dict[str, Any]: - """转换为字典格式""" - result = { - "thought": self.thought, - "expected_user_reaction": self.expected_user_reaction, - "max_wait_seconds": self.max_wait_seconds, - "actions": [action.to_dict() for action in self.actions], - } - if self.plan: - result["plan"] = self.plan - if self.emotional_updates: - result["emotional_updates"] = self.emotional_updates - return result - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> "LLMResponseModel": - """从字典创建实例""" - actions = [ - ActionModel.from_dict(action) - for action in data.get("actions", []) - ] - - # 如果没有actions,添加默认的do_nothing - if not actions: - actions = [ActionModel(type="do_nothing")] - - return cls( - thought=data.get("thought", ""), - expected_user_reaction=data.get("expected_user_reaction", ""), - max_wait_seconds=data.get("max_wait_seconds", 300), - actions=actions, - plan=data.get("plan", ""), - emotional_updates=data.get("emotional_updates"), - ) - - @classmethod - def create_error_response(cls, error_message: str) -> "LLMResponseModel": - """创建错误响应""" - return cls( - thought=f"出现了问题:{error_message}", - expected_user_reaction="用户可能会感到困惑", - max_wait_seconds=60, - actions=[ActionModel(type="do_nothing")], - ) - - -@dataclass -class ContinuousThinkingResult: - """ - 连续思考结果 - - 在等待期间触发的心理活动更新结果 - """ - thought: str - anxiety_level: float - should_follow_up: bool = False - follow_up_message: str = "" - - def to_dict(self) -> dict[str, Any]: - """转换为字典格式""" - return { - "thought": self.thought, - "anxiety_level": self.anxiety_level, - "should_follow_up": self.should_follow_up, - "follow_up_message": self.follow_up_message, - } diff --git a/src/plugins/built_in/kokoro_flow_chatter/plugin.py b/src/plugins/built_in/kokoro_flow_chatter/plugin.py deleted file mode 100644 index 09cfb884c..000000000 --- a/src/plugins/built_in/kokoro_flow_chatter/plugin.py +++ /dev/null @@ -1,218 +0,0 @@ -""" -Kokoro Flow Chatter (心流聊天器) 插件入口 - -这是一个专为私聊场景设计的AI聊天插件,实现从"消息响应者"到"对话体验者"的转变。 - -核心特点: -- 心理状态驱动的交互模型 -- 连续的时间观念和等待体验 -- 深度情感连接和长期关系维护 -- 状态机驱动的交互节奏 - -切换逻辑: -- 当 enable = true 时,KFC 接管所有私聊消息 -- 当 enable = false 时,私聊消息由 AFC (Affinity Flow Chatter) 处理 -""" - -import asyncio -from typing import Any, ClassVar - -from src.common.logger import get_logger -from src.config.config import global_config -from src.plugin_system.apis.plugin_register_api import register_plugin -from src.plugin_system.base.base_plugin import BasePlugin -from src.plugin_system.base.component_types import ComponentInfo - -logger = get_logger("kokoro_flow_chatter_plugin") - - -@register_plugin -class KokoroFlowChatterPlugin(BasePlugin): - """ - 心流聊天器插件 - - 专为私聊场景设计的深度情感交互处理器。 - - Features: - - KokoroFlowChatter: 核心聊天处理器组件 - - SessionManager: 会话管理,支持持久化 - - BackgroundScheduler: 后台调度,处理等待超时 - - PromptGenerator: 动态提示词生成 - - ActionExecutor: 动作解析和执行 - """ - - plugin_name: str = "kokoro_flow_chatter" - enable_plugin: bool = True - dependencies: ClassVar[list[str]] = [] - python_dependencies: ClassVar[list[str]] = [] - config_file_name: str = "config.toml" - - # 配置schema留空,使用config.toml直接配置 - config_schema: ClassVar[dict[str, Any]] = {} - - # 后台任务 - _session_manager = None - _scheduler = None - _initialization_task = None - - def get_plugin_components(self) -> list[tuple[ComponentInfo, type]]: - """ - 返回插件包含的组件列表 - - 根据 global_config.kokoro_flow_chatter.enable 决定是否注册 KFC。 - 如果 enable = false,返回空列表,私聊将由 AFC 处理。 - """ - components: list[tuple[ComponentInfo, type]] = [] - - # 检查是否启用 KFC - kfc_enabled = True - if global_config and hasattr(global_config, 'kokoro_flow_chatter'): - kfc_enabled = global_config.kokoro_flow_chatter.enable - - if not kfc_enabled: - logger.info("KFC 已禁用 (enable = false),私聊将由 AFC 处理") - return components - - try: - # 导入核心聊天处理器 - from .chatter import KokoroFlowChatter - - components.append(( - KokoroFlowChatter.get_chatter_info(), - KokoroFlowChatter - )) - logger.debug("成功加载 KokoroFlowChatter 组件,KFC 将接管私聊") - - except Exception as e: - logger.error(f"加载 KokoroFlowChatter 时出错: {e}") - - return components - - async def on_plugin_load(self) -> bool: - """ - 插件加载时的初始化逻辑 - - 如果 KFC 被禁用,跳过初始化。 - - Returns: - bool: 是否加载成功 - """ - # 检查是否启用 KFC - kfc_enabled = True - if global_config and hasattr(global_config, 'kokoro_flow_chatter'): - kfc_enabled = global_config.kokoro_flow_chatter.enable - - if not kfc_enabled: - logger.info("KFC 已禁用,跳过初始化") - self._is_started = False - return True - - try: - logger.info("正在初始化 Kokoro Flow Chatter 插件...") - - # 初始化会话管理器 - from .session_manager import initialize_session_manager - - session_config = self.config.get("kokoro_flow_chatter", {}).get("session", {}) - self._session_manager = await initialize_session_manager( - data_dir=session_config.get("data_dir", "data/kokoro_flow_chatter/sessions"), - max_session_age_days=session_config.get("max_session_age_days", 30), - auto_save_interval=session_config.get("auto_save_interval", 300), - ) - - # 初始化调度器 - from .kfc_scheduler_adapter import initialize_scheduler - - # 从 global_config 读取配置 - check_interval = 10.0 - if global_config and hasattr(global_config, 'kokoro_flow_chatter'): - # 使用简化后的配置结构 - pass # check_interval 保持默认值 - - self._scheduler = await initialize_scheduler( - check_interval=check_interval, - ) - - self._is_started = True - logger.info("Kokoro Flow Chatter 插件初始化完成") - return True - - except Exception as e: - logger.error(f"Kokoro Flow Chatter 插件初始化失败: {e}") - return False - - async def on_plugin_unload(self) -> bool: - """ - 插件卸载时的清理逻辑 - - Returns: - bool: 是否卸载成功 - """ - try: - logger.info("正在关闭 Kokoro Flow Chatter 插件...") - - # 停止调度器 - if self._scheduler: - from .kfc_scheduler_adapter import shutdown_scheduler - await shutdown_scheduler() - self._scheduler = None - - # 停止会话管理器 - if self._session_manager: - await self._session_manager.stop() - self._session_manager = None - - self._is_started = False - logger.info("Kokoro Flow Chatter 插件已关闭") - return True - - except Exception as e: - logger.error(f"Kokoro Flow Chatter 插件关闭失败: {e}") - return False - - def register_plugin(self) -> bool: - """ - 注册插件及其所有组件 - - 重写父类方法,添加异步初始化逻辑 - """ - # 先调用父类的注册逻辑 - result = super().register_plugin() - - if result: - # 在后台启动异步初始化 - try: - loop = asyncio.get_event_loop() - if loop.is_running(): - self._initialization_task = asyncio.create_task( - self.on_plugin_load() - ) - else: - # 如果事件循环未运行,稍后初始化 - logger.debug("事件循环未运行,将延迟初始化") - except RuntimeError: - logger.debug("无法获取事件循环,将延迟初始化") - - return result - - @property - def is_started(self) -> bool: - """插件是否已启动""" - return self._is_started - - def get_plugin_stats(self) -> dict[str, Any]: - """获取插件统计信息""" - stats: dict[str, Any] = { - "is_started": self._is_started, - "has_session_manager": self._session_manager is not None, - "has_scheduler": self._scheduler is not None, - } - - if self._scheduler: - stats["scheduler_stats"] = self._scheduler.get_stats() - - if self._session_manager: - # 异步获取会话统计需要在异步上下文中调用 - stats["session_manager_active"] = True - - return stats diff --git a/src/plugins/built_in/kokoro_flow_chatter/proactive_thinking.py b/src/plugins/built_in/kokoro_flow_chatter/proactive_thinking.py deleted file mode 100644 index 5b3b745b5..000000000 --- a/src/plugins/built_in/kokoro_flow_chatter/proactive_thinking.py +++ /dev/null @@ -1,528 +0,0 @@ -""" -Kokoro Flow Chatter 主动思考引擎 (V2) - -私聊专属的主动思考系统,实现"主动找话题、主动关心用户"的能力。 -这是KFC区别于AFC的核心特性之一。 - -触发机制: -1. 长时间沉默检测 - 当对话沉默超过阈值时主动发起话题 -2. 关键记忆触发 - 基于重要日期、事件的主动关心 -3. 情绪状态触发 - 当情感参数达到阈值时主动表达 -4. 好感度驱动 - 根据与用户的关系深度调整主动程度 - -设计理念: -- 不是"有事才找你",而是"想你了就找你" -- 主动思考应该符合人设和情感状态 -- 避免过度打扰,保持适度的边界感 -""" - -import asyncio -import random -import time -from dataclasses import dataclass, field -from enum import Enum -from typing import TYPE_CHECKING, Any, Callable, Optional - -from src.common.logger import get_logger -from src.config.config import global_config -from src.plugin_system.base.component_types import ActionInfo - -from .models import KokoroSession, MentalLogEntry, MentalLogEventType, SessionStatus - -if TYPE_CHECKING: - from .action_executor import ActionExecutor - from .prompt_generator import PromptGenerator - -logger = get_logger("kokoro_proactive_thinking") - - -class ProactiveThinkingTrigger(Enum): - """主动思考触发类型""" - SILENCE_TIMEOUT = "silence_timeout" # 长时间沉默 - 她感到挂念 - TIME_BASED = "time_based" # 时间触发(早安/晚安)- 自然的问候契机 - - -@dataclass -class ProactiveThinkingConfig: - """ - 主动思考配置 - - 设计哲学:主动行为源于内部状态和外部环境的自然反应,而非机械的限制。 - 她的主动是因为挂念、因为关心、因为想问候,而不是因为"任务"。 - """ - # 是否启用主动思考 - enabled: bool = True - - # 1. 沉默触发器:当感到长久的沉默时,她可能会想说些什么 - silence_threshold_seconds: int = 7200 # 2小时无互动触发 - silence_check_interval: int = 300 # 每5分钟检查一次 - - # 2. 关系门槛:她不会对不熟悉的人过于主动 - min_affinity_for_proactive: float = 0.3 # 最低好感度才会主动 - - # 3. 频率呼吸:为了避免打扰,她的关心总是有间隔的 - min_interval_between_proactive: int = 1800 # 两次主动思考至少间隔30分钟 - - # 4. 自然问候:在特定的时间,她会像朋友一样送上问候 - enable_morning_greeting: bool = True # 早安问候 (8:00-9:00) - enable_night_greeting: bool = True # 晚安问候 (22:00-23:00) - - # 随机性(让行为更自然) - random_delay_range: tuple[int, int] = (60, 300) # 触发后随机延迟1-5分钟 - - @classmethod - def from_global_config(cls) -> "ProactiveThinkingConfig": - """从 global_config.kokoro_flow_chatter.proactive_thinking 创建配置""" - if global_config and hasattr(global_config, 'kokoro_flow_chatter'): - kfc = global_config.kokoro_flow_chatter - proactive = kfc.proactive_thinking - return cls( - enabled=proactive.enabled, - silence_threshold_seconds=proactive.silence_threshold_seconds, - silence_check_interval=300, # 固定值 - min_affinity_for_proactive=proactive.min_affinity_for_proactive, - min_interval_between_proactive=proactive.min_interval_between_proactive, - enable_morning_greeting=proactive.enable_morning_greeting, - enable_night_greeting=proactive.enable_night_greeting, - random_delay_range=(60, 300), # 固定值 - ) - return cls() - - -@dataclass -class ProactiveThinkingState: - """主动思考状态 - 记录她的主动关心历史""" - last_proactive_time: float = 0.0 - last_morning_greeting_date: str = "" # 上次早安的日期 - last_night_greeting_date: str = "" # 上次晚安的日期 - pending_triggers: list[ProactiveThinkingTrigger] = field(default_factory=list) - - def can_trigger(self, config: ProactiveThinkingConfig) -> bool: - """ - 检查是否满足主动思考的基本条件 - - 注意:这里不使用每日限制,而是基于间隔来自然控制频率 - """ - # 检查间隔限制 - 她的关心有呼吸感,不会太频繁 - if time.time() - self.last_proactive_time < config.min_interval_between_proactive: - return False - - return True - - def record_trigger(self) -> None: - """记录一次触发""" - self.last_proactive_time = time.time() - - def record_morning_greeting(self) -> None: - """记录今天的早安""" - self.last_morning_greeting_date = time.strftime("%Y-%m-%d") - self.record_trigger() - - def record_night_greeting(self) -> None: - """记录今天的晚安""" - self.last_night_greeting_date = time.strftime("%Y-%m-%d") - self.record_trigger() - - def has_greeted_morning_today(self) -> bool: - """今天是否已经问候过早安""" - return self.last_morning_greeting_date == time.strftime("%Y-%m-%d") - - def has_greeted_night_today(self) -> bool: - """今天是否已经问候过晚安""" - return self.last_night_greeting_date == time.strftime("%Y-%m-%d") - - -class ProactiveThinkingEngine: - """ - 主动思考引擎 - - 负责检测触发条件并生成主动思考内容。 - 这是一个"内在动机驱动"而非"机械限制"的系统。 - - 她的主动源于: - - 长时间的沉默让她感到挂念 - - 与用户的好感度决定了她愿意多主动 - - 特定的时间点给了她自然的问候契机 - """ - - def __init__( - self, - stream_id: str, - config: ProactiveThinkingConfig | None = None, - ): - """ - 初始化主动思考引擎 - - Args: - stream_id: 聊天流ID - config: 配置对象 - """ - self.stream_id = stream_id - self.config = config or ProactiveThinkingConfig() - self.state = ProactiveThinkingState() - - # 回调函数 - self._on_proactive_trigger: Optional[Callable] = None - - # 后台任务 - self._check_task: Optional[asyncio.Task] = None - self._running = False - - logger.debug(f"[ProactiveThinking] 初始化完成: stream_id={stream_id}") - - def set_proactive_callback( - self, - callback: Callable[[KokoroSession, ProactiveThinkingTrigger], Any] - ) -> None: - """ - 设置主动思考触发回调 - - Args: - callback: 当触发主动思考时调用的函数 - """ - self._on_proactive_trigger = callback - - async def start(self) -> None: - """启动主动思考引擎""" - if self._running: - return - - self._running = True - self._check_task = asyncio.create_task(self._check_loop()) - logger.info(f"[ProactiveThinking] 引擎已启动: stream_id={self.stream_id}") - - async def stop(self) -> None: - """停止主动思考引擎""" - self._running = False - - if self._check_task: - self._check_task.cancel() - try: - await self._check_task - except asyncio.CancelledError: - pass - self._check_task = None - - logger.info(f"[ProactiveThinking] 引擎已停止: stream_id={self.stream_id}") - - async def _check_loop(self) -> None: - """后台检查循环""" - while self._running: - try: - await asyncio.sleep(self.config.silence_check_interval) - - if not self.config.enabled: - continue - - # 这里需要获取session来检查,但我们在引擎层面不直接持有session - # 实际的检查逻辑通过 check_triggers 方法被外部调用 - - except asyncio.CancelledError: - break - except Exception as e: - logger.error(f"[ProactiveThinking] 检查循环出错: {e}") - - async def check_triggers( - self, - session: KokoroSession, - ) -> Optional[ProactiveThinkingTrigger]: - """ - 检查触发条件 - 基于内在动机而非机械限制 - - 综合考虑: - 1. 她与用户的好感度是否足够(关系门槛) - 2. 距离上次主动是否有足够间隔(频率呼吸) - 3. 是否有自然的触发契机(沉默/时间问候) - - Args: - session: 当前会话 - - Returns: - 触发类型,如果没有触发则返回None - """ - if not self.config.enabled: - return None - - # 关系门槛:她不会对不熟悉的人过于主动 - relationship_warmth = session.emotional_state.relationship_warmth - if relationship_warmth < self.config.min_affinity_for_proactive: - logger.debug( - f"[ProactiveThinking] 好感度不足,不主动: " - f"{relationship_warmth:.2f} < {self.config.min_affinity_for_proactive}" - ) - return None - - # 频率呼吸:检查间隔 - if not self.state.can_trigger(self.config): - return None - - # 只有在 IDLE 或 WAITING 状态才考虑主动 - if session.status not in (SessionStatus.IDLE, SessionStatus.WAITING): - return None - - # 按优先级检查触发契机 - - # 1. 时间问候(早安/晚安)- 自然的问候契机 - trigger = self._check_time_greeting_trigger() - if trigger: - return trigger - - # 2. 沉默触发 - 她感到挂念 - trigger = self._check_silence_trigger(session) - if trigger: - return trigger - - return None - - def _check_time_greeting_trigger(self) -> Optional[ProactiveThinkingTrigger]: - """检查时间问候触发(早安/晚安)""" - current_hour = time.localtime().tm_hour - - # 早安问候 (8:00 - 9:00) - if self.config.enable_morning_greeting: - if 8 <= current_hour < 9 and not self.state.has_greeted_morning_today(): - logger.debug("[ProactiveThinking] 早安问候时间") - return ProactiveThinkingTrigger.TIME_BASED - - # 晚安问候 (22:00 - 23:00) - if self.config.enable_night_greeting: - if 22 <= current_hour < 23 and not self.state.has_greeted_night_today(): - logger.debug("[ProactiveThinking] 晚安问候时间") - return ProactiveThinkingTrigger.TIME_BASED - - return None - - def _check_silence_trigger( - self, - session: KokoroSession, - ) -> Optional[ProactiveThinkingTrigger]: - """检查沉默触发 - 长时间的沉默让她感到挂念""" - # 获取最后互动时间 - last_interaction = session.waiting_since or session.last_activity_at - if not last_interaction: - # 使用session创建时间 - last_interaction = session.mental_log[0].timestamp if session.mental_log else time.time() - - silence_duration = time.time() - last_interaction - - if silence_duration >= self.config.silence_threshold_seconds: - logger.debug(f"[ProactiveThinking] 沉默触发: 已沉默 {silence_duration:.0f} 秒,她感到挂念") - return ProactiveThinkingTrigger.SILENCE_TIMEOUT - - return None - - async def generate_proactive_prompt( - self, - session: KokoroSession, - trigger: ProactiveThinkingTrigger, - prompt_generator: "PromptGenerator", - available_actions: dict[str, ActionInfo] | None = None, - ) -> tuple[str, str]: - """ - 生成主动思考的提示词 - - Args: - session: 当前会话 - trigger: 触发类型 - prompt_generator: 提示词生成器 - available_actions: 可用动作 - - Returns: - (system_prompt, user_prompt) 元组 - """ - # 根据触发类型生成上下文 - trigger_context = self._build_trigger_context(session, trigger) - - # 使用prompt_generator生成主动思考提示词 - system_prompt, user_prompt = prompt_generator.generate_proactive_thinking_prompt( - session=session, - trigger_type=trigger.value, - trigger_context=trigger_context, - available_actions=available_actions, - ) - - return system_prompt, user_prompt - - def _build_trigger_context( - self, - session: KokoroSession, - trigger: ProactiveThinkingTrigger, - ) -> str: - """ - 构建触发上下文 - 描述她主动联系的内在动机 - """ - emotional_state = session.emotional_state - current_hour = time.localtime().tm_hour - - if trigger == ProactiveThinkingTrigger.TIME_BASED: - # 时间问候 - 自然的问候契机 - if 8 <= current_hour < 12: - return ( - f"早上好!新的一天开始了。" - f"我的心情是「{emotional_state.mood}」。" - f"我想和对方打个招呼,开启美好的一天。" - ) - else: - return ( - f"夜深了,已经{current_hour}点了。" - f"我的心情是「{emotional_state.mood}」。" - f"我想关心一下对方,送上晚安。" - ) - - else: # SILENCE_TIMEOUT - # 沉默触发 - 她感到挂念 - last_time = session.waiting_since or session.last_activity_at or time.time() - silence_hours = (time.time() - last_time) / 3600 - return ( - f"我们已经有 {silence_hours:.1f} 小时没有聊天了。" - f"我有些挂念对方。" - f"我现在的心情是「{emotional_state.mood}」。" - f"对方给我的印象是:{emotional_state.impression_of_user or '还不太了解'}" - ) - - async def execute_proactive_action( - self, - session: KokoroSession, - trigger: ProactiveThinkingTrigger, - action_executor: "ActionExecutor", - prompt_generator: "PromptGenerator", - llm_call: Callable[[str, str], Any], - ) -> dict[str, Any]: - """ - 执行主动思考流程 - - Args: - session: 当前会话 - trigger: 触发类型 - action_executor: 动作执行器 - prompt_generator: 提示词生成器 - llm_call: LLM调用函数(可以是同步或异步) - - Returns: - 执行结果 - """ - try: - # 1. 加载可用动作 - available_actions = await action_executor.load_actions() - - # 2. 生成提示词 - system_prompt, user_prompt = await self.generate_proactive_prompt( - session, trigger, prompt_generator, available_actions - ) - - # 3. 添加随机延迟(更自然) - delay = random.randint(*self.config.random_delay_range) - logger.debug(f"[ProactiveThinking] 延迟 {delay} 秒后执行") - await asyncio.sleep(delay) - - # 4. 调用LLM(支持同步和异步) - result = llm_call(system_prompt, user_prompt) - if asyncio.iscoroutine(result): - llm_response = await result - else: - llm_response = result - - # 5. 解析响应 - parsed_response = action_executor.parse_llm_response(llm_response) - - # 6. 记录主动思考事件 - entry = MentalLogEntry( - event_type=MentalLogEventType.CONTINUOUS_THINKING, - timestamp=time.time(), - thought=f"[主动思考-{trigger.value}] {parsed_response.thought}", - content="", - emotional_snapshot=session.emotional_state.to_dict(), - metadata={ - "trigger_type": trigger.value, - "proactive": True, - }, - ) - session.add_mental_log_entry(entry) - - # 7. 执行动作 - from src.chat.message_receive.chat_stream import get_chat_manager - chat_manager = get_chat_manager() - chat_stream = await chat_manager.get_stream(self.stream_id) if chat_manager else None - - result = await action_executor.execute_actions( - parsed_response, - session, - chat_stream - ) - - # 8. 记录触发(根据触发类型决定记录方式) - if trigger == ProactiveThinkingTrigger.TIME_BASED: - # 时间问候需要单独记录,防止同一天重复问候 - current_hour = time.localtime().tm_hour - if 6 <= current_hour < 12: - self.state.record_morning_greeting() - else: - self.state.record_night_greeting() - else: - self.state.record_trigger() - - # 9. 如果发送了消息,更新会话状态 - if result.get("has_reply"): - session.start_waiting( - expected_reaction=parsed_response.expected_user_reaction, - max_wait=parsed_response.max_wait_seconds - ) - - return { - "success": True, - "trigger": trigger.value, - "result": result, - } - - except Exception as e: - logger.error(f"[ProactiveThinking] 执行失败: {e}") - import traceback - logger.error(traceback.format_exc()) - return { - "success": False, - "trigger": trigger.value, - "error": str(e), - } - - def get_state(self) -> dict[str, Any]: - """获取当前状态""" - return { - "enabled": self.config.enabled, - "last_proactive_time": self.state.last_proactive_time, - "last_morning_greeting_date": self.state.last_morning_greeting_date, - "last_night_greeting_date": self.state.last_night_greeting_date, - "running": self._running, - } - - -# 全局引擎实例管理 -_engines: dict[str, ProactiveThinkingEngine] = {} - - -def get_proactive_thinking_engine( - stream_id: str, - config: ProactiveThinkingConfig | None = None, -) -> ProactiveThinkingEngine: - """ - 获取主动思考引擎实例 - - Args: - stream_id: 聊天流ID - config: 配置对象(如果为None,则从global_config加载) - - Returns: - ProactiveThinkingEngine实例 - """ - if stream_id not in _engines: - # 如果没有提供config,从global_config加载 - if config is None: - config = ProactiveThinkingConfig.from_global_config() - _engines[stream_id] = ProactiveThinkingEngine(stream_id, config) - return _engines[stream_id] - - -async def cleanup_engines() -> None: - """清理所有引擎实例""" - for engine in _engines.values(): - await engine.stop() - _engines.clear() diff --git a/src/plugins/built_in/kokoro_flow_chatter/prompt_generator.py b/src/plugins/built_in/kokoro_flow_chatter/prompt_generator.py deleted file mode 100644 index 14e323e90..000000000 --- a/src/plugins/built_in/kokoro_flow_chatter/prompt_generator.py +++ /dev/null @@ -1,807 +0,0 @@ -""" -Kokoro Flow Chatter Prompt生成器 - -根据会话状态动态构建LLM提示词,实现"体验-决策-行动"的交互模式。 -支持两种主要场景: -1. 回应消息(Responding):收到用户消息后的决策 -2. 超时决策(Timeout Decision):等待超时后的后续行动决策 - -V2 升级: -- 动态Action发现机制:从ActionManager获取可用Action列表 -- 支持任意复杂参数的Action -- 与AFC的Action系统完全兼容 - -V5 超融合架构升级: -- 完整集成S4U的所有上下文模块 -- 关系信息、记忆、表达习惯等全部融入 -- 支持群聊/私聊场景动态切换 -""" - -import asyncio -import time -from typing import TYPE_CHECKING, Any, Optional - -from src.common.logger import get_logger -from src.plugin_system.base.component_types import ActionInfo - -from .models import ( - EmotionalState, - KokoroSession, - MentalLogEntry, - MentalLogEventType, -) - -if TYPE_CHECKING: - from src.chat.message_receive.chat_stream import ChatStream - from src.chat.planner_actions.action_manager import ChatterActionManager - from src.common.data_models.message_manager_data_model import StreamContext - -logger = get_logger("kokoro_prompt_generator") - - -class PromptGenerator: - """ - Kokoro Flow Chatter 提示词生成器 - - 负责根据会话状态和场景生成不同的LLM提示词, - 引导AI以"体验者"的身份参与对话。 - - V2 升级:支持动态Action发现,与AFC的Action系统完全兼容 - V4 升级:完整集成用户人设、世界观、表达风格和规则 - V5 超融合架构:集成S4U所有上下文模块(关系、记忆、表达习惯等) - """ - - # ========== 超融合架构 SYSTEM_PROMPT_TEMPLATE V7 ========== - # 简洁自然版,参考 S4U 风格 - SYSTEM_PROMPT_TEMPLATE = """你不是 AI,你是一个真实的人,有自己的生活、想法和情绪。 - -## 1. 你是谁 -{personality_core} - -{personality_side} - -{identity} - -{background_story} - -## 2. 规则 -{safety_guidelines} - -- 说话要自然,像平时发微信一样 -- 有自己的边界感,不用无条件迎合 -- 称呼对方用自然的昵称,别念复杂的用户名 - -## 3. 现在的情况 -**时间**: {current_time} -**场景**: {chat_scene} -{schedule_block} - -你现在的心情:{mood} -你对对方的印象:{impression_of_user} - -## 4. 你和对方的关系 -{relation_info_block} - -{memory_block} - -## 5. 你能做的事 -{available_actions_block} - -## 6. 怎么回复 -{reply_style} - -{expression_habits_block} - -### 输出格式(JSON) -```json -{{ - "thought": "你在想什么", - "expected_user_reaction": "你觉得对方会怎么回应", - "max_wait_seconds": 等多久(60-900,不等就填0), - "actions": [ - {{"type": "reply", "content": "你要说的话"}} - ] -}} -``` - -不想做任何事就用 `{{"type": "do_nothing"}}`""" - - # 回应消息场景的用户提示词模板(V7: 支持多条消息) - RESPONDING_USER_PROMPT_TEMPLATE = """## 聊天记录 -{narrative_history} - -## 新消息 -{incoming_messages} - ---- -看完这些消息,你想怎么回应?用 JSON 输出你的想法和决策。""" - - # 超时决策场景的用户提示词模板(V7重构:简洁自然) - TIMEOUT_DECISION_USER_PROMPT_TEMPLATE = """## 聊天记录 -{narrative_history} - -## 现在的情况 -你发了消息,等了 {wait_duration_seconds:.0f} 秒({wait_duration_minutes:.1f} 分钟),对方还没回。 -你之前觉得对方可能会:{expected_user_reaction} - -{followup_warning} - -你发的最后一条:{last_bot_message} - ---- -你拿起手机看了一眼,发现对方还没回复。你想怎么办? - -选项: -1. **继续等** - 用 `do_nothing`,设个 `max_wait_seconds` 等一会儿再看 -2. **发消息** - 用 `reply`,不过别太频繁追问 -3. **算了不等了** - 用 `do_nothing`,`max_wait_seconds` 设为 0 - -用 JSON 输出你的想法和决策。""" - - # 连续思考场景的用户提示词模板 - CONTINUOUS_THINKING_USER_PROMPT_TEMPLATE = """## 聊天记录 -{narrative_history} - -## 现在的情况 -你在等对方回复,已经等了 {wait_duration_seconds:.0f} 秒。 -你之前觉得对方可能会:{expected_user_reaction} - -你发的最后一条:{last_bot_message} - ---- -等待的时候你在想什么?用 JSON 输出,`actions` 留空就行。""" - - # 主动思考场景的用户提示词模板 - PROACTIVE_THINKING_USER_PROMPT_TEMPLATE = """## 聊天记录 -{narrative_history} - -## 现在的情况 -现在是 {current_time},距离你们上次聊天已经过了 {silence_duration}。 - -{relation_block} - -{trigger_context} - ---- -你突然想起了对方。要不要联系一下? - -说实话,不联系也完全没问题——不打扰也是一种温柔。 -如果决定联系,想好说什么,要自然一点。 - -用 JSON 输出你的想法和决策。不想发消息就用 `do_nothing`。""" - - def __init__(self, persona_description: str = ""): - """ - 初始化提示词生成器 - - Args: - persona_description: 人设描述 - """ - self.persona_description = persona_description or self._get_default_persona() - - def _get_default_persona(self) -> str: - """获取默认人设描述""" - return """你是一个温暖、真诚的对话伙伴。 -你有自己的情感和想法,会真实地表达内心的感受。 -你关心与用户的关系,但也保持健康的边界。 -你可以感到开心、好奇、担心、或者稍微沮丧,这些都是正常的。""" - - def set_persona(self, persona_description: str) -> None: - """设置人设描述""" - self.persona_description = persona_description - - def _format_emotional_state(self, state: EmotionalState) -> dict[str, str | float]: - """格式化情感状态用于模板替换""" - return { - "mood": state.mood, - "mood_intensity": state.mood_intensity, - "relationship_warmth": state.relationship_warmth, - "impression_of_user": state.impression_of_user or "还没有形成明确的印象", - "anxiety_level": state.anxiety_level, - "engagement_level": state.engagement_level, - } - - def _format_narrative_history( - self, - mental_log: list[MentalLogEntry], - max_entries: int = 15, - ) -> str: - """ - 将心理活动日志格式化为叙事历史 - - Args: - mental_log: 心理活动日志列表 - max_entries: 最大条目数 - - Returns: - str: 格式化的叙事历史文本 - """ - if not mental_log: - return "(这是对话的开始,还没有历史记录)" - - # 获取最近的日志条目 - recent_entries = mental_log[-max_entries:] - - narrative_parts = [] - for entry in recent_entries: - timestamp_str = time.strftime( - "%Y-%m-%d %H:%M:%S", - time.localtime(entry.timestamp) - ) - - if entry.event_type == MentalLogEventType.USER_MESSAGE: - narrative_parts.append( - f"[{timestamp_str}] 用户说:{entry.content}" - ) - elif entry.event_type == MentalLogEventType.BOT_ACTION: - if entry.thought: - narrative_parts.append( - f"[{timestamp_str}] (你的内心:{entry.thought})" - ) - if entry.content: - narrative_parts.append( - f"[{timestamp_str}] 你回复:{entry.content}" - ) - elif entry.event_type == MentalLogEventType.WAITING_UPDATE: - if entry.thought: - narrative_parts.append( - f"[{timestamp_str}] (等待中的想法:{entry.thought})" - ) - elif entry.event_type == MentalLogEventType.CONTINUOUS_THINKING: - if entry.thought: - narrative_parts.append( - f"[{timestamp_str}] (思绪飘过:{entry.thought})" - ) - elif entry.event_type == MentalLogEventType.STATE_CHANGE: - if entry.content: - narrative_parts.append( - f"[{timestamp_str}] {entry.content}" - ) - - return "\n".join(narrative_parts) - - def _format_history_from_context( - self, - context: "StreamContext", - mental_log: list[MentalLogEntry] | None = None, - ) -> str: - """ - 从 StreamContext 的历史消息构建叙事历史 - - 这是实现"无缝融入"的关键: - - 从同一个数据库读取历史消息(与AFC共享) - - 遵循全局配置 [chat].max_context_size - - 将消息渲染成KFC的叙事体格式 - - Args: - context: 聊天流上下文,包含共享的历史消息 - mental_log: 可选的心理活动日志,用于补充内心独白 - - Returns: - str: 格式化的叙事历史文本 - """ - from src.config.config import global_config - - # 从 StreamContext 获取历史消息,遵循全局上下文长度配置 - max_context = 25 # 默认值 - if global_config and hasattr(global_config, 'chat') and global_config.chat: - max_context = getattr(global_config.chat, "max_context_size", 25) - history_messages = context.get_messages(limit=max_context, include_unread=False) - - if not history_messages and not mental_log: - return "(这是对话的开始,还没有历史记录)" - - # 获取Bot的用户ID用于判断消息来源 - bot_user_id = None - if global_config and hasattr(global_config, 'bot') and global_config.bot: - bot_user_id = str(getattr(global_config.bot, 'qq_account', '')) - - narrative_parts = [] - - # 首先,将数据库历史消息转换为叙事格式 - for msg in history_messages: - timestamp_str = time.strftime( - "%Y-%m-%d %H:%M:%S", - time.localtime(msg.time or time.time()) - ) - - # 判断是用户消息还是Bot消息 - msg_user_id = str(msg.user_info.user_id) if msg.user_info else "" - is_bot_message = bot_user_id and msg_user_id == bot_user_id - content = msg.processed_plain_text or msg.display_message or "" - - if is_bot_message: - narrative_parts.append(f"[{timestamp_str}] 你回复:{content}") - else: - sender_name = msg.user_info.user_nickname if msg.user_info else "用户" - narrative_parts.append(f"[{timestamp_str}] {sender_name}说:{content}") - - # 然后,补充 mental_log 中的内心独白(如果有) - if mental_log: - for entry in mental_log[-5:]: # 只取最近5条心理活动 - timestamp_str = time.strftime( - "%Y-%m-%d %H:%M:%S", - time.localtime(entry.timestamp) - ) - - if entry.event_type == MentalLogEventType.BOT_ACTION and entry.thought: - narrative_parts.append(f"[{timestamp_str}] (你的内心:{entry.thought})") - elif entry.event_type == MentalLogEventType.CONTINUOUS_THINKING and entry.thought: - narrative_parts.append(f"[{timestamp_str}] (思绪飘过:{entry.thought})") - - return "\n".join(narrative_parts) - - def _format_available_actions( - self, - available_actions: dict[str, ActionInfo], - ) -> str: - """ - 格式化可用动作列表为提示词块 - - Args: - available_actions: 可用动作字典 {动作名: ActionInfo} - - Returns: - str: 格式化的动作描述文本 - """ - if not available_actions: - # 使用默认的内置动作 - return self._get_default_actions_block() - - action_blocks = [] - - for action_name, action_info in available_actions.items(): - # 构建动作描述 - description = action_info.description or f"执行 {action_name} 动作" - - # 构建参数说明 - params_lines = [] - if action_info.action_parameters: - for param_name, param_desc in action_info.action_parameters.items(): - params_lines.append(f' - `{param_name}`: {param_desc}') - - # 构建使用场景 - require_lines = [] - if action_info.action_require: - for req in action_info.action_require: - require_lines.append(f" - {req}") - - # 组装动作块 - action_block = f"""### `{action_name}` -**描述**: {description}""" - - if params_lines: - action_block += f""" -**参数**: -{chr(10).join(params_lines)}""" - else: - action_block += "\n**参数**: 无" - - if require_lines: - action_block += f""" -**使用场景**: -{chr(10).join(require_lines)}""" - - # 添加示例 - example_params = {} - if action_info.action_parameters: - for param_name, param_desc in action_info.action_parameters.items(): - example_params[param_name] = f"<{param_desc}>" - - import orjson - params_json = orjson.dumps(example_params, option=orjson.OPT_INDENT_2).decode('utf-8') if example_params else "{}" - action_block += f""" -**示例**: -```json -{{ - "type": "{action_name}", - "reason": "选择这个动作的原因", - {params_json[1:-1] if params_json != '{}' else ''} -}} -```""" - - action_blocks.append(action_block) - - return "\n\n".join(action_blocks) - - def _get_default_actions_block(self) -> str: - """获取默认的内置动作描述块""" - return """### `reply` -**描述**: 发送文字回复给用户 -**参数**: - - `content`: 回复的文字内容(必须) -**示例**: -```json -{"type": "reply", "content": "你好呀!今天过得怎么样?"} -``` - -### `poke_user` -**描述**: 戳一戳用户,轻量级互动 -**参数**: 无 -**示例**: -```json -{"type": "poke_user", "reason": "想逗逗他"} -``` - -### `update_internal_state` -**描述**: 更新你的内部情感状态 -**重要约束**: -⚠️ **情绪变化必须有明确的上下文理由**: - - 只有当聊天内容中有明确触发情绪变化的事件时才更新情绪 - - 禁止无缘无故地变成负面情绪(如低落、沮丧、难过等) - - 情绪应该保持相对稳定,除非聊天中发生了真正影响情绪的事情 - - 默认保持"平静"或当前情绪即可,不需要每次都更新 - -**参数**: - - `mood`: 当前心情(如"平静"、"开心"、"好奇"等,避免负面情绪除非有明确理由) - - `mood_intensity`: 心情强度(0.0-1.0,变化幅度不宜过大) - - `impression_of_user`: 对用户的印象描述 - - `engagement_level`: 投入程度(0.0-1.0) -**示例**: -```json -{"type": "update_internal_state", "mood": "开心", "mood_intensity": 0.6, "reason": "对方分享了有趣的事情"} -``` - -### `do_nothing` -**描述**: 明确表示"思考后决定不作回应" -**参数**: 无 -**示例**: -```json -{"type": "do_nothing", "reason": "现在不是说话的好时机"} -```""" - - def generate_system_prompt( - self, - session: KokoroSession, - available_actions: Optional[dict[str, ActionInfo]] = None, - context_data: Optional[dict[str, str]] = None, - chat_stream: Optional["ChatStream"] = None, - ) -> str: - """ - 生成系统提示词 - - V6模块化升级:使用 prompt_modules 构建模块化的提示词 - - 每个模块独立构建,职责清晰 - - 回复相关(人设、上下文)与动作定义分离 - - Args: - session: 当前会话 - available_actions: 可用动作字典,如果为None则使用默认动作 - context_data: S4U上下文数据字典(包含relation_info, memory_block等) - chat_stream: 聊天流(用于判断群聊/私聊场景) - - Returns: - str: 系统提示词 - """ - from .prompt_modules import build_system_prompt - - return build_system_prompt( - session=session, - available_actions=available_actions, - context_data=context_data, - chat_stream=chat_stream, - ) - - def generate_responding_prompt( - self, - session: KokoroSession, - message_content: str, - sender_name: str, - sender_id: str, - message_time: Optional[float] = None, - available_actions: Optional[dict[str, ActionInfo]] = None, - context: Optional["StreamContext"] = None, - context_data: Optional[dict[str, str]] = None, - chat_stream: Optional["ChatStream"] = None, - all_unread_messages: Optional[list] = None, # V7: 支持多条消息 - ) -> tuple[str, str]: - """ - 生成回应消息场景的提示词 - - V3 升级:支持从 StreamContext 读取共享的历史消息 - V5 超融合:集成S4U所有上下文模块 - V7 升级:支持多条消息(打断机制合并处理pending消息) - - Args: - session: 当前会话 - message_content: 收到的主消息内容(兼容旧调用方式) - sender_name: 发送者名称 - sender_id: 发送者ID - message_time: 消息时间戳 - available_actions: 可用动作字典 - context: 聊天流上下文(可选),用于读取共享的历史消息 - context_data: S4U上下文数据字典(包含relation_info, memory_block等) - chat_stream: 聊天流(用于判断群聊/私聊场景) - all_unread_messages: 所有未读消息列表(V7新增,包含pending消息) - - Returns: - tuple[str, str]: (系统提示词, 用户提示词) - """ - system_prompt = self.generate_system_prompt( - session, - available_actions, - context_data=context_data, - chat_stream=chat_stream, - ) - - # V3: 优先从 StreamContext 读取历史(与AFC共享同一数据源) - if context: - narrative_history = self._format_history_from_context(context, session.mental_log) - else: - # 回退到仅使用 mental_log(兼容旧调用方式) - narrative_history = self._format_narrative_history(session.mental_log) - - # V7: 格式化收到的消息(支持多条) - incoming_messages = self._format_incoming_messages( - message_content=message_content, - sender_name=sender_name, - sender_id=sender_id, - message_time=message_time, - all_unread_messages=all_unread_messages, - ) - - user_prompt = self.RESPONDING_USER_PROMPT_TEMPLATE.format( - narrative_history=narrative_history, - incoming_messages=incoming_messages, - ) - - return system_prompt, user_prompt - - def _format_incoming_messages( - self, - message_content: str, - sender_name: str, - sender_id: str, - message_time: Optional[float] = None, - all_unread_messages: Optional[list] = None, - ) -> str: - """ - 格式化收到的消息(V7新增) - - 支持单条消息(兼容旧调用)和多条消息(打断合并场景) - - Args: - message_content: 主消息内容 - sender_name: 发送者名称 - sender_id: 发送者ID - message_time: 消息时间戳 - all_unread_messages: 所有未读消息列表 - - Returns: - str: 格式化的消息文本 - """ - if message_time is None: - message_time = time.time() - - # 如果有多条消息,格式化为消息组 - if all_unread_messages and len(all_unread_messages) > 1: - lines = [f"**用户连续发送了 {len(all_unread_messages)} 条消息:**\n"] - - for i, msg in enumerate(all_unread_messages, 1): - msg_time = msg.time or time.time() - msg_time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(msg_time)) - msg_sender = msg.user_info.user_nickname if msg.user_info else sender_name - msg_content = msg.processed_plain_text or msg.display_message or "" - - lines.append(f"[{i}] 来自:{msg_sender}") - lines.append(f" 时间:{msg_time_str}") - lines.append(f" 内容:{msg_content}") - lines.append("") - - lines.append("**提示**:请综合理解这些消息的整体意图,不需要逐条回复。") - return "\n".join(lines) - - # 单条消息(兼容旧格式) - message_time_str = time.strftime( - "%Y-%m-%d %H:%M:%S", - time.localtime(message_time) - ) - return f"""来自:{sender_name}(用户ID: {sender_id}) -时间:{message_time_str} -内容:{message_content}""" - - def generate_timeout_decision_prompt( - self, - session: KokoroSession, - available_actions: Optional[dict[str, ActionInfo]] = None, - ) -> tuple[str, str]: - """ - 生成超时决策场景的提示词(V7:增加连续追问限制) - - Args: - session: 当前会话 - available_actions: 可用动作字典 - - Returns: - tuple[str, str]: (系统提示词, 用户提示词) - """ - system_prompt = self.generate_system_prompt(session, available_actions) - - narrative_history = self._format_narrative_history(session.mental_log) - - wait_duration = session.get_waiting_duration() - - # V7: 生成连续追问警告 - followup_count = session.consecutive_followup_count - max_followups = session.max_consecutive_followups - - if followup_count >= max_followups: - followup_warning = f"""⚠️ **重要提醒**: -你已经连续追问了 {followup_count} 次,对方都没有回复。 -**强烈建议不要再发消息了**——继续追问会显得很缠人、很不尊重对方的空间。 -对方可能真的在忙,或者暂时不想回复,这都是正常的。 -请选择 `do_nothing` 继续等待,或者直接结束对话(设置 `max_wait_seconds: 0`)。""" - elif followup_count > 0: - followup_warning = f"""📝 提示:这已经是你第 {followup_count + 1} 次等待对方回复了。 -如果对方持续没有回应,可能真的在忙或不方便,不需要急着追问。""" - else: - followup_warning = "" - - user_prompt = self.TIMEOUT_DECISION_USER_PROMPT_TEMPLATE.format( - narrative_history=narrative_history, - wait_duration_seconds=wait_duration, - wait_duration_minutes=wait_duration / 60, - expected_user_reaction=session.expected_user_reaction or "不确定", - followup_warning=followup_warning, - last_bot_message=session.last_bot_message or "(没有记录)", - ) - - return system_prompt, user_prompt - - def generate_continuous_thinking_prompt( - self, - session: KokoroSession, - available_actions: Optional[dict[str, ActionInfo]] = None, - ) -> tuple[str, str]: - """ - 生成连续思考场景的提示词 - - Args: - session: 当前会话 - available_actions: 可用动作字典 - - Returns: - tuple[str, str]: (系统提示词, 用户提示词) - """ - system_prompt = self.generate_system_prompt(session, available_actions) - - narrative_history = self._format_narrative_history( - session.mental_log, - max_entries=10 # 连续思考时使用较少的历史 - ) - - wait_duration = session.get_waiting_duration() - - user_prompt = self.CONTINUOUS_THINKING_USER_PROMPT_TEMPLATE.format( - narrative_history=narrative_history, - wait_duration_seconds=wait_duration, - wait_duration_minutes=wait_duration / 60, - max_wait_seconds=session.max_wait_seconds, - expected_user_reaction=session.expected_user_reaction or "不确定", - last_bot_message=session.last_bot_message or "(没有记录)", - ) - - return system_prompt, user_prompt - - def generate_proactive_thinking_prompt( - self, - session: KokoroSession, - 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_context: 触发上下文描述(如"沉默了2小时") - available_actions: 可用动作字典 - context_data: S4U上下文数据(包含全局关系信息) - chat_stream: 聊天流 - - Returns: - tuple[str, str]: (系统提示词, 用户提示词) - """ - 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, - current_time=current_time, - silence_duration=silence_duration, - relation_block=relation_block, - trigger_context=trigger_context, - ) - - return system_prompt, user_prompt - - def build_messages_for_llm( - self, - system_prompt: str, - user_prompt: str, - stream_id: str = "", - ) -> list[dict[str, str]]: - """ - 构建LLM请求的消息列表 - - Args: - system_prompt: 系统提示词 - user_prompt: 用户提示词 - stream_id: 聊天流ID(用于日志) - - Returns: - list[dict]: 消息列表 - """ - # INFO日志:打印完整的KFC提示词(可观测性增强) - full_prompt = f"[SYSTEM]\n{system_prompt}\n\n[USER]\n{user_prompt}" - logger.info( - f"Final KFC prompt constructed for stream {stream_id}:\n" - f"--- PROMPT START ---\n" - f"{full_prompt}\n" - f"--- PROMPT END ---" - ) - - return [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt}, - ] - - -# 全局提示词生成器实例 -_prompt_generator: Optional[PromptGenerator] = None - - -def get_prompt_generator(persona_description: str = "") -> PromptGenerator: - """获取全局提示词生成器实例""" - global _prompt_generator - if _prompt_generator is None: - _prompt_generator = PromptGenerator(persona_description) - return _prompt_generator - - -def set_prompt_generator_persona(persona_description: str) -> None: - """设置全局提示词生成器的人设""" - generator = get_prompt_generator() - generator.set_persona(persona_description) diff --git a/src/plugins/built_in/kokoro_flow_chatter/prompt_modules.py b/src/plugins/built_in/kokoro_flow_chatter/prompt_modules.py deleted file mode 100644 index 2b3e6ac84..000000000 --- a/src/plugins/built_in/kokoro_flow_chatter/prompt_modules.py +++ /dev/null @@ -1,369 +0,0 @@ -""" -Kokoro Flow Chatter 模块化提示词组件 - -将提示词拆分为独立的模块,每个模块负责特定的内容生成: -1. 核心身份模块 - 人设、人格、世界观 -2. 行为准则模块 - 规则、安全边界 -3. 情境上下文模块 - 时间、场景、关系、记忆 -4. 动作能力模块 - 可用动作的描述 -5. 输出格式模块 - JSON格式要求 - -设计理念: -- 每个模块只负责自己的部分,互不干扰 -- 回复相关内容(人设、上下文)与动作定义分离 -- 方便独立调试和优化每个部分 -""" - -from datetime import datetime -from typing import TYPE_CHECKING, Any, Optional - -import orjson - -from src.common.logger import get_logger -from src.config.config import global_config -from src.plugin_system.base.component_types import ActionInfo - -if TYPE_CHECKING: - from src.chat.message_receive.chat_stream import ChatStream - -from .models import EmotionalState, KokoroSession - -logger = get_logger("kfc_prompt_modules") - - -# ============================================================ -# 模块1: 核心身份 - 人设/人格/背景 -# ============================================================ - -def build_identity_module() -> str: - """ - 构建核心身份模块 - - 包含:人格核心、人格细节、外貌身份、世界观背景 - 这些是回复内容的基础,决定了"你是谁" - """ - if global_config is None: - raise RuntimeError("global_config 未初始化") - - personality_cfg = global_config.personality - - personality_core = personality_cfg.personality_core or "你是一个温暖、真诚的对话伙伴。" - personality_side = personality_cfg.personality_side or "" - identity = personality_cfg.identity or "" - background_story = personality_cfg.background_story or "" - - parts = ["## 1. 你是谁", personality_core] - - if personality_side: - parts.append(f"\n{personality_side}") - - if identity: - parts.append(f"\n{identity}") - - if background_story: - parts.append(f"\n### 背景故事\n{background_story}") - - return "\n".join(parts) - - -# ============================================================ -# 模块2: 行为准则 - 规则/边界/KFC特有准则 -# ============================================================ - -def build_rules_module() -> str: - """ - 构建行为准则模块 - - 包含:安全规则 + KFC 通用准则 - 这些是行为的边界,决定了"什么能做什么不能做" - """ - if global_config is None: - raise RuntimeError("global_config 未初始化") - - safety_guidelines = global_config.personality.safety_guidelines or [] - if isinstance(safety_guidelines, list): - safety_guidelines_str = "\n".join(f"- {rule}" for rule in safety_guidelines) - else: - safety_guidelines_str = str(safety_guidelines) - - # KFC 特有准则(简洁自然版) - kfc_rules = """### 你的行为原则 -- 你是真实的人,有自己的生活、情绪和想法 -- 说话自然、口语化,像发微信一样随意 -- 有自己的边界感,不会无条件迎合 -- 称呼对方时用自然的昵称或简称,别念复杂的用户名 - -### 格式注意 -- 聊天记录中的"[表情包:描述]"是系统识别转述,不是你发的 -- 想发表情包要用 emoji 动作,不要在回复里写"[表情包:xxx]"这种格式""" - - return f"""## 2. 行为准则 -{safety_guidelines_str} - -{kfc_rules}""" - - -# ============================================================ -# 模块3: 情境上下文 - 时间/场景/内在状态/关系/记忆 -# ============================================================ - -def build_context_module( - session: KokoroSession, - chat_stream: Optional["ChatStream"] = None, - context_data: Optional[dict[str, str]] = None, -) -> str: - """ - 构建情境上下文模块 - - 包含:当前时间、聊天场景、内在状态、关系信息、记忆 - 这些是回复的上下文,决定了"当前在什么情况下" - - Args: - session: 当前会话 - chat_stream: 聊天流(判断群聊/私聊) - context_data: S4U 上下文数据 - """ - context_data = context_data or {} - - # 时间和场景 - current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S") - is_group_chat = bool(chat_stream and chat_stream.group_info) - chat_scene = "你在群里聊天" if is_group_chat else "你在和对方私聊" - - # 日程(如果有)- 只是背景,不主动提及 - schedule_block = context_data.get("schedule", "") - - # 内在状态(简化版,更自然) - es = session.emotional_state - inner_state = f"""### 你现在的状态 -- 心情:{es.mood} -- 你们的关系:{es.impression_of_user or "刚认识,还在慢慢了解"} -- 你的投入度:{'聊得挺起劲' if es.engagement_level > 0.6 else '一般般' if es.engagement_level > 0.3 else '有点心不在焉'}""" - - # 关系信息 - relation_info = context_data.get("relation_info", "") - - # 记忆 - memory_block = context_data.get("memory_block", "") - - parts = [ - "## 3. 当前情境", - f"**现在是** {current_time}", - f"**{chat_scene}**", - ] - - # 日程是你的当前状态,但不要主动提及 - if schedule_block: - parts.append(f"""### 你的当前状态(重要:不要主动提及) -{schedule_block} -⚠️ 这是你当前正在做的事,作为你的背景状态存在。除非对方主动问你"在干嘛"、"忙不忙"等问题,否则**绝对不要在回复中主动提起**。就像真人聊天一样,没人会无缘无故说"我正在XX"。""") - - parts.append("") - parts.append(inner_state) - - if relation_info: - parts.append("") - parts.append("## 4. 你和对方的关系") - parts.append(relation_info) - - if memory_block: - parts.append("") - parts.append(memory_block) - - return "\n".join(parts) - - -# ============================================================ -# 模块4: 动作能力 - 可用动作的描述 -# ============================================================ - -def build_actions_module(available_actions: Optional[dict[str, ActionInfo]] = None) -> str: - """ - 构建动作能力模块 - - 包含:所有可用动作的描述、参数、示例 - 这部分与回复内容分离,只描述"能做什么" - - Args: - available_actions: 可用动作字典 - """ - if not available_actions: - actions_block = _get_default_actions_block() - else: - actions_block = _format_available_actions(available_actions) - - return f"""## 5. 你能做的事情 - -{actions_block}""" - - -def _format_available_actions(available_actions: dict[str, ActionInfo]) -> str: - """格式化可用动作列表(简洁版)""" - action_blocks = [] - - for action_name, action_info in available_actions.items(): - description = action_info.description or f"执行 {action_name}" - - # 构建动作块(简洁格式) - action_block = f"### `{action_name}` - {description}" - - # 参数说明(如果有) - if action_info.action_parameters: - params_lines = [f" - `{name}`: {desc}" for name, desc in action_info.action_parameters.items()] - action_block += f"\n参数:\n{chr(10).join(params_lines)}" - - # 使用场景(如果有) - if action_info.action_require: - require_lines = [f" - {req}" for req in action_info.action_require] - action_block += f"\n使用场景:\n{chr(10).join(require_lines)}" - - # 简洁示例 - example_params = "" - if action_info.action_parameters: - param_examples = [f'"{name}": "..."' for name in action_info.action_parameters.keys()] - example_params = ", " + ", ".join(param_examples) - - action_block += f'\n```json\n{{"type": "{action_name}"{example_params}}}\n```' - - action_blocks.append(action_block) - - return "\n\n".join(action_blocks) - - -def _get_default_actions_block() -> str: - """获取默认的内置动作描述块""" - return """### `reply` - 发消息 -发送文字回复 -```json -{"type": "reply", "content": "你要说的话"} -``` - -### `poke_user` - 戳一戳 -戳对方一下 -```json -{"type": "poke_user"} -``` - -### `update_internal_state` - 更新你的状态 -更新你的心情和对对方的印象 -```json -{"type": "update_internal_state", "mood": "开心", "impression_of_user": "挺有趣的人"} -``` - -### `do_nothing` - 不做任何事 -想了想,决定现在不说话 -```json -{"type": "do_nothing"} -```""" - - -# ============================================================ -# 模块5: 表达与输出格式 - 回复风格 + JSON格式 -# ============================================================ - -def build_output_module( - context_data: Optional[dict[str, str]] = None, -) -> str: - """ - 构建输出格式模块 - - 包含:表达风格、表达习惯、JSON 输出格式要求 - 这部分定义了"怎么说"和"输出什么格式" - - Args: - context_data: S4U 上下文数据(包含 expression_habits) - """ - if global_config is None: - raise RuntimeError("global_config 未初始化") - - context_data = context_data or {} - - reply_style = global_config.personality.reply_style or "" - expression_habits = context_data.get("expression_habits", "") - - # JSON 输出格式说明 - 简洁版 - json_format = """### 输出格式 -用 JSON 输出你的想法和决策: - -```json -{ - "thought": "你的内心想法,想说什么就说什么", - "expected_user_reaction": "你觉得对方会怎么回应", - "max_wait_seconds": 等待秒数(60-900),不想等就填0, - "actions": [ - {"type": "reply", "content": "你要发送的消息"}, - {"type": "其他动作", ...} - ] -} -``` - -说明: -- `thought`:你脑子里在想什么,越自然越好 -- `actions`:你要做的事,可以组合多个动作 -- `max_wait_seconds`:设定一个时间,对方没回的话你会再想想要不要说点什么 -- 即使什么都不想做,也放一个 `{"type": "do_nothing"}`""" - - parts = ["## 6. 你的表达方式"] - - if reply_style: - parts.append(reply_style) - - if expression_habits: - parts.append("") - parts.append(expression_habits) - - parts.append("") - parts.append(json_format) - - return "\n".join(parts) - - -# ============================================================ -# 组装完整的 System Prompt -# ============================================================ - -def build_system_prompt( - session: KokoroSession, - available_actions: Optional[dict[str, ActionInfo]] = None, - context_data: Optional[dict[str, str]] = None, - chat_stream: Optional["ChatStream"] = None, -) -> str: - """ - 组装完整的系统提示词 - - 按模块顺序组装: - 1. 角色框架(你是对话体验者) - 2. 核心身份(人设) - 3. 行为准则(规则) - 4. 情境上下文(时间/状态/关系) - 5. 动作能力(可用动作) - 6. 输出格式(表达风格 + JSON) - - Args: - session: 当前会话 - available_actions: 可用动作字典 - context_data: S4U 上下文数据 - chat_stream: 聊天流 - - Returns: - 完整的系统提示词 - """ - # 角色框架 - 简洁自然 - role_frame = '你不是AI助手,你是一个真实的人,有自己的生活、想法和情绪。聊天时就像平时发微信一样自然。' - - # 组装各模块 - modules = [ - role_frame, - "", - build_identity_module(), - "", - build_rules_module(), - "", - build_context_module(session, chat_stream, context_data), - "", - build_actions_module(available_actions), - "", - build_output_module(context_data), - ] - - return "\n".join(modules) diff --git a/src/plugins/built_in/kokoro_flow_chatter/response_post_processor.py b/src/plugins/built_in/kokoro_flow_chatter/response_post_processor.py deleted file mode 100644 index e463d696b..000000000 --- a/src/plugins/built_in/kokoro_flow_chatter/response_post_processor.py +++ /dev/null @@ -1,169 +0,0 @@ -""" -KFC 响应后处理器 - -实现与全局后处理流程的集成: -- 中文错别字生成(typo_generator) -- 消息分割(punctuation/llm模式) - -设计理念:复用全局配置和AFC的核心分割逻辑,与AFC保持一致的后处理行为。 -""" - -import re -from typing import Any, Optional, TYPE_CHECKING - -from src.common.logger import get_logger -from src.config.config import global_config - -if TYPE_CHECKING: - from src.chat.utils.typo_generator import ChineseTypoGenerator - -logger = get_logger("kokoro_post_processor") - -# 延迟导入错别字生成器(避免循环导入和启动时的额外开销) -_typo_generator: Optional["ChineseTypoGenerator"] = None - - -def _get_typo_generator(): - """延迟加载错别字生成器""" - global _typo_generator - if _typo_generator is None: - try: - from src.chat.utils.typo_generator import ChineseTypoGenerator - - if global_config is None: - logger.warning("[KFC PostProcessor] global_config 未初始化") - return None - - # 从全局配置读取参数 - typo_cfg = global_config.chinese_typo - _typo_generator = ChineseTypoGenerator( - error_rate=typo_cfg.error_rate, - min_freq=typo_cfg.min_freq, - tone_error_rate=typo_cfg.tone_error_rate, - word_replace_rate=typo_cfg.word_replace_rate, - ) - logger.info("[KFC PostProcessor] 错别字生成器已初始化") - except Exception as e: - logger.warning(f"[KFC PostProcessor] 初始化错别字生成器失败: {e}") - _typo_generator = None - return _typo_generator - - -def split_by_punctuation(text: str, max_length: int = 256, max_sentences: int = 8) -> list[str]: - """ - 基于标点符号分割消息 - 复用AFC的核心逻辑 - - V6修复: 不再依赖长度判断,而是直接调用AFC的分割函数 - - Args: - text: 原始文本 - max_length: 单条消息最大长度(用于二次合并过长片段) - max_sentences: 最大句子数 - - Returns: - list[str]: 分割后的消息列表 - """ - if not text: - return [] - - # 直接复用AFC的核心分割逻辑 - from src.chat.utils.utils import split_into_sentences_w_remove_punctuation - - # AFC的分割函数会根据标点分割并概率性合并 - sentences = split_into_sentences_w_remove_punctuation(text) - - if not sentences: - return [text] if text else [] - - # 限制句子数量 - if len(sentences) > max_sentences: - sentences = sentences[:max_sentences] - - # 如果某个片段超长,进行二次切分 - result = [] - for sentence in sentences: - if len(sentence) > max_length: - # 超长片段按max_length硬切分 - for i in range(0, len(sentence), max_length): - chunk = sentence[i:i + max_length] - if chunk.strip(): - result.append(chunk.strip()) - else: - if sentence.strip(): - result.append(sentence.strip()) - - return result if result else [text] - - -async def process_reply_content(content: str) -> list[str]: - """ - 处理回复内容(主入口) - - 遵循全局配置: - - [response_post_process].enable_response_post_process - - [chinese_typo].enable - - [response_splitter].enable 和 .split_mode - - Args: - content: 原始回复内容 - - Returns: - list[str]: 处理后的消息列表(可能被分割成多条) - """ - if not content: - return [] - - if global_config is None: - logger.warning("[KFC PostProcessor] global_config 未初始化,返回原始内容") - return [content] - - # 检查全局开关 - post_process_cfg = global_config.response_post_process - if not post_process_cfg.enable_response_post_process: - logger.info("[KFC PostProcessor] 全局后处理已禁用,返回原始内容") - return [content] - - processed_content = content - - # Step 1: 错别字生成 - typo_cfg = global_config.chinese_typo - if typo_cfg.enable: - try: - typo_gen = _get_typo_generator() - if typo_gen: - processed_content, correction_suggestion = typo_gen.create_typo_sentence(content) - if correction_suggestion: - logger.info(f"[KFC PostProcessor] 生成错别字,建议纠正: {correction_suggestion}") - else: - logger.info("[KFC PostProcessor] 已应用错别字生成") - except Exception as e: - logger.warning(f"[KFC PostProcessor] 错别字生成失败: {e}") - # 失败时使用原内容 - processed_content = content - - # Step 2: 消息分割 - splitter_cfg = global_config.response_splitter - if splitter_cfg.enable: - split_mode = splitter_cfg.split_mode - max_length = splitter_cfg.max_length - max_sentences = splitter_cfg.max_sentence_num - - if split_mode == "punctuation": - # 基于标点符号分割 - result = split_by_punctuation( - processed_content, - max_length=max_length, - max_sentences=max_sentences, - ) - logger.info(f"[KFC PostProcessor] 标点分割完成,分为 {len(result)} 条消息") - return result - elif split_mode == "llm": - # LLM模式:目前暂不支持,回退到不分割 - logger.info("[KFC PostProcessor] LLM分割模式暂不支持,返回完整内容") - return [processed_content] - else: - logger.warning(f"[KFC PostProcessor] 未知分割模式: {split_mode}") - return [processed_content] - else: - # 分割器禁用,返回完整内容 - return [processed_content] diff --git a/src/plugins/built_in/kokoro_flow_chatter/session_manager.py b/src/plugins/built_in/kokoro_flow_chatter/session_manager.py deleted file mode 100644 index b5b102691..000000000 --- a/src/plugins/built_in/kokoro_flow_chatter/session_manager.py +++ /dev/null @@ -1,561 +0,0 @@ -""" -Kokoro Flow Chatter 会话管理器 - -负责管理用户会话的完整生命周期: -- 创建、加载、保存会话 -- 会话状态持久化 -- 会话清理和维护 -""" - -import asyncio -import json -import os -import time -from pathlib import Path -from typing import Optional - -from src.common.logger import get_logger - -from .models import ( - EmotionalState, - KokoroSession, - MentalLogEntry, - MentalLogEventType, - SessionStatus, -) - -logger = get_logger("kokoro_session_manager") - - -class SessionManager: - """ - Kokoro Flow Chatter 会话管理器 - - 单例模式实现,为每个私聊用户维护独立的会话 - - Features: - - 会话的创建、获取、更新和删除 - - 自动持久化到JSON文件 - - 会话过期清理 - - 线程安全的并发访问 - """ - - _instance: Optional["SessionManager"] = None - _lock = asyncio.Lock() - - def __new__(cls, *args, **kwargs): - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance - - def __init__( - self, - data_dir: str = "data/kokoro_flow_chatter/sessions", - max_session_age_days: int = 30, - auto_save_interval: int = 300, - ): - """ - 初始化会话管理器 - - Args: - data_dir: 会话数据存储目录 - max_session_age_days: 会话最大保留天数 - auto_save_interval: 自动保存间隔(秒) - """ - # 避免重复初始化 - if hasattr(self, "_initialized") and self._initialized: - return - - self._initialized = True - self.data_dir = Path(data_dir) - self.max_session_age_days = max_session_age_days - self.auto_save_interval = auto_save_interval - - # 内存中的会话缓存 - self._sessions: dict[str, KokoroSession] = {} - self._session_locks: dict[str, asyncio.Lock] = {} - - # 后台任务 - self._auto_save_task: Optional[asyncio.Task] = None - self._cleanup_task: Optional[asyncio.Task] = None - self._running = False - - # 确保数据目录存在 - self._ensure_data_dir() - - logger.info(f"SessionManager 初始化完成,数据目录: {self.data_dir}") - - def _ensure_data_dir(self) -> None: - """确保数据目录存在""" - self.data_dir.mkdir(parents=True, exist_ok=True) - - def _get_session_file_path(self, user_id: str) -> Path: - """获取会话文件路径""" - # 清理user_id中的特殊字符 - safe_user_id = "".join(c if c.isalnum() or c in "-_" else "_" for c in user_id) - return self.data_dir / f"{safe_user_id}.json" - - async def _get_session_lock(self, user_id: str) -> asyncio.Lock: - """获取会话级别的锁""" - if user_id not in self._session_locks: - self._session_locks[user_id] = asyncio.Lock() - return self._session_locks[user_id] - - async def start(self) -> None: - """启动会话管理器的后台任务""" - if self._running: - return - - self._running = True - - # 启动自动保存任务 - self._auto_save_task = asyncio.create_task(self._auto_save_loop()) - - # 启动清理任务 - self._cleanup_task = asyncio.create_task(self._cleanup_loop()) - - logger.info("SessionManager 后台任务已启动") - - async def stop(self) -> None: - """停止会话管理器并保存所有会话""" - self._running = False - - # 取消后台任务 - if self._auto_save_task: - self._auto_save_task.cancel() - try: - await self._auto_save_task - except asyncio.CancelledError: - pass - - if self._cleanup_task: - self._cleanup_task.cancel() - try: - await self._cleanup_task - except asyncio.CancelledError: - pass - - # 保存所有会话 - await self.save_all_sessions() - - logger.info("SessionManager 已停止,所有会话已保存") - - async def _auto_save_loop(self) -> None: - """自动保存循环""" - while self._running: - try: - await asyncio.sleep(self.auto_save_interval) - await self.save_all_sessions() - except asyncio.CancelledError: - break - except Exception as e: - logger.error(f"自动保存会话时出错: {e}") - - async def _cleanup_loop(self) -> None: - """清理过期会话循环""" - while self._running: - try: - # 每小时清理一次 - await asyncio.sleep(3600) - await self.cleanup_expired_sessions() - except asyncio.CancelledError: - break - except Exception as e: - logger.error(f"清理过期会话时出错: {e}") - - async def get_session(self, user_id: str, stream_id: str) -> KokoroSession: - """ - 获取或创建用户会话 - - Args: - user_id: 用户ID - stream_id: 聊天流ID - - Returns: - KokoroSession: 用户会话对象 - """ - lock = await self._get_session_lock(user_id) - async with lock: - # 检查内存缓存 - if user_id in self._sessions: - session = self._sessions[user_id] - # 更新stream_id(可能发生变化) - session.stream_id = stream_id - return session - - # 尝试从文件加载 - session = await self._load_session_from_file(user_id) - if session: - session.stream_id = stream_id - self._sessions[user_id] = session - logger.debug(f"从文件加载会话: {user_id}") - return session - - # 创建新会话 - session = KokoroSession( - user_id=user_id, - stream_id=stream_id, - status=SessionStatus.IDLE, - emotional_state=EmotionalState(), - mental_log=[], - ) - - # 添加初始日志条目 - initial_entry = MentalLogEntry( - event_type=MentalLogEventType.STATE_CHANGE, - timestamp=time.time(), - thought="与这位用户的对话开始了,我对接下来的交流充满期待。", - content="会话创建", - emotional_snapshot=session.emotional_state.to_dict(), - ) - session.add_mental_log_entry(initial_entry) - - self._sessions[user_id] = session - logger.info(f"创建新会话: {user_id}") - - return session - - async def _load_session_from_file(self, user_id: str) -> Optional[KokoroSession]: - """从文件加载会话""" - file_path = self._get_session_file_path(user_id) - - if not file_path.exists(): - return None - - try: - with open(file_path, "r", encoding="utf-8") as f: - data = json.load(f) - - session = KokoroSession.from_dict(data) - - # V7: 情绪健康检查 - 防止从持久化数据恢复无厘头的负面情绪 - session = self._sanitize_emotional_state(session) - - logger.debug(f"成功从文件加载会话: {user_id}") - return session - - except json.JSONDecodeError as e: - logger.error(f"解析会话文件失败 {user_id}: {e}") - # 备份损坏的文件 - backup_path = file_path.with_suffix(".json.bak") - os.rename(file_path, backup_path) - return None - except Exception as e: - logger.error(f"加载会话文件失败 {user_id}: {e}") - return None - - def _sanitize_emotional_state(self, session: KokoroSession) -> KokoroSession: - """ - V7: 情绪健康检查 - - 检查并修正不合理的情绪状态,防止: - 1. 无厘头的负面情绪从持久化数据恢复 - 2. 情绪强度过高(>0.8)的负面情绪 - 3. 长时间未更新的情绪状态 - - Args: - session: 会话对象 - - Returns: - 修正后的会话对象 - """ - emotional_state = session.emotional_state - current_mood = emotional_state.mood.lower() if emotional_state.mood else "" - - # 负面情绪关键词列表 - negative_moods = [ - "低落", "沮丧", "难过", "伤心", "失落", "郁闷", "烦躁", "焦虑", - "担忧", "害怕", "恐惧", "愤怒", "生气", "不安", "忧郁", "悲伤", - "sad", "depressed", "anxious", "angry", "upset", "worried" - ] - - is_negative = any(neg in current_mood for neg in negative_moods) - - # 检查1: 如果是负面情绪且强度较高(>0.6),重置为平静 - if is_negative and emotional_state.mood_intensity > 0.6: - logger.warning( - f"[KFC] 检测到高强度负面情绪 ({emotional_state.mood}, {emotional_state.mood_intensity:.1%})," - f"重置为平静状态" - ) - emotional_state.mood = "平静" - emotional_state.mood_intensity = 0.3 - - # 检查2: 如果情绪超过24小时未更新,重置为平静 - import time as time_module - time_since_update = time_module.time() - emotional_state.last_update_time - if time_since_update > 86400: # 24小时 = 86400秒 - logger.info( - f"[KFC] 情绪状态超过24小时未更新 ({time_since_update/3600:.1f}h)," - f"重置为平静状态" - ) - emotional_state.mood = "平静" - emotional_state.mood_intensity = 0.3 - emotional_state.anxiety_level = 0.0 - emotional_state.last_update_time = time_module.time() - - # 检查3: 焦虑程度过高也需要重置 - if emotional_state.anxiety_level > 0.8: - logger.info(f"[KFC] 焦虑程度过高 ({emotional_state.anxiety_level:.1%}),重置为正常") - emotional_state.anxiety_level = 0.3 - - return session - - async def save_session(self, user_id: str) -> bool: - """ - 保存单个会话到文件 - - Args: - user_id: 用户ID - - Returns: - bool: 是否保存成功 - """ - lock = await self._get_session_lock(user_id) - async with lock: - if user_id not in self._sessions: - return False - - session = self._sessions[user_id] - file_path = self._get_session_file_path(user_id) - - try: - data = session.to_dict() - - # 先写入临时文件,再重命名(原子操作) - temp_path = file_path.with_suffix(".json.tmp") - with open(temp_path, "w", encoding="utf-8") as f: - json.dump(data, f, ensure_ascii=False, indent=2) - - os.replace(temp_path, file_path) - logger.debug(f"保存会话成功: {user_id}") - return True - - except Exception as e: - logger.error(f"保存会话失败 {user_id}: {e}") - return False - - async def save_all_sessions(self) -> int: - """ - 保存所有会话 - - Returns: - int: 成功保存的会话数量 - """ - saved_count = 0 - for user_id in list(self._sessions.keys()): - if await self.save_session(user_id): - saved_count += 1 - - if saved_count > 0: - logger.debug(f"批量保存完成,共保存 {saved_count} 个会话") - - return saved_count - - async def update_session( - self, - user_id: str, - status: Optional[SessionStatus] = None, - emotional_state: Optional[EmotionalState] = None, - mental_log_entry: Optional[MentalLogEntry] = None, - **kwargs, - ) -> bool: - """ - 更新会话状态 - - Args: - user_id: 用户ID - status: 新的会话状态 - emotional_state: 新的情感状态 - mental_log_entry: 要添加的心理日志条目 - **kwargs: 其他要更新的字段 - - Returns: - bool: 是否更新成功 - """ - lock = await self._get_session_lock(user_id) - async with lock: - if user_id not in self._sessions: - return False - - session = self._sessions[user_id] - - if status is not None: - old_status = session.status - session.status = status - logger.debug(f"会话状态变更 {user_id}: {old_status} -> {status}") - - if emotional_state is not None: - session.emotional_state = emotional_state - - if mental_log_entry is not None: - session.add_mental_log_entry(mental_log_entry) - - # 更新其他字段 - for key, value in kwargs.items(): - if hasattr(session, key): - setattr(session, key, value) - - session.last_activity_at = time.time() - - return True - - async def delete_session(self, user_id: str) -> bool: - """ - 删除会话 - - Args: - user_id: 用户ID - - Returns: - bool: 是否删除成功 - """ - lock = await self._get_session_lock(user_id) - async with lock: - # 从内存中删除 - if user_id in self._sessions: - del self._sessions[user_id] - - # 从文件系统删除 - file_path = self._get_session_file_path(user_id) - if file_path.exists(): - try: - os.remove(file_path) - logger.info(f"删除会话: {user_id}") - return True - except Exception as e: - logger.error(f"删除会话文件失败 {user_id}: {e}") - return False - - return True - - async def cleanup_expired_sessions(self) -> int: - """ - 清理过期会话 - - Returns: - int: 清理的会话数量 - """ - cleaned_count = 0 - current_time = time.time() - max_age_seconds = self.max_session_age_days * 24 * 3600 - - # 检查文件系统中的所有会话 - for file_path in self.data_dir.glob("*.json"): - try: - with open(file_path, "r", encoding="utf-8") as f: - data = json.load(f) - - last_activity = data.get("last_activity_at", 0) - if current_time - last_activity > max_age_seconds: - user_id = data.get("user_id", file_path.stem) - - # 从内存中删除 - if user_id in self._sessions: - del self._sessions[user_id] - - # 删除文件 - os.remove(file_path) - cleaned_count += 1 - logger.info(f"清理过期会话: {user_id}") - - except Exception as e: - logger.error(f"清理会话时出错 {file_path}: {e}") - - if cleaned_count > 0: - logger.info(f"共清理 {cleaned_count} 个过期会话") - - return cleaned_count - - async def get_all_waiting_sessions(self) -> list[KokoroSession]: - """ - 获取所有处于等待状态的会话 - - Returns: - list[KokoroSession]: 等待中的会话列表 - """ - waiting_sessions = [] - - for session in self._sessions.values(): - if session.status == SessionStatus.WAITING: - waiting_sessions.append(session) - - 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: - """ - 获取会话统计信息 - - Returns: - dict: 统计信息字典 - """ - total_in_memory = len(self._sessions) - status_counts = {} - - for session in self._sessions.values(): - status = str(session.status) - status_counts[status] = status_counts.get(status, 0) + 1 - - # 统计文件系统中的会话 - total_on_disk = len(list(self.data_dir.glob("*.json"))) - - return { - "total_in_memory": total_in_memory, - "total_on_disk": total_on_disk, - "status_counts": status_counts, - "data_directory": str(self.data_dir), - } - - def get_session_sync(self, user_id: str) -> Optional[KokoroSession]: - """ - 同步获取会话(仅从内存缓存) - - Args: - user_id: 用户ID - - Returns: - Optional[KokoroSession]: 会话对象,如果不存在返回None - """ - return self._sessions.get(user_id) - - -# 全局会话管理器实例 -_session_manager: Optional[SessionManager] = None - - -def get_session_manager() -> SessionManager: - """获取全局会话管理器实例""" - global _session_manager - if _session_manager is None: - _session_manager = SessionManager() - return _session_manager - - -async def initialize_session_manager( - data_dir: str = "data/kokoro_flow_chatter/sessions", - **kwargs, -) -> SessionManager: - """ - 初始化并启动会话管理器 - - Args: - data_dir: 数据存储目录 - **kwargs: 其他配置参数 - - Returns: - SessionManager: 会话管理器实例 - """ - global _session_manager - _session_manager = SessionManager(data_dir=data_dir, **kwargs) - await _session_manager.start() - return _session_manager diff --git a/src/plugins/built_in/kokoro_flow_chatter_v2/__init__.py b/src/plugins/built_in/kokoro_flow_chatter_v2/__init__.py index a7e8604e8..692bdc389 100644 --- a/src/plugins/built_in/kokoro_flow_chatter_v2/__init__.py +++ b/src/plugins/built_in/kokoro_flow_chatter_v2/__init__.py @@ -20,7 +20,6 @@ from .models import ( from .session import KokoroSession, SessionManager, get_session_manager from .chatter import KokoroFlowChatterV2 from .replyer import generate_response -from .action_executor import ActionExecutor from .proactive_thinker import ( ProactiveThinker, get_proactive_thinker, @@ -50,7 +49,6 @@ __all__ = [ # Core Components "KokoroFlowChatterV2", "generate_response", - "ActionExecutor", # Proactive Thinker "ProactiveThinker", "get_proactive_thinker", diff --git a/src/plugins/built_in/kokoro_flow_chatter_v2/action_executor.py b/src/plugins/built_in/kokoro_flow_chatter_v2/action_executor.py deleted file mode 100644 index aa0fe2ef1..000000000 --- a/src/plugins/built_in/kokoro_flow_chatter_v2/action_executor.py +++ /dev/null @@ -1,228 +0,0 @@ -""" -Kokoro Flow Chatter V2 - 动作执行器 - -负责执行 LLM 决策的动作 -""" - -import asyncio -import time -from typing import TYPE_CHECKING, Any, Optional - -from src.chat.planner_actions.action_manager import ChatterActionManager -from src.common.logger import get_logger -from src.plugin_system.apis import send_api - -from .models import ActionModel, LLMResponse - -if TYPE_CHECKING: - from src.chat.message_receive.chat_stream import ChatStream - -logger = get_logger("kfc_v2_action_executor") - - -class ActionExecutor: - """ - 动作执行器 - - 职责: - - 执行 reply、poke_user 等动作 - - 通过 ActionManager 执行动态注册的动作 - """ - - # 内置动作(不通过 ActionManager) - BUILTIN_ACTIONS = {"reply", "do_nothing"} - - def __init__(self, stream_id: str): - self.stream_id = stream_id - self._action_manager = ChatterActionManager() - self._available_actions: dict = {} - - # 统计 - self._stats = { - "total_executed": 0, - "successful": 0, - "failed": 0, - } - - async def load_actions(self) -> dict: - """加载可用动作""" - await self._action_manager.load_actions(self.stream_id) - self._available_actions = self._action_manager.get_using_actions() - logger.debug(f"[ActionExecutor] 加载了 {len(self._available_actions)} 个动作") - return self._available_actions - - def get_available_actions(self) -> dict: - """获取可用动作""" - return self._available_actions.copy() - - async def execute( - self, - response: LLMResponse, - chat_stream: Optional["ChatStream"], - ) -> dict[str, Any]: - """ - 执行动作列表 - - Args: - response: LLM 响应 - chat_stream: 聊天流 - - Returns: - 执行结果 - """ - results = [] - has_reply = False - reply_content = "" - - for action in response.actions: - try: - result = await self._execute_action(action, chat_stream) - results.append(result) - - if result.get("success"): - self._stats["successful"] += 1 - if action.type in ("reply", "respond"): - has_reply = True - reply_content = action.params.get("content", "") - else: - self._stats["failed"] += 1 - - except Exception as e: - logger.error(f"[ActionExecutor] 执行动作失败 {action.type}: {e}") - results.append({ - "action_type": action.type, - "success": False, - "error": str(e), - }) - self._stats["failed"] += 1 - - self._stats["total_executed"] += 1 - - return { - "success": all(r.get("success", False) for r in results), - "results": results, - "has_reply": has_reply, - "reply_content": reply_content, - } - - async def _execute_action( - self, - action: ActionModel, - chat_stream: Optional["ChatStream"], - ) -> dict[str, Any]: - """执行单个动作""" - action_type = action.type - - if action_type == "reply": - return await self._execute_reply(action, chat_stream) - - elif action_type == "do_nothing": - logger.debug("[ActionExecutor] 执行 do_nothing") - return {"action_type": "do_nothing", "success": True} - - elif action_type == "poke_user": - return await self._execute_via_manager(action, chat_stream) - - elif action_type in self._available_actions: - return await self._execute_via_manager(action, chat_stream) - - else: - logger.warning(f"[ActionExecutor] 未知动作类型: {action_type}") - return { - "action_type": action_type, - "success": False, - "error": f"未知动作类型: {action_type}", - } - - async def _execute_reply( - self, - action: ActionModel, - chat_stream: Optional["ChatStream"], - ) -> dict[str, Any]: - """执行回复动作""" - content = action.params.get("content", "") - - if not content: - return { - "action_type": "reply", - "success": False, - "error": "回复内容为空", - } - - try: - # 消息后处理(分割、错别字等) - processed_messages = await self._process_reply_content(content) - - all_success = True - for msg in processed_messages: - success = await send_api.text_to_stream( - text=msg, - stream_id=self.stream_id, - typing=True, - ) - if not success: - all_success = False - - return { - "action_type": "reply", - "success": all_success, - "reply_text": content, - } - - except Exception as e: - logger.error(f"[ActionExecutor] 发送回复失败: {e}") - return { - "action_type": "reply", - "success": False, - "error": str(e), - } - - async def _process_reply_content(self, content: str) -> list[str]: - """处理回复内容(分割、错别字等)""" - try: - # 复用 v1 的后处理器 - from src.plugins.built_in.kokoro_flow_chatter.response_post_processor import ( - process_reply_content, - ) - - messages = await process_reply_content(content) - return messages if messages else [content] - - except Exception as e: - logger.warning(f"[ActionExecutor] 消息处理失败,使用原始内容: {e}") - return [content] - - async def _execute_via_manager( - self, - action: ActionModel, - chat_stream: Optional["ChatStream"], - ) -> dict[str, Any]: - """通过 ActionManager 执行动作""" - try: - result = await self._action_manager.execute_action( - action_name=action.type, - chat_id=self.stream_id, - target_message=None, - reasoning=f"KFC决策: {action.type}", - action_data=action.params, - thinking_id=None, - log_prefix="[KFC V2]", - ) - - return { - "action_type": action.type, - "success": result.get("success", False), - "result": result, - } - - except Exception as e: - logger.error(f"[ActionExecutor] ActionManager 执行失败: {e}") - return { - "action_type": action.type, - "success": False, - "error": str(e), - } - - def get_stats(self) -> dict: - """获取统计信息""" - return self._stats.copy() diff --git a/src/plugins/built_in/kokoro_flow_chatter_v2/chatter.py b/src/plugins/built_in/kokoro_flow_chatter_v2/chatter.py index 410226b95..38aaec0bd 100644 --- a/src/plugins/built_in/kokoro_flow_chatter_v2/chatter.py +++ b/src/plugins/built_in/kokoro_flow_chatter_v2/chatter.py @@ -10,21 +10,20 @@ Kokoro Flow Chatter V2 - Chatter 主类 import asyncio import time -from typing import TYPE_CHECKING, Any, ClassVar, Optional +from typing import TYPE_CHECKING, Any, ClassVar +from src.chat.planner_actions.action_manager import ChatterActionManager from src.common.data_models.message_manager_data_model import StreamContext from src.common.logger import get_logger from src.plugin_system.base.base_chatter import BaseChatter from src.plugin_system.base.component_types import ChatType -from .action_executor import ActionExecutor -from .models import EventType, SessionStatus +from .models import SessionStatus from .replyer import generate_response from .session import get_session_manager if TYPE_CHECKING: - from src.chat.planner_actions.action_manager import ChatterActionManager - from src.common.data_models.database_data_model import DatabaseMessages + pass logger = get_logger("kfc_v2_chatter") @@ -62,7 +61,6 @@ class KokoroFlowChatterV2(BaseChatter): # 核心组件 self.session_manager = get_session_manager() - self.action_executor = ActionExecutor(stream_id) # 并发控制 self._lock = asyncio.Lock() @@ -129,9 +127,12 @@ class KokoroFlowChatterV2(BaseChatter): timestamp=msg.time, ) - # 6. 加载可用动作 - await self.action_executor.load_actions() - available_actions = self.action_executor.get_available_actions() + # 6. 加载可用动作(通过 ActionModifier 过滤) + from src.chat.planner_actions.action_modifier import ActionModifier + + action_modifier = ActionModifier(self.action_manager, self.stream_id) + await action_modifier.modify_actions(chatter_name="KokoroFlowChatterV2") + available_actions = self.action_manager.get_using_actions() # 7. 获取聊天流 chat_stream = await self._get_chat_stream() @@ -146,7 +147,21 @@ class KokoroFlowChatterV2(BaseChatter): ) # 9. 执行动作 - exec_result = await self.action_executor.execute(response, chat_stream) + exec_results = [] + has_reply = False + for action in response.actions: + result = await self.action_manager.execute_action( + action_name=action.type, + chat_id=self.stream_id, + target_message=target_message, + reasoning=response.thought, + action_data=action.params, + thinking_id=None, + log_prefix="[KFC V2]", + ) + exec_results.append(result) + if result.get("success") and action.type in ("reply", "respond"): + has_reply = True # 10. 记录 Bot 规划到 mental_log session.add_bot_planning( @@ -174,7 +189,7 @@ class KokoroFlowChatterV2(BaseChatter): # 14. 更新统计 self._stats["messages_processed"] += len(unread_messages) - if exec_result.get("has_reply"): + if has_reply: self._stats["successful_responses"] += 1 logger.info( @@ -187,7 +202,7 @@ class KokoroFlowChatterV2(BaseChatter): return self._build_result( success=True, message="processed", - has_reply=exec_result.get("has_reply", False), + has_reply=has_reply, thought=response.thought, situation_type=situation_type, ) @@ -252,10 +267,7 @@ class KokoroFlowChatterV2(BaseChatter): def get_stats(self) -> dict[str, Any]: """获取统计信息""" - return { - **self._stats, - "action_executor_stats": self.action_executor.get_stats(), - } + return self._stats.copy() @property def is_processing(self) -> bool: diff --git a/src/plugins/built_in/kokoro_flow_chatter_v2/proactive_thinker.py b/src/plugins/built_in/kokoro_flow_chatter_v2/proactive_thinker.py index 382b9500f..0cab996ec 100644 --- a/src/plugins/built_in/kokoro_flow_chatter_v2/proactive_thinker.py +++ b/src/plugins/built_in/kokoro_flow_chatter_v2/proactive_thinker.py @@ -15,11 +15,11 @@ import time from datetime import datetime from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional +from src.chat.planner_actions.action_manager import ChatterActionManager 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 -from .action_executor import ActionExecutor from .models import EventType, SessionStatus from .replyer import generate_response from .session import KokoroSession, get_session_manager @@ -257,8 +257,8 @@ class ProactiveThinker: chat_stream = await self._get_chat_stream(session.stream_id) # 加载动作 - action_executor = ActionExecutor(session.stream_id) - await action_executor.load_actions() + action_manager = ChatterActionManager() + await action_manager.load_actions(session.stream_id) # 调用 Replyer 生成超时决策 response = await generate_response( @@ -266,11 +266,20 @@ class ProactiveThinker: user_name=session.user_id, # 这里可以改进,获取真实用户名 situation_type="timeout", chat_stream=chat_stream, - available_actions=action_executor.get_available_actions(), + available_actions=action_manager.get_using_actions(), ) # 执行动作 - exec_result = await action_executor.execute(response, chat_stream) + for action in response.actions: + await action_manager.execute_action( + action_name=action.type, + chat_id=session.stream_id, + target_message=None, + reasoning=response.thought, + action_data=action.params, + thinking_id=None, + log_prefix="[KFC V2 ProactiveThinker]", + ) # 记录到 mental_log session.add_bot_planning( @@ -389,8 +398,8 @@ class ProactiveThinker: chat_stream = await self._get_chat_stream(session.stream_id) # 加载动作 - action_executor = ActionExecutor(session.stream_id) - await action_executor.load_actions() + action_manager = ChatterActionManager() + await action_manager.load_actions(session.stream_id) # 计算沉默时长 silence_seconds = time.time() - session.last_activity_at @@ -405,7 +414,7 @@ class ProactiveThinker: user_name=session.user_id, situation_type="proactive", chat_stream=chat_stream, - available_actions=action_executor.get_available_actions(), + available_actions=action_manager.get_using_actions(), extra_context={ "trigger_reason": trigger_reason, "silence_duration": silence_duration, @@ -425,7 +434,16 @@ class ProactiveThinker: return # 执行动作 - exec_result = await action_executor.execute(response, chat_stream) + for action in response.actions: + await action_manager.execute_action( + action_name=action.type, + chat_id=session.stream_id, + target_message=None, + reasoning=response.thought, + action_data=action.params, + thinking_id=None, + log_prefix="[KFC V2 ProactiveThinker]", + ) # 记录到 mental_log session.add_bot_planning(