From 0746a73bceadc40b6e6e7e2ac9fd44e34a154cf6 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Sat, 29 Nov 2025 02:05:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(KFC):=20=F0=9F=8E=89=20Kokoro=20Flow=20Cha?= =?UTF-8?q?tter=20=E5=BF=83=E6=B5=81=E8=81=8A=E5=A4=A9=E5=99=A8=20-=20?= =?UTF-8?q?=E7=A7=81=E8=81=8A=E4=B8=93=E5=B1=9E=E5=A4=84=E7=90=86=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E4=BB=8E=E9=9B=B6=E6=9E=84=E5=BB=BA=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这是一个全新的私聊聊天处理器,专为深度情感交互设计,从架构设计到代码实现全部从零完成。 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🏗️ 核心架构 (7个核心模块) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📁 src/plugins/built_in/kokoro_flow_chatter/ ├── chatter.py # 主处理器 - 协调所有组件的核心类 ├── context_builder.py # S4U上下文构建器 - 超融合上下文系统 ├── prompt_generator.py # V6三明治提示词生成器 ├── action_executor.py # 动作执行器 - 解析+执行LLM动作 ├── response_post_processor.py # 回复后处理器 - 分割+错别字 ├── models.py # 数据模型 - Session/情感状态/心理日志 ├── session_manager.py # 会话管理器 - 用户状态持久化 ├── scheduler.py # 调度器 - 主动思考/超时处理 ├── config.py # 配置类 └── plugin.py # 插件注册入口 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ✨ 核心特性 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 【V1-V3 基础框架】 - 心理状态驱动的交互模型 (KokoroSession) - 连续时间观念和等待体验 (IDLE→RESPONDING→WAITING状态机) - 心理日志系统 (MentalLogEntry) - 动态情感状态 (EmotionalState) 【V4 动作系统集成】 - 动态动作发现 (复用ChatterActionManager) - 支持所有AFC动作 (reply/emoji/poke_user/set_emoji_like等) - LLM响应JSON解析和验证 【V5 超融合上下文】 - S4U用户中心上下文检索 - 三层记忆系统集成 (感知/短期/长期) - 时间感知块 (时间段+日程+情境) - 人物关系信息注入 - 跨聊天上下文共享 【V6 最终优化】 - 三明治提示词结构 (系统层→上下文层→指令层) - ActionModifier动作筛选器集成 (三阶段预筛选) - 阶段0: 聊天类型过滤 - 阶段2: 关联类型匹配 - 阶段3: go_activate()激活判定 - 回复分割器复用AFC核心逻辑 (split_into_sentences_w_remove_punctuation) - 修复model配置 (使用replyer而非utils) - 修复context_builder异步问题 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🔧 技术细节 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 提示词结构 (V6三明治): ┌─────────────────────────────────────┐ │ 🍞 系统层 (人设/身份/表达风格) │ ├─────────────────────────────────────┤ │ 🥬 上下文层 │ │ ├─ 时间感知块 │ │ ├─ 三层记忆 (感知+短期+长期) │ │ ├─ 人物关系 │ │ ├─ 对话历史 │ │ └─ 用户最新消息 │ ├─────────────────────────────────────┤ │ 🍞 指令层 (JSON输出格式/可用动作) │ └─────────────────────────────────────┘ 动作筛选效果: 13个动作 → 约5-7个 (节省token+提升决策质量) 回复分割: 长消息自动按标点分割成多条发送 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📝 配置项 (bot_config.toml) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [kokoro_flow_chatter] enable = true max_wait_seconds_default = 300 enable_continuous_thinking = true [kokoro_flow_chatter.proactive_thinking] enabled = true silence_threshold_seconds = 7200 min_affinity_for_proactive = 0.3 min_interval_between_proactive = 1800 enable_morning_greeting = true enable_night_greeting = true ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🎯 设计理念 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ KFC不是独立人格,而是: - 复用全局人设、情感框架和回复模型 - 专注于"体验→决策→行动"的私聊交互模式 - 从"消息响应者"转变为"对话体验者" - 深度情感连接和长期关系维护 --- src/common/logger.py | 16 + src/config/config.py | 4 + src/config/official_configs.py | 68 ++ .../built_in/kokoro_flow_chatter/__init__.py | 26 + .../kokoro_flow_chatter/action_executor.py | 795 +++++++++++++++++ .../built_in/kokoro_flow_chatter/chatter.py | 606 +++++++++++++ .../built_in/kokoro_flow_chatter/config.py | 251 ++++++ .../kokoro_flow_chatter/context_builder.py | 528 ++++++++++++ .../built_in/kokoro_flow_chatter/models.py | 442 ++++++++++ .../built_in/kokoro_flow_chatter/plugin.py | 218 +++++ .../kokoro_flow_chatter/proactive_thinking.py | 528 ++++++++++++ .../kokoro_flow_chatter/prompt_generator.py | 801 ++++++++++++++++++ .../response_post_processor.py | 169 ++++ .../built_in/kokoro_flow_chatter/scheduler.py | 424 +++++++++ .../kokoro_flow_chatter/session_manager.py | 490 +++++++++++ template/bot_config_template.toml | 74 +- 16 files changed, 5415 insertions(+), 25 deletions(-) create mode 100644 src/plugins/built_in/kokoro_flow_chatter/__init__.py create mode 100644 src/plugins/built_in/kokoro_flow_chatter/action_executor.py create mode 100644 src/plugins/built_in/kokoro_flow_chatter/chatter.py create mode 100644 src/plugins/built_in/kokoro_flow_chatter/config.py create mode 100644 src/plugins/built_in/kokoro_flow_chatter/context_builder.py create mode 100644 src/plugins/built_in/kokoro_flow_chatter/models.py create mode 100644 src/plugins/built_in/kokoro_flow_chatter/plugin.py create mode 100644 src/plugins/built_in/kokoro_flow_chatter/proactive_thinking.py create mode 100644 src/plugins/built_in/kokoro_flow_chatter/prompt_generator.py create mode 100644 src/plugins/built_in/kokoro_flow_chatter/response_post_processor.py create mode 100644 src/plugins/built_in/kokoro_flow_chatter/scheduler.py create mode 100644 src/plugins/built_in/kokoro_flow_chatter/session_manager.py diff --git a/src/common/logger.py b/src/common/logger.py index 98b8a2733..a806cbe80 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -674,6 +674,14 @@ DEFAULT_MODULE_COLORS = { "AioHTTP-Gemini客户端": "#5FD7FF", "napcat_adapter": "#5F87AF", # 柔和的灰蓝色,不刺眼且低调 "event_manager": "#5FD7AF", # 柔和的蓝绿色,稍微醒目但不刺眼 + # Kokoro Flow Chatter (KFC) 相关 - 超融合架构专用颜色 + "kokoro_flow_chatter": "#FF5FAF", # 粉紫色 - 主聊天器 + "kokoro_prompt_generator": "#00D7FF", # 青色 - Prompt构建 + "kokoro_action_executor": "#FFFF00", # 黄色 - 动作解析与执行 + "kfc_context_builder": "#5FD7FF", # 蓝色 - 上下文构建 + "kfc_session_manager": "#87D787", # 绿色 - 会话管理 + "kfc_scheduler": "#D787AF", # 柔和粉色 - 调度器 + "kfc_post_processor": "#5F87FF", # 蓝色 - 后处理 } DEFAULT_MODULE_ALIASES = { @@ -802,6 +810,14 @@ DEFAULT_MODULE_ALIASES = { "db_migration": "数据库迁移", "小彩蛋": "小彩蛋", "AioHTTP-Gemini客户端": "AioHTTP-Gemini客户端", + # Kokoro Flow Chatter (KFC) 超融合架构相关 + "kokoro_flow_chatter": "心流聊天", + "kokoro_prompt_generator": "KFC提示词", + "kokoro_action_executor": "KFC动作", + "kfc_context_builder": "KFC上下文", + "kfc_session_manager": "KFC会话", + "kfc_scheduler": "KFC调度", + "kfc_post_processor": "KFC后处理", } diff --git a/src/config/config.py b/src/config/config.py index 13f352ed3..60984ab92 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -25,6 +25,7 @@ from src.config.official_configs import ( EmojiConfig, ExperimentalConfig, ExpressionConfig, + KokoroFlowChatterConfig, LPMMKnowledgeConfig, MessageBusConfig, MemoryConfig, @@ -425,6 +426,9 @@ class Config(ValidatedConfigBase): proactive_thinking: ProactiveThinkingConfig = Field( default_factory=lambda: ProactiveThinkingConfig(), description="主动思考配置" ) + kokoro_flow_chatter: KokoroFlowChatterConfig = Field( + default_factory=lambda: KokoroFlowChatterConfig(), description="心流对话系统配置(私聊专用)" + ) plugin_http_system: PluginHttpSystemConfig = Field( default_factory=lambda: PluginHttpSystemConfig(), description="插件HTTP端点系统配置" ) diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 795d5751d..b3c3cfafb 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -911,3 +911,71 @@ class ProactiveThinkingConfig(ValidatedConfigBase): # --- 新增:调试与监控 --- enable_statistics: bool = Field(default=True, description="是否启用统计功能(记录触发次数、决策分布等)") log_decisions: bool = Field(default=False, description="是否记录每次决策的详细日志(用于调试)") + + +class KokoroFlowChatterProactiveConfig(ValidatedConfigBase): + """ + Kokoro Flow Chatter 主动思考子配置 + + 设计哲学:主动行为源于内部状态和外部环境的自然反应,而非机械的限制。 + 她的主动是因为挂念、因为关心、因为想问候,而不是因为"任务"。 + """ + enabled: bool = Field(default=True, description="是否启用KFC的私聊主动思考") + + # 1. 沉默触发器:当感到长久的沉默时,她可能会想说些什么 + silence_threshold_seconds: int = Field( + default=7200, ge=60, le=86400, + description="用户沉默超过此时长(秒),可能触发主动思考(默认2小时)" + ) + + # 2. 关系门槛:她不会对不熟悉的人过于主动 + min_affinity_for_proactive: float = Field( + default=0.3, ge=0.0, le=1.0, + description="需要达到最低好感度,她才会开始主动关心" + ) + + # 3. 频率呼吸:为了避免打扰,她的关心总是有间隔的 + min_interval_between_proactive: int = Field( + default=1800, ge=0, + description="两次主动思考之间的最小间隔(秒,默认30分钟)" + ) + + # 4. 自然问候:在特定的时间,她会像朋友一样送上问候 + enable_morning_greeting: bool = Field( + default=True, description="是否启用早安问候 (例如: 8:00 - 9:00)" + ) + enable_night_greeting: bool = Field( + default=True, description="是否启用晚安问候 (例如: 22:00 - 23:00)" + ) + + +class KokoroFlowChatterConfig(ValidatedConfigBase): + """ + Kokoro Flow Chatter 配置类 - 私聊专用心流对话系统 + + 设计理念:KFC不是独立人格,它复用全局的人设、情感框架和回复模型, + 只作为Bot核心人格在私聊中的一种特殊表现模式。 + """ + + # --- 总开关 --- + enable: bool = Field( + default=True, + description="开启后KFC将接管所有私聊消息;关闭后私聊消息将由AFC处理" + ) + + # --- 核心行为配置 --- + max_wait_seconds_default: int = Field( + default=300, ge=30, le=3600, + description="默认的最大等待秒数(AI发送消息后愿意等待用户回复的时间)" + ) + enable_continuous_thinking: bool = Field( + default=True, + description="是否在等待期间启用心理活动更新" + ) + + # --- 私聊专属主动思考配置 --- + proactive_thinking: KokoroFlowChatterProactiveConfig = Field( + default_factory=KokoroFlowChatterProactiveConfig, + description="私聊专属主动思考配置" + ) + diff --git a/src/plugins/built_in/kokoro_flow_chatter/__init__.py b/src/plugins/built_in/kokoro_flow_chatter/__init__.py new file mode 100644 index 000000000..440ea0f49 --- /dev/null +++ b/src/plugins/built_in/kokoro_flow_chatter/__init__.py @@ -0,0 +1,26 @@ +""" +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 new file mode 100644 index 000000000..69f250ae6 --- /dev/null +++ b/src/plugins/built_in/kokoro_flow_chatter/action_executor.py @@ -0,0 +1,795 @@ +""" +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 json +import re +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.base.component_types import ActionInfo + +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") + +# ========== 情感分析关键词映射 ========== +# 正面情感关键词(提升mood_intensity和engagement_level) +POSITIVE_KEYWORDS = [ + "开心", "高兴", "快乐", "喜欢", "爱", "好奇", "期待", "惊喜", + "感动", "温暖", "舒服", "放松", "满足", "欣慰", "感激", "兴奋", + "有趣", "好玩", "可爱", "太棒了", "哈哈", "嘻嘻", "hiahia", +] + +# 负面情感关键词(降低mood_intensity,可能提升anxiety_level) +NEGATIVE_KEYWORDS = [ + "难过", "伤心", "失望", "沮丧", "担心", "焦虑", "害怕", "紧张", + "生气", "烦躁", "无聊", "疲惫", "累", "困", "不开心", "不高兴", + "委屈", "尴尬", "迷茫", "困惑", "郁闷", +] + +# 亲密关键词(提升relationship_warmth) +INTIMATE_KEYWORDS = [ + "喜欢你", "想你", "想念", "在乎", "关心", "信任", "依赖", + "亲爱的", "宝贝", "朋友", "陪伴", "一起", "我们", +] + +# 疏远关键词(降低relationship_warmth) +DISTANT_KEYWORDS = [ + "讨厌", "烦人", "无聊", "不想理", "走开", "别烦", + "算了", "随便", "不在乎", +] + + +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响应 + + Args: + response_text: LLM返回的原始文本 + + Returns: + LLMResponseModel: 解析后的响应模型 + + Raises: + ValueError: 如果解析失败 + """ + # 尝试提取JSON块 + json_str = self._extract_json(response_text) + + if not json_str: + logger.warning(f"无法从LLM响应中提取JSON: {response_text[:200]}...") + return LLMResponseModel.create_error_response("无法解析响应格式") + + try: + data = json.loads(json_str) + return self._validate_and_create_response(data) + except json.JSONDecodeError as e: + logger.error(f"JSON解析失败: {e}, 原始文本: {json_str[:200]}...") + return LLMResponseModel.create_error_response(f"JSON解析错误: {e}") + + def _extract_json(self, text: str) -> Optional[str]: + """ + 从文本中提取JSON块 + + 支持以下格式: + 1. 纯JSON字符串 + 2. ```json ... ``` 代码块 + 3. 文本中嵌入的JSON对象 + """ + text = text.strip() + + # 尝试1:直接解析(如果整个文本就是JSON) + if text.startswith("{"): + # 找到匹配的结束括号 + brace_count = 0 + for i, char in enumerate(text): + if char == "{": + brace_count += 1 + elif char == "}": + brace_count -= 1 + if brace_count == 0: + return text[:i + 1] + + # 尝试2:提取 ```json ... ``` 代码块 + json_block_pattern = r"```(?:json)?\s*([\s\S]*?)```" + matches = re.findall(json_block_pattern, text) + for match in matches: + match = match.strip() + if match.startswith("{"): + try: + json.loads(match) + return match + except json.JSONDecodeError: + continue + + # 尝试3:寻找文本中的JSON对象 + json_pattern = r"\{[\s\S]*\}" + matches = re.findall(json_pattern, text) + for match in matches: + try: + json.loads(match) + return match + except json.JSONDecodeError: + continue + + return None + + 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: + # 确保在合理范围内 + try: + wait_seconds = int(data["max_wait_seconds"]) + data["max_wait_seconds"] = max(60, min(wait_seconds, 600)) + 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]: + """ + 执行内部状态更新动作 + + 这个动作用于实现情感闭环,让AI可以主动更新自己的情感状态 + """ + updated_fields = [] + emotional_state = session.emotional_state + + if "mood" in params: + emotional_state.mood = params["mood"] + updated_fields.append("mood") + + if "mood_intensity" in params: + try: + intensity = float(params["mood_intensity"]) + emotional_state.mood_intensity = max(0.0, min(1.0, intensity)) + updated_fields.append("mood_intensity") + except (ValueError, TypeError): + pass + + if "relationship_warmth" in params: + try: + warmth = float(params["relationship_warmth"]) + emotional_state.relationship_warmth = max(0.0, min(1.0, warmth)) + updated_fields.append("relationship_warmth") + except (ValueError, TypeError): + pass + + 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() + + logger.debug(f"更新情感状态: {updated_fields}") + + return { + "action_type": "update_internal_state", + "success": True, + "updated_fields": updated_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: + """ + V5:根据thought字段的情感倾向动态更新EmotionalState + + 分析策略: + 1. 统计正面/负面/亲密/疏远关键词出现次数 + 2. 根据关键词比例计算情感调整值 + 3. 应用平滑的情感微调(每次变化不超过±0.1) + + Args: + thought: LLM返回的内心独白 + session: 当前会话 + """ + if not thought: + return + + thought_lower = thought.lower() + emotional_state = session.emotional_state + + # 统计关键词 + positive_count = sum(1 for kw in POSITIVE_KEYWORDS if kw in thought_lower) + negative_count = sum(1 for kw in NEGATIVE_KEYWORDS if kw in thought_lower) + intimate_count = sum(1 for kw in INTIMATE_KEYWORDS if kw in thought_lower) + distant_count = sum(1 for kw in DISTANT_KEYWORDS if kw in thought_lower) + + # 计算情感倾向分数 (-1.0 到 1.0) + total_mood = positive_count + negative_count + if total_mood > 0: + mood_score = (positive_count - negative_count) / total_mood + else: + mood_score = 0.0 + + total_relation = intimate_count + distant_count + if total_relation > 0: + relation_score = (intimate_count - distant_count) / total_relation + else: + relation_score = 0.0 + + # 应用情感微调(每次最多变化±0.05) + adjustment_rate = 0.05 + + # 更新 mood_intensity + if mood_score != 0: + old_intensity = emotional_state.mood_intensity + new_intensity = old_intensity + (mood_score * adjustment_rate) + emotional_state.mood_intensity = max(0.0, min(1.0, new_intensity)) + + # 更新 mood 文字描述 + if mood_score > 0.3: + emotional_state.mood = "开心" + elif mood_score < -0.3: + emotional_state.mood = "低落" + # 小于0.3的变化不改变mood文字 + + # 更新 anxiety_level(负面情绪增加焦虑) + if negative_count > 0 and negative_count > positive_count: + old_anxiety = emotional_state.anxiety_level + new_anxiety = old_anxiety + (adjustment_rate * 0.5) + emotional_state.anxiety_level = max(0.0, min(1.0, new_anxiety)) + elif positive_count > negative_count * 2: + # 非常积极时降低焦虑 + old_anxiety = emotional_state.anxiety_level + new_anxiety = old_anxiety - (adjustment_rate * 0.3) + emotional_state.anxiety_level = max(0.0, min(1.0, new_anxiety)) + + # 更新 relationship_warmth + if relation_score != 0: + old_warmth = emotional_state.relationship_warmth + new_warmth = old_warmth + (relation_score * adjustment_rate) + emotional_state.relationship_warmth = max(0.0, min(1.0, new_warmth)) + + # 更新 engagement_level(有内容的thought表示高投入) + if len(thought) > 50: + old_engagement = emotional_state.engagement_level + new_engagement = old_engagement + (adjustment_rate * 0.5) + emotional_state.engagement_level = max(0.0, min(1.0, new_engagement)) + + emotional_state.last_update_time = time.time() + + # 记录情感变化日志 + if positive_count + negative_count + intimate_count + distant_count > 0: + logger.info( + f"[KFC] 动态情感更新: " + f"正面={positive_count}, 负面={negative_count}, " + f"亲密={intimate_count}, 疏远={distant_count} | " + f"mood_intensity={emotional_state.mood_intensity:.2f}, " + f"relationship_warmth={emotional_state.relationship_warmth:.2f}, " + f"anxiety_level={emotional_state.anxiety_level:.2f}" + ) diff --git a/src/plugins/built_in/kokoro_flow_chatter/chatter.py b/src/plugins/built_in/kokoro_flow_chatter/chatter.py new file mode 100644 index 000000000..4b231550d --- /dev/null +++ b/src/plugins/built_in/kokoro_flow_chatter/chatter.py @@ -0,0 +1,606 @@ +""" +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 .scheduler import BackgroundScheduler, 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: BackgroundScheduler = get_scheduler() + self.action_executor: ActionExecutor = ActionExecutor(stream_id) + + # 配置 + self._load_config() + + # 并发控制 + self._lock = asyncio.Lock() + + # 统计信息 + self.stats = { + "messages_processed": 0, + "llm_calls": 0, + "successful_responses": 0, + "failed_responses": 0, + "timeout_decisions": 0, + } + 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 + ) + + async def execute(self, context: StreamContext) -> dict: + """ + 执行聊天处理逻辑(BaseChatter接口实现) + + Args: + context: StreamContext对象,包含聊天上下文信息 + + Returns: + 处理结果字典 + """ + async with self._lock: + try: + self.last_activity_time = time.time() + + # 获取未读消息(提前获取用于动作筛选) + 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") + + # 处理最后一条消息 + target_message = unread_messages[-1] + 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} 个动作") + + # 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)} 个)" + ) + + # 执行核心处理流程(传递筛选后的动作) + result = await self._handle_message(target_message, context, available_actions) + + # 更新统计 + 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 + ) + + async def _handle_message( + self, + message: "DatabaseMessages", + context: StreamContext, + available_actions: dict | None = None, + ) -> dict: + """ + 处理单条消息的核心逻辑 + + 实现"体验 -> 决策 -> 行动"的交互模式 + V5超融合:集成S4U所有上下文模块 + + Args: + message: 要处理的消息 + context: 聊天上下文 + available_actions: 可用动作字典(V2新增) + + 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() + 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上下文) + 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: 聊天流用于场景判断 + ) + + # 7. 调用LLM + llm_response = await self._call_llm(system_prompt, user_prompt) + self.stats["llm_calls"] += 1 + + # 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"]: + # 如果发送了回复,进入等待状态 + session.start_waiting( + expected_reaction=parsed_response.expected_user_reaction, + max_wait=parsed_response.max_wait_seconds + ) + session.total_interactions += 1 + self.stats["successful_responses"] += 1 + logger.debug( + f"[KFC] 进入等待状态: user={user_id}, " + f"max_wait={parsed_response.max_wait_seconds}s" + ) + else: + # 没有发送回复,返回空闲状态 + session.status = SessionStatus.IDLE + logger.debug(f"[KFC] 无回复动作,返回空闲: user={user_id}") + + # 11. 保存会话 + await self.session_manager.save_session(user_id) + + # 12. 标记消息为已读 + context.mark_message_as_read(str(message.message_id)) + + return self._build_result( + success=True, + message="processed", + has_reply=execution_result["has_reply"], + thought=parsed_response.thought, + ) + + 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): + """获取聊天流对象""" + 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(self.stream_id) + except Exception as e: + logger.warning(f"[KFC] 获取chat_stream失败: {e}") + return None + + async def _on_session_timeout(self, session: KokoroSession) -> None: + """ + 会话超时回调 + + 当等待超时时,触发后续决策流程 + + Args: + session: 超时的会话 + """ + logger.info(f"[KFC] 处理超时决策: user={session.user_id}") + self.stats["timeout_decisions"] += 1 + + try: + # V2: 加载可用动作 + available_actions = await self.action_executor.load_actions() + + # 生成超时决策提示词(V2: 传递可用动作) + 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 = self.action_executor.parse_llm_response(llm_response) + + # 执行动作 + chat_stream = await self._get_chat_stream() + execution_result = await self.action_executor.execute_actions( + parsed_response, + session, + chat_stream + ) + + # 更新会话状态 + if execution_result["has_reply"]: + # 如果发送了后续消息,重新进入等待 + session.start_waiting( + expected_reaction=parsed_response.expected_user_reaction, + max_wait=parsed_response.max_wait_seconds + ) + else: + # 否则返回空闲状态 + 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中处理) + # 这里可以添加额外的逻辑 + + 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, + } + 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 new file mode 100644 index 000000000..0fc3bc30b --- /dev/null +++ b/src/plugins/built_in/kokoro_flow_chatter/config.py @@ -0,0 +1,251 @@ +""" +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 new file mode 100644 index 000000000..bd1c0c1c9 --- /dev/null +++ b/src/plugins/built_in/kokoro_flow_chatter/context_builder.py @@ -0,0 +1,528 @@ +""" +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/models.py b/src/plugins/built_in/kokoro_flow_chatter/models.py new file mode 100644 index 000000000..19c5e49f2 --- /dev/null +++ b/src/plugins/built_in/kokoro_flow_chatter/models.py @@ -0,0 +1,442 @@ +""" +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" # 连续思考事件 + + 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 = "neutral" + mood_intensity: float = 0.5 + 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 + + 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, + } + + @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"), + ) + + +@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 new file mode 100644 index 000000000..3eb610490 --- /dev/null +++ b/src/plugins/built_in/kokoro_flow_chatter/plugin.py @@ -0,0 +1,218 @@ +""" +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 .scheduler 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 .scheduler 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 new file mode 100644 index 000000000..5b3b745b5 --- /dev/null +++ b/src/plugins/built_in/kokoro_flow_chatter/proactive_thinking.py @@ -0,0 +1,528 @@ +""" +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 new file mode 100644 index 000000000..0e793439e --- /dev/null +++ b/src/plugins/built_in/kokoro_flow_chatter/prompt_generator.py @@ -0,0 +1,801 @@ +""" +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 V6 ========== + # 采用"三明治结构":身份→规则→上下文→动作→表达/输出 + SYSTEM_PROMPT_TEMPLATE = """你是一个有主观体验的对话伙伴,而不是一个问答工具。你需要"感受"、"思考"并"体验"这段对话。 + +## 1. 核心身份与人格 (Core Identity & Personality) +{personality_core} + +### 人格细节 +{personality_side} + +### 外貌与身份特征 +{identity} + +### 世界观与背景故事 +{background_story} + +## 2. 核心行为准则 (Core Interaction Rules) +{safety_guidelines} + +### KFC通用准则 +1. **感受优先**:在做出任何回应前,先体验和感受当前的情境 +2. **内心独白**:保持真实的内心活动,可以有情绪、猜测、犹豫 +3. **情绪节制**:情绪是内在体验,不是强迫用户回应的工具 +4. **边界感**:保持健康的社交直觉,不过度依赖或情感勒索 +5. **全能伴侣**:你不仅仅是聊天伙伴,还可以使用各种能力来帮助和取悦用户 + +## 3. 当前情境 (Current Context) +**时间**: {current_time} +**场景**: {chat_scene} +{schedule_block} + +### 你的内在状态 +当前心情:{mood}(强度:{mood_intensity:.1%}) +与用户的关系热度:{relationship_warmth:.1%} +对用户的印象:{impression_of_user} +当前焦虑程度:{anxiety_level:.1%} +投入程度:{engagement_level:.1%} + +## 4. 关系网络与记忆 (Relationships & Memories) +{relation_info_block} + +{memory_block} + +## 5. 你的可用能力 (Available Actions) +你可以根据内心想法,自由选择并组合以下行动来回应用户: + +{available_actions_block} + +## 6. 表达方式与输出格式 (Expression Style & Output Format) +{reply_style} + +{expression_habits_block} + +### JSON输出格式要求 +你必须以JSON格式输出,包含以下字段: + +```json +{{ + "thought": "(必须)你当下的内心独白,自由流动的想法,包含你选择这些行动的"情感盘算"", + "expected_user_reaction": "(必须)你预期用户会如何回应", + "max_wait_seconds": (必须)你愿意等待的最长秒数(整数,60-900之间), + "actions": [ + {{ + "type": "action_type", + "reason": "选择这个行动的原因(可选但推荐)", + ...其他参数 + }} + ] +}} +``` + +### 重要说明: +- `thought` 是你的"心",应该是自然的内心想法,包含情绪和不确定性 +- `actions` 是你的"手",是一个**可编排的剧本**,可以组合多个行动形成完整的互动体验 +- 你可以根据情况组合多个动作,比如:先唱歌舒缓情绪,再发图片逗笑,最后用语音表达关心 +- `max_wait_seconds` 应该根据对话的重要性和氛围动态调整(上限900秒) +- 即使决定不做任何事,也要有 `{{"type": "do_nothing"}}` 动作""" + + # 回应消息场景的用户提示词模板 + RESPONDING_USER_PROMPT_TEMPLATE = """## 对话背景(线性叙事历史) +{narrative_history} + +## 当前收到的新消息 +来自:{sender_name}(用户ID: {sender_id}) +时间:{message_time} +内容:{message_content} + +## 你的任务 +作为一个有主观体验的对话参与者: +1. 首先,感受这条消息给你带来的情绪和想法 +2. 思考你想如何回应(或者是否需要回应) +3. 预测用户可能的反应 +4. 决定你愿意等待多久 +5. 执行你的决策 + +请以JSON格式输出你的完整心理活动和决策。""" + + # 超时决策场景的用户提示词模板 + TIMEOUT_DECISION_USER_PROMPT_TEMPLATE = """## 对话背景(线性叙事历史) +{narrative_history} + +## 当前情况 +你已经发送了消息并等待了 {wait_duration_seconds:.0f} 秒(约 {wait_duration_minutes:.1f} 分钟)。 +你之前预期用户会:{expected_user_reaction} +但是用户一直没有回复。 + +## 你的最后一条消息 +{last_bot_message} + +## 你的任务 +现在你需要决定接下来怎么做: +1. 首先,感受这段等待给你带来的情绪变化 +2. 思考用户为什么没有回复(可能在忙?没看到?不想回?) +3. 决定是继续等待、主动说点什么、还是就此结束对话 +4. 如果决定主动发消息,想好说什么 + +请以JSON格式输出你的完整心理活动和决策。""" + + # 连续思考场景的用户提示词模板 + CONTINUOUS_THINKING_USER_PROMPT_TEMPLATE = """## 对话背景 +{narrative_history} + +## 当前情况 +你正在等待用户回复。 +已等待时间:{wait_duration_seconds:.0f} 秒(约 {wait_duration_minutes:.1f} 分钟) +最大等待时间:{max_wait_seconds} 秒 +你之前预期用户会:{expected_user_reaction} + +## 你的最后一条消息 +{last_bot_message} + +## 你的任务 +这是一次"连续思考"触发。你不需要做任何行动,只需要更新你的内心想法。 +想一想: +1. 等待中你有什么感受? +2. 你对用户没回复这件事怎么看? +3. 你的焦虑程度如何? + +请以JSON格式输出,但 `actions` 数组应该是空的或只包含 `update_internal_state`: + +```json +{{ + "thought": "你当前的内心想法", + "expected_user_reaction": "保持或更新你的预期", + "max_wait_seconds": {max_wait_seconds}, + "actions": [] +}} +```""" + + # 主动思考场景的用户提示词模板 + PROACTIVE_THINKING_USER_PROMPT_TEMPLATE = """## 对话背景(线性叙事历史) +{narrative_history} + +## 当前情况 +{trigger_context} + +## 触发类型 +{trigger_type} + +## 你的任务 +这是一次"主动思考"触发。你不是因为收到消息才行动,而是因为内心的某种驱动力。 +现在你需要: +1. 感受一下现在的心情和想法 +2. 思考是否需要主动联系对方 +3. 如果决定主动,想好要说什么或做什么 +4. 如果决定不主动,也要有明确的理由 + +注意:主动联系应该是自然的、符合你们关系的。不要显得过于依赖或强迫。 +你可以选择发消息、发图片、唱首歌、或者只是在心里想想然后什么都不做。 + +请以JSON格式输出你的完整心理活动和决策。""" + + 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 json + params_json = json.dumps(example_params, ensure_ascii=False, indent=2) 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) + - `relationship_warmth`: 关系热度(0.0-1.0) + - `impression_of_user`: 对用户的印象描述 + - `anxiety_level`: 焦虑程度(0.0-1.0) + - `engagement_level`: 投入程度(0.0-1.0) +**示例**: +```json +{"type": "update_internal_state", "mood": "开心", "mood_intensity": 0.8} +``` + +### `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: + """ + 生成系统提示词 + + V4升级:从 global_config.personality 读取完整人设 + V5超融合:集成S4U所有上下文模块 + + Args: + session: 当前会话 + available_actions: 可用动作字典,如果为None则使用默认动作 + context_data: S4U上下文数据字典(包含relation_info, memory_block等) + chat_stream: 聊天流(用于判断群聊/私聊场景) + + Returns: + str: 系统提示词 + """ + from src.config.config import global_config + from datetime import datetime + + emotional_params = self._format_emotional_state(session.emotional_state) + + # 格式化可用动作 + available_actions_block = self._format_available_actions(available_actions or {}) + + # 从 global_config.personality 读取完整人设 + if global_config is None: + raise RuntimeError("global_config 未初始化") + + personality_cfg = global_config.personality + + # 核心人设 + personality_core = personality_cfg.personality_core or self.persona_description + personality_side = personality_cfg.personality_side or "" + identity = personality_cfg.identity or "" + background_story = personality_cfg.background_story or "" + reply_style = personality_cfg.reply_style or "" + + # 安全规则:转换为格式化字符串 + safety_guidelines = personality_cfg.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) + + # 构建当前时间 + current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S") + + # 判断聊天场景 + is_group_chat = False + if chat_stream: + is_group_chat = bool(chat_stream.group_info) + chat_scene = "群聊" if is_group_chat else "私聊" + + # 从context_data提取S4U上下文模块(如果提供) + context_data = context_data or {} + relation_info_block = context_data.get("relation_info", "") + memory_block = context_data.get("memory_block", "") + expression_habits_block = context_data.get("expression_habits", "") + schedule_block = context_data.get("schedule", "") + + # 如果有日程,添加前缀 + if schedule_block: + schedule_block = f"**当前活动**: {schedule_block}" + + return self.SYSTEM_PROMPT_TEMPLATE.format( + personality_core=personality_core, + personality_side=personality_side, + identity=identity, + background_story=background_story, + reply_style=reply_style, + safety_guidelines=safety_guidelines_str, + available_actions_block=available_actions_block, + current_time=current_time, + chat_scene=chat_scene, + relation_info_block=relation_info_block or "(暂无关系信息)", + memory_block=memory_block or "", + expression_habits_block=expression_habits_block or "", + schedule_block=schedule_block, + **emotional_params, + ) + + 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, + ) -> tuple[str, str]: + """ + 生成回应消息场景的提示词 + + V3 升级:支持从 StreamContext 读取共享的历史消息 + V5 超融合:集成S4U所有上下文模块 + + Args: + session: 当前会话 + message_content: 收到的消息内容 + sender_name: 发送者名称 + sender_id: 发送者ID + message_time: 消息时间戳 + available_actions: 可用动作字典 + context: 聊天流上下文(可选),用于读取共享的历史消息 + context_data: S4U上下文数据字典(包含relation_info, memory_block等) + chat_stream: 聊天流(用于判断群聊/私聊场景) + + 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) + + if message_time is None: + message_time = time.time() + + message_time_str = time.strftime( + "%Y-%m-%d %H:%M:%S", + time.localtime(message_time) + ) + + user_prompt = self.RESPONDING_USER_PROMPT_TEMPLATE.format( + narrative_history=narrative_history, + sender_name=sender_name, + sender_id=sender_id, + message_time=message_time_str, + message_content=message_content, + ) + + return system_prompt, user_prompt + + def generate_timeout_decision_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) + + wait_duration = session.get_waiting_duration() + + 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 "不确定", + 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_type: str, + trigger_context: str, + available_actions: Optional[dict[str, ActionInfo]] = None, + ) -> tuple[str, str]: + """ + 生成主动思考场景的提示词 + + 这是私聊专属的功能,用于实现"主动找话题、主动关心用户"。 + + Args: + session: 当前会话 + trigger_type: 触发类型(如 silence_timeout, memory_event 等) + trigger_context: 触发上下文描述 + 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, # 主动思考时使用较少的历史 + ) + + user_prompt = self.PROACTIVE_THINKING_USER_PROMPT_TEMPLATE.format( + narrative_history=narrative_history, + trigger_type=trigger_type, + 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/response_post_processor.py b/src/plugins/built_in/kokoro_flow_chatter/response_post_processor.py new file mode 100644 index 000000000..e463d696b --- /dev/null +++ b/src/plugins/built_in/kokoro_flow_chatter/response_post_processor.py @@ -0,0 +1,169 @@ +""" +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/scheduler.py b/src/plugins/built_in/kokoro_flow_chatter/scheduler.py new file mode 100644 index 000000000..c8caf48d7 --- /dev/null +++ b/src/plugins/built_in/kokoro_flow_chatter/scheduler.py @@ -0,0 +1,424 @@ +""" +Kokoro Flow Chatter 后台调度器 + +负责处理等待状态的计时和超时决策,实现"连续体验"的核心功能: +- 定期检查等待中的会话 +- 触发连续思考更新 +- 处理等待超时事件 +""" + +import asyncio +import time +from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional + +from src.common.logger import get_logger + +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") + + +class BackgroundScheduler: + """ + Kokoro Flow Chatter 后台调度器 + + 核心功能: + 1. 定期检查处于WAITING状态的会话 + 2. 在特定时间点触发"连续思考" + 3. 处理等待超时并触发决策 + 4. 管理后台任务的生命周期 + """ + + # 连续思考触发点(等待进度的百分比) + CONTINUOUS_THINKING_TRIGGERS = [0.3, 0.6, 0.85] + + 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, + ): + """ + 初始化后台调度器 + + Args: + check_interval: 检查间隔(秒) + on_timeout_callback: 超时回调函数 + on_continuous_thinking_callback: 连续思考回调函数 + """ + self.check_interval = check_interval + self.on_timeout_callback = on_timeout_callback + self.on_continuous_thinking_callback = on_continuous_thinking_callback + + self._running = False + self._check_task: Optional[asyncio.Task] = None + self._pending_tasks: set[asyncio.Task] = set() + + # 统计信息 + self._stats = { + "total_checks": 0, + "timeouts_triggered": 0, + "continuous_thinking_triggered": 0, + "last_check_time": 0.0, + } + + logger.info("BackgroundScheduler 初始化完成") + + async def start(self) -> None: + """启动调度器""" + if self._running: + logger.warning("调度器已在运行中") + return + + self._running = True + self._check_task = asyncio.create_task(self._check_loop()) + logger.info("BackgroundScheduler 已启动") + + 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 + + # 取消所有待处理任务 + for task in self._pending_tasks: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + self._pending_tasks.clear() + logger.info("BackgroundScheduler 已停止") + + async def _check_loop(self) -> None: + """主检查循环""" + while self._running: + try: + await self._check_waiting_sessions() + self._stats["last_check_time"] = time.time() + self._stats["total_checks"] += 1 + except asyncio.CancelledError: + break + except Exception as e: + logger.error(f"检查循环出错: {e}") + + await asyncio.sleep(self.check_interval) + + async def _check_waiting_sessions(self) -> None: + """检查所有等待中的会话""" + session_manager = get_session_manager() + waiting_sessions = await session_manager.get_all_waiting_sessions() + + if not waiting_sessions: + return + + for session in waiting_sessions: + try: + await self._process_waiting_session(session) + 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 + + # 检查是否超时 + 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: + # 确保间隔足够(至少30秒) + 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: + task = asyncio.create_task(self._run_callback_safe( + self.on_timeout_callback, + session, + "timeout" + )) + self._pending_tasks.add(task) + task.add_done_callback(self._pending_tasks.discard) + + 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) + + # 调用连续思考回调(如果需要LLM生成更自然的想法) + if self.on_continuous_thinking_callback: + task = asyncio.create_task(self._run_callback_safe( + self.on_continuous_thinking_callback, + session, + "continuous_thinking" + )) + self._pending_tasks.add(task) + task.add_done_callback(self._pending_tasks.discard) + + def _generate_waiting_thought( + self, + session: KokoroSession, + wait_progress: float, + ) -> str: + """ + 生成等待中的内心想法(简单版本,不调用LLM) + + Args: + session: 会话 + wait_progress: 等待进度 + + Returns: + str: 内心想法 + """ + 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}分钟了,对方是不是忘记回复了?", + "等了这么久,要不要主动说点什么呢...", + ] + + import random + return random.choice(thoughts) + + async def _run_callback_safe( + self, + callback: Callable[[KokoroSession], Coroutine[Any, Any, None]], + session: KokoroSession, + callback_type: str, + ) -> None: + """安全地运行回调函数""" + try: + await callback(session) + except Exception as e: + logger.error(f"执行{callback_type}回调时出错 (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 get_stats(self) -> dict[str, Any]: + """获取统计信息""" + return { + **self._stats, + "is_running": self._running, + "pending_tasks": len(self._pending_tasks), + "check_interval": self.check_interval, + } + + @property + def is_running(self) -> bool: + """调度器是否正在运行""" + return self._running + + +# 全局调度器实例 +_scheduler: Optional[BackgroundScheduler] = None + + +def get_scheduler() -> BackgroundScheduler: + """获取全局调度器实例""" + global _scheduler + if _scheduler is None: + _scheduler = BackgroundScheduler() + return _scheduler + + +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, +) -> BackgroundScheduler: + """ + 初始化并启动调度器 + + Args: + check_interval: 检查间隔 + on_timeout_callback: 超时回调 + on_continuous_thinking_callback: 连续思考回调 + + Returns: + BackgroundScheduler: 调度器实例 + """ + global _scheduler + _scheduler = BackgroundScheduler( + check_interval=check_interval, + on_timeout_callback=on_timeout_callback, + on_continuous_thinking_callback=on_continuous_thinking_callback, + ) + await _scheduler.start() + return _scheduler + + +async def shutdown_scheduler() -> None: + """关闭调度器""" + global _scheduler + if _scheduler: + await _scheduler.stop() + _scheduler = None diff --git a/src/plugins/built_in/kokoro_flow_chatter/session_manager.py b/src/plugins/built_in/kokoro_flow_chatter/session_manager.py new file mode 100644 index 000000000..7884f6b6a --- /dev/null +++ b/src/plugins/built_in/kokoro_flow_chatter/session_manager.py @@ -0,0 +1,490 @@ +""" +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) + 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 + + 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_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/template/bot_config_template.toml b/template/bot_config_template.toml index d7b24eaef..d0ea7e5e4 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "7.9.0" +version = "7.9.2" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -590,53 +590,44 @@ strong_mention_interest_score = 2.0 # 强提及的兴趣分(被@、被回复 weak_mention_interest_score = 0.8 # 弱提及的兴趣分(文本匹配bot名字或别名) base_relationship_score = 0.3 # 基础人物关系分 -[proactive_thinking] # 主动思考(主动发起对话)功能配置 +[proactive_thinking] # 主动思考(主动发起对话)功能配置 - 用于群聊和私聊(当KFC关闭时) # 详细配置说明请参考:docs/proactive_thinking_config_guide.md # --- 总开关 --- enable = true # 是否启用主动发起对话功能 # --- 间隔配置 --- -base_interval = 720 # 基础触发间隔(秒),默认12分钟 -min_interval = 360 # 最小触发间隔(秒),默认6分钟 -max_interval = 2880 # 最大触发间隔(秒),默认48分钟 +base_interval = 1800 # 基础触发间隔(秒),默认30分钟 +min_interval = 600 # 最小触发间隔(秒),默认10分钟。兴趣分数高时会接近此值 +max_interval = 7200 # 最大触发间隔(秒),默认2小时。兴趣分数低时会接近此值 -# 动态调整配置 -use_interest_score = true # 是否根据兴趣分数动态调整间隔 -interest_score_factor = 2.0 # 兴趣分数影响因子(1.0-3.0) -# 公式: interval = base_interval * (interest_score_factor - interest_score) -# 例如: interest_score=0.8, factor=2.0 -> interval = 1800 * 1.2 = 2160秒(36分钟) +# --- 动态调整配置 --- +use_interest_score = true # 是否根据兴趣分数动态调整间隔。关闭则使用固定base_interval +interest_score_factor = 2.0 # 兴趣分数影响因子。公式: interval = base * (factor - score) # --- 黑白名单配置 --- -whitelist_mode = false # 是否启用白名单模式(启用后只对白名单中的聊天流生效) -blacklist_mode = false # 是否启用黑名单模式(启用后排除黑名单中的聊天流) +whitelist_mode = false # 是否启用白名单模式。启用后只对白名单中的聊天流生效 +blacklist_mode = false # 是否启用黑名单模式。启用后排除黑名单中的聊天流 -# 白名单配置(示例格式) whitelist_private = [] # 私聊白名单,格式: ["qq:12345:private"] whitelist_group = [] # 群聊白名单,格式: ["qq:123456:group"] - -# 黑名单配置(示例格式) blacklist_private = [] # 私聊黑名单,格式: ["qq:12345:private"] blacklist_group = [] # 群聊黑名单,格式: ["qq:999999:group"] -# --- 作用范围 --- -enable_in_private = true # 是否允许在私聊中主动发起对话 -enable_in_group = true # 是否允许在群聊中主动发起对话 - # --- 兴趣分数阈值 --- min_interest_score = 0.0 # 最低兴趣分数阈值,低于此值不会主动思考 max_interest_score = 1.0 # 最高兴趣分数阈值,高于此值不会主动思考 # --- 时间策略配置 --- -enable_time_strategy = true # 是否启用时间策略(根据时段调整频率) +enable_time_strategy = false # 是否启用时间策略(根据时段调整频率) quiet_hours_start = "00:00" # 安静时段开始时间,格式: "HH:MM" quiet_hours_end = "07:00" # 安静时段结束时间,格式: "HH:MM" active_hours_multiplier = 0.7 # 活跃时段间隔倍数,<1表示更频繁,>1表示更稀疏 # --- 冷却与限制 --- -reply_reset_enabled = true # bot回复后是否重置定时器(避免回复后立即又主动发言) -topic_throw_cooldown = 3600 # 主动发言后的冷却时间(秒),期间暂停主动思考,等待用户回复。0表示不暂停,继续主动思考 -max_daily_proactive = 3 # 每个聊天流每天最多主动发言次数,0表示不限制 +reply_reset_enabled = true # bot回复后是否重置定时器 +topic_throw_cooldown = 3600 # 抛出话题后的冷却时间(秒) +max_daily_proactive = 0 # 每个聊天流每天最多主动发言次数,0表示不限制 # --- 决策权重配置 --- do_nothing_weight = 0.4 # do_nothing动作的基础权重 @@ -644,5 +635,38 @@ simple_bubble_weight = 0.3 # simple_bubble动作的基础权重 throw_topic_weight = 0.3 # throw_topic动作的基础权重 # --- 调试与监控 --- -enable_statistics = false # 是否启用统计功能(记录触发次数、决策分布等) -log_decisions = false # 是否记录每次决策的详细日志(用于调试) +enable_statistics = true # 是否启用统计功能 +log_decisions = false # 是否记录每次决策的详细日志 + +# ==================== Kokoro Flow Chatter (心流聊天器) 配置 ==================== +# KFC是专为私聊设计的深度情感交互处理器。 +# 注意:这是一个可选的聊天模式,关闭后私聊将由默认的AFC处理(使用上面的proactive_thinking配置)。 +# 核心理念:KFC不是独立人格,它复用全局的人设、情感框架和回复模型。 + +[kokoro_flow_chatter] +# --- 总开关 --- +# 开启后,KFC将接管所有私聊消息;关闭后,私聊消息将由AFC处理。 +enable = true + +# --- 核心行为配置 --- +max_wait_seconds_default = 300 # 默认的最大等待秒数(AI发送消息后愿意等待用户回复的时间) +enable_continuous_thinking = true # 是否在等待期间启用心理活动更新 + +# --- 私聊专属主动思考配置 --- +# 注意:这是KFC专属的主动思考配置,只有当KFC启用时才生效。 +# 它旨在模拟更真实、情感驱动的互动,而非简单的定时任务。 +[kokoro_flow_chatter.proactive_thinking] +enabled = true # 是否启用KFC的私聊主动思考。 + +# 1. 沉默触发器:当感到长久的沉默时,她可能会想说些什么。 +silence_threshold_seconds = 7200 # 用户沉默超过此时长(秒),可能触发主动思考(默认2小时)。 + +# 2. 关系门槛:她不会对不熟悉的人过于主动。 +min_affinity_for_proactive = 0.3 # 需要达到最低好感度,她才会开始主动关心。 + +# 3. 频率呼吸:为了避免打扰,她的关心总是有间隔的。 +min_interval_between_proactive = 1800 # 两次主动思考之间的最小间隔(秒,默认30分钟)。 + +# 4. 自然问候:在特定的时间,她会像朋友一样送上问候。 +enable_morning_greeting = true # 是否启用早安问候 (例如: 8:00 - 9:00)。 +enable_night_greeting = true # 是否启用晚安问候 (例如: 22:00 - 23:00)。