feat: 使用提示管理和会话处理来实现Kokoro Flow Chatter V2
- 在Kokoro Flow Chatter V2中添加提示模块以管理提示信息。 - 创建一个构建器,用于根据用户交互和会话上下文构建提示。 - 为不同场景(新消息、及时回复等)注册各种提示模板。 - 开发一个回复模块,使用LLM API生成回复。 - 实现会话管理以处理用户交互并维护状态。 - 引入心理日志条目以追踪用户与机器人的交互情况。 - 确保各模块中都有适当的日志记录和错误处理。
This commit is contained in:
66
src/plugins/built_in/kokoro_flow_chatter_v2/__init__.py
Normal file
66
src/plugins/built_in/kokoro_flow_chatter_v2/__init__.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
Kokoro Flow Chatter V2 - 私聊特化的心流聊天器
|
||||
|
||||
重构版本,核心设计理念:
|
||||
1. Chatter 职责极简化:只负责"收到消息 → 规划执行"
|
||||
2. Session 状态简化:只有 IDLE 和 WAITING 两种状态
|
||||
3. 独立的 Replyer:专属的提示词构建和 LLM 交互
|
||||
4. 独立的主动思考器:负责等待管理和主动发起
|
||||
5. 大模板 + 小模板:线性叙事风格的提示词架构
|
||||
"""
|
||||
|
||||
from .models import (
|
||||
EventType,
|
||||
SessionStatus,
|
||||
MentalLogEntry,
|
||||
WaitingConfig,
|
||||
ActionModel,
|
||||
LLMResponse,
|
||||
)
|
||||
from .session import KokoroSession, SessionManager, get_session_manager
|
||||
from .chatter import KokoroFlowChatterV2
|
||||
from .replyer import generate_response
|
||||
from .action_executor import ActionExecutor
|
||||
from .proactive_thinker import (
|
||||
ProactiveThinker,
|
||||
get_proactive_thinker,
|
||||
start_proactive_thinker,
|
||||
stop_proactive_thinker,
|
||||
)
|
||||
from .config import (
|
||||
KokoroFlowChatterV2Config,
|
||||
get_config,
|
||||
load_config,
|
||||
reload_config,
|
||||
)
|
||||
from .plugin import KokoroFlowChatterV2Plugin
|
||||
|
||||
__all__ = [
|
||||
# Models
|
||||
"EventType",
|
||||
"SessionStatus",
|
||||
"MentalLogEntry",
|
||||
"WaitingConfig",
|
||||
"ActionModel",
|
||||
"LLMResponse",
|
||||
# Session
|
||||
"KokoroSession",
|
||||
"SessionManager",
|
||||
"get_session_manager",
|
||||
# Core Components
|
||||
"KokoroFlowChatterV2",
|
||||
"generate_response",
|
||||
"ActionExecutor",
|
||||
# Proactive Thinker
|
||||
"ProactiveThinker",
|
||||
"get_proactive_thinker",
|
||||
"start_proactive_thinker",
|
||||
"stop_proactive_thinker",
|
||||
# Config
|
||||
"KokoroFlowChatterV2Config",
|
||||
"get_config",
|
||||
"load_config",
|
||||
"reload_config",
|
||||
# Plugin
|
||||
"KokoroFlowChatterV2Plugin",
|
||||
]
|
||||
228
src/plugins/built_in/kokoro_flow_chatter_v2/action_executor.py
Normal file
228
src/plugins/built_in/kokoro_flow_chatter_v2/action_executor.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""
|
||||
Kokoro Flow Chatter V2 - 动作执行器
|
||||
|
||||
负责执行 LLM 决策的动作
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from src.chat.planner_actions.action_manager import ChatterActionManager
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.apis import send_api
|
||||
|
||||
from .models import ActionModel, LLMResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
|
||||
logger = get_logger("kfc_v2_action_executor")
|
||||
|
||||
|
||||
class ActionExecutor:
|
||||
"""
|
||||
动作执行器
|
||||
|
||||
职责:
|
||||
- 执行 reply、poke_user 等动作
|
||||
- 通过 ActionManager 执行动态注册的动作
|
||||
"""
|
||||
|
||||
# 内置动作(不通过 ActionManager)
|
||||
BUILTIN_ACTIONS = {"reply", "do_nothing"}
|
||||
|
||||
def __init__(self, stream_id: str):
|
||||
self.stream_id = stream_id
|
||||
self._action_manager = ChatterActionManager()
|
||||
self._available_actions: dict = {}
|
||||
|
||||
# 统计
|
||||
self._stats = {
|
||||
"total_executed": 0,
|
||||
"successful": 0,
|
||||
"failed": 0,
|
||||
}
|
||||
|
||||
async def load_actions(self) -> dict:
|
||||
"""加载可用动作"""
|
||||
await self._action_manager.load_actions(self.stream_id)
|
||||
self._available_actions = self._action_manager.get_using_actions()
|
||||
logger.debug(f"[ActionExecutor] 加载了 {len(self._available_actions)} 个动作")
|
||||
return self._available_actions
|
||||
|
||||
def get_available_actions(self) -> dict:
|
||||
"""获取可用动作"""
|
||||
return self._available_actions.copy()
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
response: LLMResponse,
|
||||
chat_stream: Optional["ChatStream"],
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
执行动作列表
|
||||
|
||||
Args:
|
||||
response: LLM 响应
|
||||
chat_stream: 聊天流
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
results = []
|
||||
has_reply = False
|
||||
reply_content = ""
|
||||
|
||||
for action in response.actions:
|
||||
try:
|
||||
result = await self._execute_action(action, chat_stream)
|
||||
results.append(result)
|
||||
|
||||
if result.get("success"):
|
||||
self._stats["successful"] += 1
|
||||
if action.type in ("reply", "respond"):
|
||||
has_reply = True
|
||||
reply_content = action.params.get("content", "")
|
||||
else:
|
||||
self._stats["failed"] += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ActionExecutor] 执行动作失败 {action.type}: {e}")
|
||||
results.append({
|
||||
"action_type": action.type,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
})
|
||||
self._stats["failed"] += 1
|
||||
|
||||
self._stats["total_executed"] += 1
|
||||
|
||||
return {
|
||||
"success": all(r.get("success", False) for r in results),
|
||||
"results": results,
|
||||
"has_reply": has_reply,
|
||||
"reply_content": reply_content,
|
||||
}
|
||||
|
||||
async def _execute_action(
|
||||
self,
|
||||
action: ActionModel,
|
||||
chat_stream: Optional["ChatStream"],
|
||||
) -> dict[str, Any]:
|
||||
"""执行单个动作"""
|
||||
action_type = action.type
|
||||
|
||||
if action_type == "reply":
|
||||
return await self._execute_reply(action, chat_stream)
|
||||
|
||||
elif action_type == "do_nothing":
|
||||
logger.debug("[ActionExecutor] 执行 do_nothing")
|
||||
return {"action_type": "do_nothing", "success": True}
|
||||
|
||||
elif action_type == "poke_user":
|
||||
return await self._execute_via_manager(action, chat_stream)
|
||||
|
||||
elif action_type in self._available_actions:
|
||||
return await self._execute_via_manager(action, chat_stream)
|
||||
|
||||
else:
|
||||
logger.warning(f"[ActionExecutor] 未知动作类型: {action_type}")
|
||||
return {
|
||||
"action_type": action_type,
|
||||
"success": False,
|
||||
"error": f"未知动作类型: {action_type}",
|
||||
}
|
||||
|
||||
async def _execute_reply(
|
||||
self,
|
||||
action: ActionModel,
|
||||
chat_stream: Optional["ChatStream"],
|
||||
) -> dict[str, Any]:
|
||||
"""执行回复动作"""
|
||||
content = action.params.get("content", "")
|
||||
|
||||
if not content:
|
||||
return {
|
||||
"action_type": "reply",
|
||||
"success": False,
|
||||
"error": "回复内容为空",
|
||||
}
|
||||
|
||||
try:
|
||||
# 消息后处理(分割、错别字等)
|
||||
processed_messages = await self._process_reply_content(content)
|
||||
|
||||
all_success = True
|
||||
for msg in processed_messages:
|
||||
success = await send_api.text_to_stream(
|
||||
text=msg,
|
||||
stream_id=self.stream_id,
|
||||
typing=True,
|
||||
)
|
||||
if not success:
|
||||
all_success = False
|
||||
|
||||
return {
|
||||
"action_type": "reply",
|
||||
"success": all_success,
|
||||
"reply_text": content,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ActionExecutor] 发送回复失败: {e}")
|
||||
return {
|
||||
"action_type": "reply",
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
async def _process_reply_content(self, content: str) -> list[str]:
|
||||
"""处理回复内容(分割、错别字等)"""
|
||||
try:
|
||||
# 复用 v1 的后处理器
|
||||
from src.plugins.built_in.kokoro_flow_chatter.response_post_processor import (
|
||||
process_reply_content,
|
||||
)
|
||||
|
||||
messages = await process_reply_content(content)
|
||||
return messages if messages else [content]
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[ActionExecutor] 消息处理失败,使用原始内容: {e}")
|
||||
return [content]
|
||||
|
||||
async def _execute_via_manager(
|
||||
self,
|
||||
action: ActionModel,
|
||||
chat_stream: Optional["ChatStream"],
|
||||
) -> dict[str, Any]:
|
||||
"""通过 ActionManager 执行动作"""
|
||||
try:
|
||||
result = await self._action_manager.execute_action(
|
||||
action_name=action.type,
|
||||
chat_id=self.stream_id,
|
||||
target_message=None,
|
||||
reasoning=f"KFC决策: {action.type}",
|
||||
action_data=action.params,
|
||||
thinking_id=None,
|
||||
log_prefix="[KFC V2]",
|
||||
)
|
||||
|
||||
return {
|
||||
"action_type": action.type,
|
||||
"success": result.get("success", False),
|
||||
"result": result,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ActionExecutor] ActionManager 执行失败: {e}")
|
||||
return {
|
||||
"action_type": action.type,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""获取统计信息"""
|
||||
return self._stats.copy()
|
||||
263
src/plugins/built_in/kokoro_flow_chatter_v2/chatter.py
Normal file
263
src/plugins/built_in/kokoro_flow_chatter_v2/chatter.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""
|
||||
Kokoro Flow Chatter V2 - Chatter 主类
|
||||
|
||||
极简设计,只负责:
|
||||
1. 收到消息
|
||||
2. 调用 Replyer 生成响应
|
||||
3. 执行动作
|
||||
4. 更新 Session
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional
|
||||
|
||||
from src.common.data_models.message_manager_data_model import StreamContext
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.base.base_chatter import BaseChatter
|
||||
from src.plugin_system.base.component_types import ChatType
|
||||
|
||||
from .action_executor import ActionExecutor
|
||||
from .models import EventType, SessionStatus
|
||||
from .replyer import generate_response
|
||||
from .session import get_session_manager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.chat.planner_actions.action_manager import ChatterActionManager
|
||||
from src.common.data_models.database_data_model import DatabaseMessages
|
||||
|
||||
logger = get_logger("kfc_v2_chatter")
|
||||
|
||||
# 控制台颜色
|
||||
SOFT_PURPLE = "\033[38;5;183m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
class KokoroFlowChatterV2(BaseChatter):
|
||||
"""
|
||||
Kokoro Flow Chatter V2 - 私聊特化的心流聊天器
|
||||
|
||||
核心设计:
|
||||
- Chatter 只负责 "收到消息 → 规划执行" 的流程
|
||||
- 无论 Session 之前是什么状态,流程都一样
|
||||
- 区别只体现在提示词中
|
||||
|
||||
不负责:
|
||||
- 等待超时处理(由 ProactiveThinker 负责)
|
||||
- 连续思考(由 ProactiveThinker 负责)
|
||||
- 主动发起对话(由 ProactiveThinker 负责)
|
||||
"""
|
||||
|
||||
chatter_name: str = "KokoroFlowChatterV2"
|
||||
chatter_description: str = "心流聊天器 V2 - 私聊特化的深度情感交互处理器"
|
||||
chat_types: ClassVar[list[ChatType]] = [ChatType.PRIVATE]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stream_id: str,
|
||||
action_manager: "ChatterActionManager",
|
||||
plugin_config: dict | None = None,
|
||||
):
|
||||
super().__init__(stream_id, action_manager, plugin_config)
|
||||
|
||||
# 核心组件
|
||||
self.session_manager = get_session_manager()
|
||||
self.action_executor = ActionExecutor(stream_id)
|
||||
|
||||
# 并发控制
|
||||
self._lock = asyncio.Lock()
|
||||
self._processing = False
|
||||
|
||||
# 统计
|
||||
self._stats = {
|
||||
"messages_processed": 0,
|
||||
"successful_responses": 0,
|
||||
"failed_responses": 0,
|
||||
}
|
||||
|
||||
logger.info(f"{SOFT_PURPLE}[KFC V2]{RESET} 初始化完成: stream_id={stream_id}")
|
||||
|
||||
async def execute(self, context: StreamContext) -> dict:
|
||||
"""
|
||||
执行聊天处理
|
||||
|
||||
流程:
|
||||
1. 获取 Session
|
||||
2. 获取未读消息
|
||||
3. 记录用户消息到 mental_log
|
||||
4. 确定 situation_type(根据之前的等待状态)
|
||||
5. 调用 Replyer 生成响应
|
||||
6. 执行动作
|
||||
7. 更新 Session(记录 Bot 规划,设置等待状态)
|
||||
8. 保存 Session
|
||||
"""
|
||||
async with self._lock:
|
||||
self._processing = True
|
||||
|
||||
try:
|
||||
# 1. 获取未读消息
|
||||
unread_messages = context.get_unread_messages()
|
||||
if not unread_messages:
|
||||
return self._build_result(success=True, message="no_unread_messages")
|
||||
|
||||
# 2. 取最后一条消息作为主消息
|
||||
target_message = unread_messages[-1]
|
||||
user_info = target_message.user_info
|
||||
|
||||
if not user_info:
|
||||
return self._build_result(success=False, message="no_user_info")
|
||||
|
||||
user_id = str(user_info.user_id)
|
||||
user_name = user_info.user_nickname or user_id
|
||||
|
||||
# 3. 获取或创建 Session
|
||||
session = await self.session_manager.get_session(user_id, self.stream_id)
|
||||
|
||||
# 4. 确定 situation_type(根据之前的等待状态)
|
||||
situation_type = self._determine_situation_type(session)
|
||||
|
||||
# 5. 记录用户消息到 mental_log
|
||||
for msg in unread_messages:
|
||||
msg_content = msg.processed_plain_text or msg.display_message or ""
|
||||
msg_user_name = msg.user_info.user_nickname if msg.user_info else user_name
|
||||
msg_user_id = str(msg.user_info.user_id) if msg.user_info else user_id
|
||||
|
||||
session.add_user_message(
|
||||
content=msg_content,
|
||||
user_name=msg_user_name,
|
||||
user_id=msg_user_id,
|
||||
timestamp=msg.time,
|
||||
)
|
||||
|
||||
# 6. 加载可用动作
|
||||
await self.action_executor.load_actions()
|
||||
available_actions = self.action_executor.get_available_actions()
|
||||
|
||||
# 7. 获取聊天流
|
||||
chat_stream = await self._get_chat_stream()
|
||||
|
||||
# 8. 调用 Replyer 生成响应
|
||||
response = await generate_response(
|
||||
session=session,
|
||||
user_name=user_name,
|
||||
situation_type=situation_type,
|
||||
chat_stream=chat_stream,
|
||||
available_actions=available_actions,
|
||||
)
|
||||
|
||||
# 9. 执行动作
|
||||
exec_result = await self.action_executor.execute(response, chat_stream)
|
||||
|
||||
# 10. 记录 Bot 规划到 mental_log
|
||||
session.add_bot_planning(
|
||||
thought=response.thought,
|
||||
actions=[a.to_dict() for a in response.actions],
|
||||
expected_reaction=response.expected_reaction,
|
||||
max_wait_seconds=response.max_wait_seconds,
|
||||
)
|
||||
|
||||
# 11. 更新 Session 状态
|
||||
if response.max_wait_seconds > 0:
|
||||
session.start_waiting(
|
||||
expected_reaction=response.expected_reaction,
|
||||
max_wait_seconds=response.max_wait_seconds,
|
||||
)
|
||||
else:
|
||||
session.end_waiting()
|
||||
|
||||
# 12. 标记消息为已读
|
||||
for msg in unread_messages:
|
||||
context.mark_message_as_read(str(msg.message_id))
|
||||
|
||||
# 13. 保存 Session
|
||||
await self.session_manager.save_session(user_id)
|
||||
|
||||
# 14. 更新统计
|
||||
self._stats["messages_processed"] += len(unread_messages)
|
||||
if exec_result.get("has_reply"):
|
||||
self._stats["successful_responses"] += 1
|
||||
|
||||
logger.info(
|
||||
f"{SOFT_PURPLE}[KFC V2]{RESET} 处理完成: "
|
||||
f"user={user_name}, situation={situation_type}, "
|
||||
f"actions={[a.type for a in response.actions]}, "
|
||||
f"wait={response.max_wait_seconds}s"
|
||||
)
|
||||
|
||||
return self._build_result(
|
||||
success=True,
|
||||
message="processed",
|
||||
has_reply=exec_result.get("has_reply", False),
|
||||
thought=response.thought,
|
||||
situation_type=situation_type,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self._stats["failed_responses"] += 1
|
||||
logger.error(f"[KFC V2] 处理失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return self._build_result(success=False, message=str(e), error=True)
|
||||
|
||||
finally:
|
||||
self._processing = False
|
||||
|
||||
def _determine_situation_type(self, session) -> str:
|
||||
"""
|
||||
确定当前情况类型
|
||||
|
||||
根据 Session 之前的状态决定提示词的 situation_type
|
||||
"""
|
||||
if session.status == SessionStatus.WAITING:
|
||||
# 之前在等待
|
||||
if session.waiting_config.is_timeout():
|
||||
# 超时了才收到回复
|
||||
return "reply_late"
|
||||
else:
|
||||
# 在预期内收到回复
|
||||
return "reply_in_time"
|
||||
else:
|
||||
# 之前是 IDLE
|
||||
return "new_message"
|
||||
|
||||
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 V2] 获取 chat_stream 失败: {e}")
|
||||
return None
|
||||
|
||||
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,
|
||||
"action_executor_stats": self.action_executor.get_stats(),
|
||||
}
|
||||
|
||||
@property
|
||||
def is_processing(self) -> bool:
|
||||
"""是否正在处理"""
|
||||
return self._processing
|
||||
221
src/plugins/built_in/kokoro_flow_chatter_v2/config.py
Normal file
221
src/plugins/built_in/kokoro_flow_chatter_v2/config.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
Kokoro Flow Chatter V2 - 配置
|
||||
|
||||
可以通过 TOML 配置文件覆盖默认值
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class WaitingDefaults:
|
||||
"""等待配置默认值"""
|
||||
|
||||
# 默认最大等待时间(秒)
|
||||
default_max_wait_seconds: int = 300
|
||||
|
||||
# 最小等待时间
|
||||
min_wait_seconds: int = 30
|
||||
|
||||
# 最大等待时间
|
||||
max_wait_seconds: int = 1800
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProactiveConfig:
|
||||
"""主动思考配置"""
|
||||
|
||||
# 是否启用主动思考
|
||||
enabled: bool = True
|
||||
|
||||
# 沉默阈值(秒),超过此时间考虑主动发起
|
||||
silence_threshold_seconds: int = 7200
|
||||
|
||||
# 两次主动发起最小间隔(秒)
|
||||
min_interval_between_proactive: int = 1800
|
||||
|
||||
# 勿扰时段开始(HH:MM 格式)
|
||||
quiet_hours_start: str = "23:00"
|
||||
|
||||
# 勿扰时段结束
|
||||
quiet_hours_end: str = "07:00"
|
||||
|
||||
# 主动发起概率(0.0 ~ 1.0)
|
||||
trigger_probability: float = 0.3
|
||||
|
||||
|
||||
@dataclass
|
||||
class PromptConfig:
|
||||
"""提示词配置"""
|
||||
|
||||
# 活动记录保留条数
|
||||
max_activity_entries: int = 30
|
||||
|
||||
# 每条记录最大字符数
|
||||
max_entry_length: int = 500
|
||||
|
||||
# 是否包含人物关系信息
|
||||
include_relation: bool = True
|
||||
|
||||
# 是否包含记忆信息
|
||||
include_memory: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionConfig:
|
||||
"""会话配置"""
|
||||
|
||||
# Session 持久化目录(相对于 data/)
|
||||
session_dir: str = "kokoro_flow_chatter_v2/sessions"
|
||||
|
||||
# Session 自动过期时间(秒),超过此时间未活动自动清理
|
||||
session_expire_seconds: int = 86400 * 7 # 7 天
|
||||
|
||||
# 活动记录保留上限
|
||||
max_mental_log_entries: int = 100
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMConfig:
|
||||
"""LLM 配置"""
|
||||
|
||||
# 模型名称(空则使用默认)
|
||||
model_name: str = ""
|
||||
|
||||
# Temperature
|
||||
temperature: float = 0.8
|
||||
|
||||
# 最大 Token
|
||||
max_tokens: int = 1024
|
||||
|
||||
# 请求超时(秒)
|
||||
timeout: float = 60.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class KokoroFlowChatterV2Config:
|
||||
"""Kokoro Flow Chatter V2 总配置"""
|
||||
|
||||
# 是否启用
|
||||
enabled: bool = True
|
||||
|
||||
# 启用的消息源类型(空列表表示全部)
|
||||
enabled_stream_types: List[str] = field(default_factory=lambda: ["private"])
|
||||
|
||||
# 等待配置
|
||||
waiting: WaitingDefaults = field(default_factory=WaitingDefaults)
|
||||
|
||||
# 主动思考配置
|
||||
proactive: ProactiveConfig = field(default_factory=ProactiveConfig)
|
||||
|
||||
# 提示词配置
|
||||
prompt: PromptConfig = field(default_factory=PromptConfig)
|
||||
|
||||
# 会话配置
|
||||
session: SessionConfig = field(default_factory=SessionConfig)
|
||||
|
||||
# LLM 配置
|
||||
llm: LLMConfig = field(default_factory=LLMConfig)
|
||||
|
||||
# 调试模式
|
||||
debug: bool = False
|
||||
|
||||
|
||||
# 全局配置单例
|
||||
_config: Optional[KokoroFlowChatterV2Config] = None
|
||||
|
||||
|
||||
def get_config() -> KokoroFlowChatterV2Config:
|
||||
"""获取全局配置"""
|
||||
global _config
|
||||
if _config is None:
|
||||
_config = load_config()
|
||||
return _config
|
||||
|
||||
|
||||
def load_config() -> KokoroFlowChatterV2Config:
|
||||
"""从全局配置加载 KFC V2 配置"""
|
||||
from src.config.config import global_config
|
||||
|
||||
config = KokoroFlowChatterV2Config()
|
||||
|
||||
# 尝试从全局配置读取
|
||||
if not global_config:
|
||||
return config
|
||||
|
||||
try:
|
||||
if hasattr(global_config, 'kokoro_flow_chatter_v2'):
|
||||
kfc_cfg = getattr(global_config, 'kokoro_flow_chatter_v2')
|
||||
|
||||
# 基础配置
|
||||
if hasattr(kfc_cfg, 'enabled'):
|
||||
config.enabled = kfc_cfg.enabled
|
||||
if hasattr(kfc_cfg, 'enabled_stream_types'):
|
||||
config.enabled_stream_types = list(kfc_cfg.enabled_stream_types)
|
||||
if hasattr(kfc_cfg, 'debug'):
|
||||
config.debug = kfc_cfg.debug
|
||||
|
||||
# 等待配置
|
||||
if hasattr(kfc_cfg, 'waiting'):
|
||||
wait_cfg = kfc_cfg.waiting
|
||||
config.waiting = WaitingDefaults(
|
||||
default_max_wait_seconds=getattr(wait_cfg, 'default_max_wait_seconds', 300),
|
||||
min_wait_seconds=getattr(wait_cfg, 'min_wait_seconds', 30),
|
||||
max_wait_seconds=getattr(wait_cfg, 'max_wait_seconds', 1800),
|
||||
)
|
||||
|
||||
# 主动思考配置
|
||||
if hasattr(kfc_cfg, 'proactive'):
|
||||
pro_cfg = kfc_cfg.proactive
|
||||
config.proactive = ProactiveConfig(
|
||||
enabled=getattr(pro_cfg, 'enabled', True),
|
||||
silence_threshold_seconds=getattr(pro_cfg, 'silence_threshold_seconds', 7200),
|
||||
min_interval_between_proactive=getattr(pro_cfg, 'min_interval_between_proactive', 1800),
|
||||
quiet_hours_start=getattr(pro_cfg, 'quiet_hours_start', "23:00"),
|
||||
quiet_hours_end=getattr(pro_cfg, 'quiet_hours_end', "07:00"),
|
||||
trigger_probability=getattr(pro_cfg, 'trigger_probability', 0.3),
|
||||
)
|
||||
|
||||
# 提示词配置
|
||||
if hasattr(kfc_cfg, 'prompt'):
|
||||
pmt_cfg = kfc_cfg.prompt
|
||||
config.prompt = PromptConfig(
|
||||
max_activity_entries=getattr(pmt_cfg, 'max_activity_entries', 30),
|
||||
max_entry_length=getattr(pmt_cfg, 'max_entry_length', 500),
|
||||
include_relation=getattr(pmt_cfg, 'include_relation', True),
|
||||
include_memory=getattr(pmt_cfg, 'include_memory', True),
|
||||
)
|
||||
|
||||
# 会话配置
|
||||
if hasattr(kfc_cfg, 'session'):
|
||||
sess_cfg = kfc_cfg.session
|
||||
config.session = SessionConfig(
|
||||
session_dir=getattr(sess_cfg, 'session_dir', "kokoro_flow_chatter_v2/sessions"),
|
||||
session_expire_seconds=getattr(sess_cfg, 'session_expire_seconds', 86400 * 7),
|
||||
max_mental_log_entries=getattr(sess_cfg, 'max_mental_log_entries', 100),
|
||||
)
|
||||
|
||||
# LLM 配置
|
||||
if hasattr(kfc_cfg, 'llm'):
|
||||
llm_cfg = kfc_cfg.llm
|
||||
config.llm = LLMConfig(
|
||||
model_name=getattr(llm_cfg, 'model_name', ""),
|
||||
temperature=getattr(llm_cfg, 'temperature', 0.8),
|
||||
max_tokens=getattr(llm_cfg, 'max_tokens', 1024),
|
||||
timeout=getattr(llm_cfg, 'timeout', 60.0),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
from src.common.logger import get_logger
|
||||
logger = get_logger("kfc_v2_config")
|
||||
logger.warning(f"加载 KFC V2 配置失败,使用默认值: {e}")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def reload_config() -> KokoroFlowChatterV2Config:
|
||||
"""重新加载配置"""
|
||||
global _config
|
||||
_config = load_config()
|
||||
return _config
|
||||
338
src/plugins/built_in/kokoro_flow_chatter_v2/context_builder.py
Normal file
338
src/plugins/built_in/kokoro_flow_chatter_v2/context_builder.py
Normal file
@@ -0,0 +1,338 @@
|
||||
"""
|
||||
Kokoro Flow Chatter V2 上下文构建器
|
||||
|
||||
为 KFC V2 提供完整的情境感知能力。
|
||||
包含:
|
||||
- 关系信息 (relation_info)
|
||||
- 记忆块 (memory_block)
|
||||
- 表达习惯 (expression_habits)
|
||||
- 日程信息 (schedule)
|
||||
- 时间信息 (time)
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
logger = get_logger("kfc_v2_context_builder")
|
||||
|
||||
|
||||
def _get_config():
|
||||
"""获取全局配置(带类型断言)"""
|
||||
assert global_config is not None, "global_config 未初始化"
|
||||
return global_config
|
||||
|
||||
|
||||
class KFCContextBuilder:
|
||||
"""
|
||||
KFC V2 上下文构建器
|
||||
|
||||
为提示词提供完整的情境感知数据。
|
||||
"""
|
||||
|
||||
def __init__(self, chat_stream: "ChatStream"):
|
||||
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)
|
||||
|
||||
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:
|
||||
"""获取聊天历史文本"""
|
||||
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:
|
||||
"""构建关系信息块"""
|
||||
config = _get_config()
|
||||
|
||||
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 f"你与{sender_name}是普通朋友关系。"
|
||||
|
||||
async def _build_memory_block(self, chat_history: str, target_message: str) -> 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:
|
||||
"""构建表达习惯块"""
|
||||
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:
|
||||
"""构建日程信息块"""
|
||||
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_kfc_context(
|
||||
chat_stream: "ChatStream",
|
||||
sender_name: str,
|
||||
target_message: str,
|
||||
context: Optional["StreamContext"] = None,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
便捷函数:构建KFC所需的所有上下文
|
||||
"""
|
||||
builder = KFCContextBuilder(chat_stream)
|
||||
return await builder.build_all_context(sender_name, target_message, context)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"KFCContextBuilder",
|
||||
"build_kfc_context",
|
||||
]
|
||||
320
src/plugins/built_in/kokoro_flow_chatter_v2/models.py
Normal file
320
src/plugins/built_in/kokoro_flow_chatter_v2/models.py
Normal file
@@ -0,0 +1,320 @@
|
||||
"""
|
||||
Kokoro Flow Chatter V2 - 数据模型
|
||||
|
||||
定义核心数据结构:
|
||||
- EventType: 活动流事件类型
|
||||
- SessionStatus: 会话状态(仅 IDLE 和 WAITING)
|
||||
- MentalLogEntry: 心理活动日志条目
|
||||
- WaitingConfig: 等待配置
|
||||
- ActionModel: 动作模型
|
||||
- LLMResponse: LLM 响应结构
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
import time
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
"""
|
||||
活动流事件类型
|
||||
|
||||
用于标记 mental_log 中不同类型的事件,
|
||||
每种类型对应一个提示词小模板
|
||||
"""
|
||||
# 用户相关
|
||||
USER_MESSAGE = "user_message" # 用户发送消息
|
||||
|
||||
# Bot 行动相关
|
||||
BOT_PLANNING = "bot_planning" # Bot 规划(thought + actions)
|
||||
|
||||
# 等待相关
|
||||
WAITING_START = "waiting_start" # 开始等待
|
||||
WAITING_UPDATE = "waiting_update" # 等待期间心理变化
|
||||
REPLY_RECEIVED_IN_TIME = "reply_in_time" # 在预期内收到回复
|
||||
REPLY_RECEIVED_LATE = "reply_late" # 超出预期收到回复
|
||||
WAIT_TIMEOUT = "wait_timeout" # 等待超时
|
||||
|
||||
# 主动思考相关
|
||||
PROACTIVE_TRIGGER = "proactive_trigger" # 主动思考触发(长期沉默)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
class SessionStatus(Enum):
|
||||
"""
|
||||
会话状态
|
||||
|
||||
极简设计,只有两种稳定状态:
|
||||
- IDLE: 空闲,没有期待回复
|
||||
- WAITING: 等待对方回复中
|
||||
"""
|
||||
IDLE = "idle"
|
||||
WAITING = "waiting"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
@dataclass
|
||||
class WaitingConfig:
|
||||
"""
|
||||
等待配置
|
||||
|
||||
当 Bot 发送消息后设置的等待参数
|
||||
"""
|
||||
expected_reaction: str = "" # 期望对方如何回应
|
||||
max_wait_seconds: int = 0 # 最长等待时间(秒),0 表示不等待
|
||||
started_at: float = 0.0 # 开始等待的时间戳
|
||||
last_thinking_at: float = 0.0 # 上次连续思考的时间戳
|
||||
thinking_count: int = 0 # 连续思考次数
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""是否正在等待"""
|
||||
return self.max_wait_seconds > 0 and self.started_at > 0
|
||||
|
||||
def get_elapsed_seconds(self) -> float:
|
||||
"""获取已等待时间(秒)"""
|
||||
if not self.is_active():
|
||||
return 0.0
|
||||
return time.time() - self.started_at
|
||||
|
||||
def get_elapsed_minutes(self) -> float:
|
||||
"""获取已等待时间(分钟)"""
|
||||
return self.get_elapsed_seconds() / 60
|
||||
|
||||
def is_timeout(self) -> bool:
|
||||
"""是否已超时"""
|
||||
if not self.is_active():
|
||||
return False
|
||||
return self.get_elapsed_seconds() >= self.max_wait_seconds
|
||||
|
||||
def get_progress(self) -> float:
|
||||
"""获取等待进度 (0.0 - 1.0)"""
|
||||
if not self.is_active() or self.max_wait_seconds <= 0:
|
||||
return 0.0
|
||||
return min(self.get_elapsed_seconds() / self.max_wait_seconds, 1.0)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"expected_reaction": self.expected_reaction,
|
||||
"max_wait_seconds": self.max_wait_seconds,
|
||||
"started_at": self.started_at,
|
||||
"last_thinking_at": self.last_thinking_at,
|
||||
"thinking_count": self.thinking_count,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "WaitingConfig":
|
||||
return cls(
|
||||
expected_reaction=data.get("expected_reaction", ""),
|
||||
max_wait_seconds=data.get("max_wait_seconds", 0),
|
||||
started_at=data.get("started_at", 0.0),
|
||||
last_thinking_at=data.get("last_thinking_at", 0.0),
|
||||
thinking_count=data.get("thinking_count", 0),
|
||||
)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""重置等待配置"""
|
||||
self.expected_reaction = ""
|
||||
self.max_wait_seconds = 0
|
||||
self.started_at = 0.0
|
||||
self.last_thinking_at = 0.0
|
||||
self.thinking_count = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class MentalLogEntry:
|
||||
"""
|
||||
心理活动日志条目
|
||||
|
||||
记录活动流中的每一个事件节点,
|
||||
用于构建线性叙事风格的提示词
|
||||
"""
|
||||
event_type: EventType
|
||||
timestamp: float
|
||||
|
||||
# 通用字段
|
||||
content: str = "" # 事件内容(消息文本、动作描述等)
|
||||
|
||||
# 用户消息相关
|
||||
user_name: str = "" # 发送者名称
|
||||
user_id: str = "" # 发送者 ID
|
||||
|
||||
# Bot 规划相关
|
||||
thought: str = "" # 内心想法
|
||||
actions: list[dict] = field(default_factory=list) # 执行的动作列表
|
||||
expected_reaction: str = "" # 期望的回应
|
||||
max_wait_seconds: int = 0 # 设定的等待时间
|
||||
|
||||
# 等待相关
|
||||
elapsed_seconds: float = 0.0 # 已等待时间
|
||||
waiting_thought: str = "" # 等待期间的想法
|
||||
mood: str = "" # 当前心情
|
||||
|
||||
# 元数据
|
||||
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,
|
||||
"content": self.content,
|
||||
"user_name": self.user_name,
|
||||
"user_id": self.user_id,
|
||||
"thought": self.thought,
|
||||
"actions": self.actions,
|
||||
"expected_reaction": self.expected_reaction,
|
||||
"max_wait_seconds": self.max_wait_seconds,
|
||||
"elapsed_seconds": self.elapsed_seconds,
|
||||
"waiting_thought": self.waiting_thought,
|
||||
"mood": self.mood,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "MentalLogEntry":
|
||||
event_type_str = data.get("event_type", "user_message")
|
||||
try:
|
||||
event_type = EventType(event_type_str)
|
||||
except ValueError:
|
||||
event_type = EventType.USER_MESSAGE
|
||||
|
||||
return cls(
|
||||
event_type=event_type,
|
||||
timestamp=data.get("timestamp", time.time()),
|
||||
content=data.get("content", ""),
|
||||
user_name=data.get("user_name", ""),
|
||||
user_id=data.get("user_id", ""),
|
||||
thought=data.get("thought", ""),
|
||||
actions=data.get("actions", []),
|
||||
expected_reaction=data.get("expected_reaction", ""),
|
||||
max_wait_seconds=data.get("max_wait_seconds", 0),
|
||||
elapsed_seconds=data.get("elapsed_seconds", 0.0),
|
||||
waiting_thought=data.get("waiting_thought", ""),
|
||||
mood=data.get("mood", ""),
|
||||
metadata=data.get("metadata", {}),
|
||||
)
|
||||
|
||||
def get_time_str(self, format: str = "%H:%M") -> str:
|
||||
"""获取格式化的时间字符串"""
|
||||
return time.strftime(format, time.localtime(self.timestamp))
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionModel:
|
||||
"""
|
||||
动作模型
|
||||
|
||||
表示 LLM 决策的单个动作
|
||||
"""
|
||||
type: str # 动作类型
|
||||
params: dict[str, Any] = field(default_factory=dict) # 动作参数
|
||||
reason: str = "" # 选择该动作的理由
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = {"type": self.type}
|
||||
if self.reason:
|
||||
result["reason"] = self.reason
|
||||
result.update(self.params)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "ActionModel":
|
||||
action_type = data.get("type", "do_nothing")
|
||||
reason = data.get("reason", "")
|
||||
params = {k: v for k, v in data.items() if k not in ("type", "reason")}
|
||||
return cls(type=action_type, params=params, reason=reason)
|
||||
|
||||
def get_description(self) -> str:
|
||||
"""获取动作的文字描述"""
|
||||
if self.type == "reply":
|
||||
content = self.params.get("content", "")
|
||||
return f'发送消息:"{content[:50]}{"..." if len(content) > 50 else ""}"'
|
||||
elif self.type == "poke_user":
|
||||
return "戳了戳对方"
|
||||
elif self.type == "do_nothing":
|
||||
return "什么都没做"
|
||||
elif self.type == "send_emoji":
|
||||
emoji = self.params.get("emoji", "")
|
||||
return f"发送表情:{emoji}"
|
||||
else:
|
||||
return f"执行动作:{self.type}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMResponse:
|
||||
"""
|
||||
LLM 响应结构
|
||||
|
||||
定义 LLM 输出的 JSON 格式
|
||||
"""
|
||||
thought: str # 内心想法
|
||||
actions: list[ActionModel] # 动作列表
|
||||
expected_reaction: str = "" # 期望对方的回应
|
||||
max_wait_seconds: int = 0 # 最长等待时间(0 = 不等待)
|
||||
|
||||
# 可选字段
|
||||
mood: str = "" # 当前心情
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"thought": self.thought,
|
||||
"actions": [a.to_dict() for a in self.actions],
|
||||
"expected_reaction": self.expected_reaction,
|
||||
"max_wait_seconds": self.max_wait_seconds,
|
||||
"mood": self.mood,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "LLMResponse":
|
||||
actions_data = data.get("actions", [])
|
||||
actions = [ActionModel.from_dict(a) for a in actions_data] if actions_data else []
|
||||
|
||||
# 如果没有动作,添加默认的 do_nothing
|
||||
if not actions:
|
||||
actions = [ActionModel(type="do_nothing")]
|
||||
|
||||
# 处理 max_wait_seconds,确保在合理范围内
|
||||
max_wait = data.get("max_wait_seconds", 0)
|
||||
try:
|
||||
max_wait = int(max_wait)
|
||||
max_wait = max(0, min(max_wait, 1800)) # 0-30分钟
|
||||
except (ValueError, TypeError):
|
||||
max_wait = 0
|
||||
|
||||
return cls(
|
||||
thought=data.get("thought", ""),
|
||||
actions=actions,
|
||||
expected_reaction=data.get("expected_reaction", ""),
|
||||
max_wait_seconds=max_wait,
|
||||
mood=data.get("mood", ""),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_error_response(cls, error_message: str) -> "LLMResponse":
|
||||
"""创建错误响应"""
|
||||
return cls(
|
||||
thought=f"出现了问题:{error_message}",
|
||||
actions=[ActionModel(type="do_nothing")],
|
||||
expected_reaction="",
|
||||
max_wait_seconds=0,
|
||||
)
|
||||
|
||||
def has_reply(self) -> bool:
|
||||
"""是否包含回复动作"""
|
||||
return any(a.type in ("reply", "respond") for a in self.actions)
|
||||
|
||||
def get_reply_content(self) -> str:
|
||||
"""获取回复内容"""
|
||||
for action in self.actions:
|
||||
if action.type in ("reply", "respond"):
|
||||
return action.params.get("content", "")
|
||||
return ""
|
||||
|
||||
def get_actions_description(self) -> str:
|
||||
"""获取所有动作的文字描述"""
|
||||
descriptions = [a.get_description() for a in self.actions]
|
||||
return " + ".join(descriptions)
|
||||
105
src/plugins/built_in/kokoro_flow_chatter_v2/plugin.py
Normal file
105
src/plugins/built_in/kokoro_flow_chatter_v2/plugin.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Kokoro Flow Chatter V2 - 插件注册
|
||||
|
||||
注册 Chatter
|
||||
"""
|
||||
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.base.base_plugin import BasePlugin
|
||||
from src.plugin_system.base.component_types import ChatterInfo
|
||||
from src.plugin_system.decorators import register_plugin
|
||||
|
||||
from .chatter import KokoroFlowChatterV2
|
||||
from .config import get_config
|
||||
from .proactive_thinker import start_proactive_thinker, stop_proactive_thinker
|
||||
|
||||
logger = get_logger("kfc_v2_plugin")
|
||||
|
||||
|
||||
@register_plugin
|
||||
class KokoroFlowChatterV2Plugin(BasePlugin):
|
||||
"""
|
||||
Kokoro Flow Chatter V2 插件
|
||||
|
||||
专为私聊设计的增强 Chatter:
|
||||
- 线性叙事提示词架构
|
||||
- 等待机制与心理状态演变
|
||||
- 主动思考能力
|
||||
"""
|
||||
|
||||
plugin_name: str = "kokoro_flow_chatter_v2"
|
||||
enable_plugin: bool = True
|
||||
plugin_priority: int = 50 # 高于默认 Chatter
|
||||
dependencies: ClassVar[list[str]] = []
|
||||
python_dependencies: ClassVar[list[str]] = []
|
||||
config_file_name: str = "config.toml"
|
||||
|
||||
# 状态
|
||||
_is_started: bool = False
|
||||
|
||||
async def on_plugin_loaded(self):
|
||||
"""插件加载时"""
|
||||
config = get_config()
|
||||
|
||||
if not config.enabled:
|
||||
logger.info("[KFC V2] 插件已禁用")
|
||||
return
|
||||
|
||||
logger.info("[KFC V2] 插件已加载")
|
||||
|
||||
# 启动主动思考器
|
||||
if config.proactive.enabled:
|
||||
try:
|
||||
await start_proactive_thinker()
|
||||
logger.info("[KFC V2] 主动思考器已启动")
|
||||
self._is_started = True
|
||||
except Exception as e:
|
||||
logger.error(f"[KFC V2] 启动主动思考器失败: {e}")
|
||||
|
||||
async def on_plugin_unloaded(self):
|
||||
"""插件卸载时"""
|
||||
try:
|
||||
await stop_proactive_thinker()
|
||||
logger.info("[KFC V2] 主动思考器已停止")
|
||||
self._is_started = False
|
||||
except Exception as e:
|
||||
logger.warning(f"[KFC V2] 停止主动思考器失败: {e}")
|
||||
|
||||
def get_plugin_components(self):
|
||||
"""返回组件列表"""
|
||||
config = get_config()
|
||||
|
||||
if not config.enabled:
|
||||
return []
|
||||
|
||||
components = []
|
||||
|
||||
try:
|
||||
# 注册 Chatter
|
||||
components.append((
|
||||
KokoroFlowChatterV2.get_chatter_info(),
|
||||
KokoroFlowChatterV2,
|
||||
))
|
||||
logger.debug("[KFC V2] 成功加载 KokoroFlowChatterV2 组件")
|
||||
except Exception as e:
|
||||
logger.error(f"[KFC V2] 加载组件失败: {e}")
|
||||
|
||||
return components
|
||||
|
||||
def get_plugin_info(self) -> dict[str, Any]:
|
||||
"""获取插件信息"""
|
||||
return {
|
||||
"name": self.plugin_name,
|
||||
"display_name": "Kokoro Flow Chatter V2",
|
||||
"version": "2.0.0",
|
||||
"author": "MoFox",
|
||||
"description": "专为私聊设计的增强 Chatter",
|
||||
"features": [
|
||||
"线性叙事提示词架构",
|
||||
"心理活动流记录",
|
||||
"等待机制与超时处理",
|
||||
"主动思考能力",
|
||||
],
|
||||
}
|
||||
500
src/plugins/built_in/kokoro_flow_chatter_v2/proactive_thinker.py
Normal file
500
src/plugins/built_in/kokoro_flow_chatter_v2/proactive_thinker.py
Normal file
@@ -0,0 +1,500 @@
|
||||
"""
|
||||
Kokoro Flow Chatter V2 - 主动思考器
|
||||
|
||||
独立组件,负责:
|
||||
1. 等待期间的连续思考(更新心理状态)
|
||||
2. 等待超时决策(继续等 or 做点什么)
|
||||
3. 长期沉默后主动发起对话
|
||||
|
||||
通过 UnifiedScheduler 定期触发,与 Chatter 解耦
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.plugin_system.apis.unified_scheduler import TriggerType, unified_scheduler
|
||||
|
||||
from .action_executor import ActionExecutor
|
||||
from .models import EventType, SessionStatus
|
||||
from .replyer import generate_response
|
||||
from .session import KokoroSession, get_session_manager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
|
||||
logger = get_logger("kfc_v2_proactive_thinker")
|
||||
|
||||
|
||||
class ProactiveThinker:
|
||||
"""
|
||||
主动思考器
|
||||
|
||||
独立于 Chatter,负责处理:
|
||||
1. 等待期间的连续思考
|
||||
2. 等待超时
|
||||
3. 长期沉默后主动发起
|
||||
|
||||
核心逻辑:
|
||||
- 定期检查所有 WAITING 状态的 Session
|
||||
- 触发连续思考或超时决策
|
||||
- 定期检查长期沉默的 Session,考虑主动发起
|
||||
"""
|
||||
|
||||
# 连续思考触发点(等待进度百分比)
|
||||
THINKING_TRIGGERS = [0.3, 0.6, 0.85]
|
||||
|
||||
# 任务名称
|
||||
TASK_WAITING_CHECK = "kfc_v2_waiting_check"
|
||||
TASK_PROACTIVE_CHECK = "kfc_v2_proactive_check"
|
||||
|
||||
def __init__(self):
|
||||
self.session_manager = get_session_manager()
|
||||
|
||||
# 配置
|
||||
self._load_config()
|
||||
|
||||
# 调度任务 ID
|
||||
self._waiting_schedule_id: Optional[str] = None
|
||||
self._proactive_schedule_id: Optional[str] = None
|
||||
self._running = False
|
||||
|
||||
# 统计
|
||||
self._stats = {
|
||||
"waiting_checks": 0,
|
||||
"continuous_thinking_triggered": 0,
|
||||
"timeout_decisions": 0,
|
||||
"proactive_triggered": 0,
|
||||
}
|
||||
|
||||
def _load_config(self) -> None:
|
||||
"""加载配置"""
|
||||
# 默认配置
|
||||
self.waiting_check_interval = 15.0 # 等待检查间隔(秒)
|
||||
self.proactive_check_interval = 300.0 # 主动思考检查间隔(秒)
|
||||
self.silence_threshold = 7200 # 沉默阈值(秒)
|
||||
self.min_proactive_interval = 1800 # 两次主动思考最小间隔(秒)
|
||||
self.quiet_hours_start = "23:00"
|
||||
self.quiet_hours_end = "07:00"
|
||||
|
||||
# 从全局配置读取
|
||||
if global_config and hasattr(global_config, 'kokoro_flow_chatter'):
|
||||
kfc_config = global_config.kokoro_flow_chatter
|
||||
if hasattr(kfc_config, 'proactive_thinking'):
|
||||
proactive_cfg = kfc_config.proactive_thinking
|
||||
self.silence_threshold = getattr(proactive_cfg, 'silence_threshold_seconds', 7200)
|
||||
self.min_proactive_interval = getattr(proactive_cfg, 'min_interval_between_proactive', 1800)
|
||||
|
||||
async def start(self) -> None:
|
||||
"""启动主动思考器"""
|
||||
if self._running:
|
||||
logger.warning("[ProactiveThinker] 已在运行中")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
|
||||
# 注册等待检查任务
|
||||
self._waiting_schedule_id = await unified_scheduler.create_schedule(
|
||||
callback=self._check_waiting_sessions,
|
||||
trigger_type=TriggerType.TIME,
|
||||
trigger_config={"delay_seconds": self.waiting_check_interval},
|
||||
is_recurring=True,
|
||||
task_name=self.TASK_WAITING_CHECK,
|
||||
force_overwrite=True,
|
||||
timeout=60.0,
|
||||
)
|
||||
|
||||
# 注册主动思考检查任务
|
||||
self._proactive_schedule_id = await unified_scheduler.create_schedule(
|
||||
callback=self._check_proactive_sessions,
|
||||
trigger_type=TriggerType.TIME,
|
||||
trigger_config={"delay_seconds": self.proactive_check_interval},
|
||||
is_recurring=True,
|
||||
task_name=self.TASK_PROACTIVE_CHECK,
|
||||
force_overwrite=True,
|
||||
timeout=120.0,
|
||||
)
|
||||
|
||||
logger.info("[ProactiveThinker] 已启动")
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""停止主动思考器"""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
self._running = False
|
||||
|
||||
if self._waiting_schedule_id:
|
||||
await unified_scheduler.remove_schedule(self._waiting_schedule_id)
|
||||
if self._proactive_schedule_id:
|
||||
await unified_scheduler.remove_schedule(self._proactive_schedule_id)
|
||||
|
||||
logger.info("[ProactiveThinker] 已停止")
|
||||
|
||||
# ========================
|
||||
# 等待检查
|
||||
# ========================
|
||||
|
||||
async def _check_waiting_sessions(self) -> None:
|
||||
"""检查所有等待中的 Session"""
|
||||
self._stats["waiting_checks"] += 1
|
||||
|
||||
sessions = await self.session_manager.get_waiting_sessions()
|
||||
if not sessions:
|
||||
return
|
||||
|
||||
# 并行处理
|
||||
tasks = [
|
||||
asyncio.create_task(self._process_waiting_session(s))
|
||||
for s in sessions
|
||||
]
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
async def _process_waiting_session(self, session: KokoroSession) -> None:
|
||||
"""处理单个等待中的 Session"""
|
||||
try:
|
||||
if session.status != SessionStatus.WAITING:
|
||||
return
|
||||
|
||||
if not session.waiting_config.is_active():
|
||||
return
|
||||
|
||||
# 检查是否超时
|
||||
if session.waiting_config.is_timeout():
|
||||
await self._handle_timeout(session)
|
||||
return
|
||||
|
||||
# 检查是否需要触发连续思考
|
||||
progress = session.waiting_config.get_progress()
|
||||
if self._should_trigger_thinking(session, progress):
|
||||
await self._handle_continuous_thinking(session, progress)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ProactiveThinker] 处理等待 Session 失败 {session.user_id}: {e}")
|
||||
|
||||
def _should_trigger_thinking(self, session: KokoroSession, progress: float) -> bool:
|
||||
"""判断是否应触发连续思考"""
|
||||
# 计算应该触发的次数
|
||||
expected_count = sum(1 for t in self.THINKING_TRIGGERS if progress >= t)
|
||||
|
||||
if session.waiting_config.thinking_count >= expected_count:
|
||||
return False
|
||||
|
||||
# 确保两次思考之间有间隔
|
||||
if session.waiting_config.last_thinking_at > 0:
|
||||
elapsed = time.time() - session.waiting_config.last_thinking_at
|
||||
if elapsed < 30: # 至少 30 秒间隔
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def _handle_continuous_thinking(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
progress: float,
|
||||
) -> None:
|
||||
"""处理连续思考"""
|
||||
self._stats["continuous_thinking_triggered"] += 1
|
||||
|
||||
# 生成等待中的想法
|
||||
thought = self._generate_waiting_thought(session, progress)
|
||||
|
||||
# 记录到 mental_log
|
||||
session.add_waiting_update(
|
||||
waiting_thought=thought,
|
||||
mood="", # 可以根据进度设置心情
|
||||
)
|
||||
|
||||
# 更新思考计数
|
||||
session.waiting_config.thinking_count += 1
|
||||
session.waiting_config.last_thinking_at = time.time()
|
||||
|
||||
# 保存
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
|
||||
logger.debug(
|
||||
f"[ProactiveThinker] 连续思考: user={session.user_id}, "
|
||||
f"progress={progress:.1%}, thought={thought[:30]}..."
|
||||
)
|
||||
|
||||
def _generate_waiting_thought(self, session: KokoroSession, progress: float) -> str:
|
||||
"""生成等待中的想法"""
|
||||
elapsed_minutes = session.waiting_config.get_elapsed_minutes()
|
||||
|
||||
if progress < 0.4:
|
||||
thoughts = [
|
||||
f"已经等了 {elapsed_minutes:.0f} 分钟了,对方可能在忙吧...",
|
||||
"不知道对方在做什么呢",
|
||||
"再等等看吧",
|
||||
]
|
||||
elif progress < 0.7:
|
||||
thoughts = [
|
||||
f"等了 {elapsed_minutes:.0f} 分钟了,有点担心...",
|
||||
"对方是不是忘记回复了?",
|
||||
"嗯...还是没有消息",
|
||||
]
|
||||
else:
|
||||
thoughts = [
|
||||
f"已经等了 {elapsed_minutes:.0f} 分钟了,感觉有点焦虑",
|
||||
"要不要主动说点什么呢...",
|
||||
"快到时间了,对方还是没回",
|
||||
]
|
||||
|
||||
return random.choice(thoughts)
|
||||
|
||||
async def _handle_timeout(self, session: KokoroSession) -> None:
|
||||
"""处理等待超时"""
|
||||
self._stats["timeout_decisions"] += 1
|
||||
|
||||
logger.info(f"[ProactiveThinker] 等待超时: user={session.user_id}")
|
||||
|
||||
try:
|
||||
# 获取聊天流
|
||||
chat_stream = await self._get_chat_stream(session.stream_id)
|
||||
|
||||
# 加载动作
|
||||
action_executor = ActionExecutor(session.stream_id)
|
||||
await action_executor.load_actions()
|
||||
|
||||
# 调用 Replyer 生成超时决策
|
||||
response = await generate_response(
|
||||
session=session,
|
||||
user_name=session.user_id, # 这里可以改进,获取真实用户名
|
||||
situation_type="timeout",
|
||||
chat_stream=chat_stream,
|
||||
available_actions=action_executor.get_available_actions(),
|
||||
)
|
||||
|
||||
# 执行动作
|
||||
exec_result = await action_executor.execute(response, chat_stream)
|
||||
|
||||
# 记录到 mental_log
|
||||
session.add_bot_planning(
|
||||
thought=response.thought,
|
||||
actions=[a.to_dict() for a in response.actions],
|
||||
expected_reaction=response.expected_reaction,
|
||||
max_wait_seconds=response.max_wait_seconds,
|
||||
)
|
||||
|
||||
# 更新状态
|
||||
if response.max_wait_seconds > 0:
|
||||
# 继续等待
|
||||
session.start_waiting(
|
||||
expected_reaction=response.expected_reaction,
|
||||
max_wait_seconds=response.max_wait_seconds,
|
||||
)
|
||||
else:
|
||||
# 不再等待
|
||||
session.end_waiting()
|
||||
|
||||
# 保存
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
|
||||
logger.info(
|
||||
f"[ProactiveThinker] 超时决策完成: user={session.user_id}, "
|
||||
f"actions={[a.type for a in response.actions]}, "
|
||||
f"continue_wait={response.max_wait_seconds > 0}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ProactiveThinker] 处理超时失败: {e}")
|
||||
# 出错时结束等待
|
||||
session.end_waiting()
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
|
||||
# ========================
|
||||
# 主动思考(长期沉默)
|
||||
# ========================
|
||||
|
||||
async def _check_proactive_sessions(self) -> None:
|
||||
"""检查是否有需要主动发起对话的 Session"""
|
||||
# 检查是否在勿扰时段
|
||||
if self._is_quiet_hours():
|
||||
return
|
||||
|
||||
sessions = await self.session_manager.get_all_sessions()
|
||||
current_time = time.time()
|
||||
|
||||
for session in sessions:
|
||||
try:
|
||||
trigger_reason = self._should_trigger_proactive(session, current_time)
|
||||
if trigger_reason:
|
||||
await self._handle_proactive(session, trigger_reason)
|
||||
except Exception as e:
|
||||
logger.error(f"[ProactiveThinker] 检查主动思考失败 {session.user_id}: {e}")
|
||||
|
||||
def _is_quiet_hours(self) -> bool:
|
||||
"""检查是否在勿扰时段"""
|
||||
try:
|
||||
now = datetime.now()
|
||||
current_minutes = now.hour * 60 + now.minute
|
||||
|
||||
start_parts = self.quiet_hours_start.split(":")
|
||||
start_minutes = int(start_parts[0]) * 60 + int(start_parts[1])
|
||||
|
||||
end_parts = self.quiet_hours_end.split(":")
|
||||
end_minutes = int(end_parts[0]) * 60 + int(end_parts[1])
|
||||
|
||||
if start_minutes <= end_minutes:
|
||||
return start_minutes <= current_minutes < end_minutes
|
||||
else:
|
||||
return current_minutes >= start_minutes or current_minutes < end_minutes
|
||||
except:
|
||||
return False
|
||||
|
||||
def _should_trigger_proactive(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
current_time: float,
|
||||
) -> Optional[str]:
|
||||
"""判断是否应触发主动思考"""
|
||||
# 只检查 IDLE 状态的 Session
|
||||
if session.status != SessionStatus.IDLE:
|
||||
return None
|
||||
|
||||
# 检查沉默时长
|
||||
silence_duration = current_time - session.last_activity_at
|
||||
if silence_duration < self.silence_threshold:
|
||||
return None
|
||||
|
||||
# 检查距离上次主动思考的间隔
|
||||
if session.last_proactive_at:
|
||||
time_since_last = current_time - session.last_proactive_at
|
||||
if time_since_last < self.min_proactive_interval:
|
||||
return None
|
||||
|
||||
# 概率触发(避免每次检查都触发)
|
||||
if random.random() > 0.3: # 30% 概率
|
||||
return None
|
||||
|
||||
silence_hours = silence_duration / 3600
|
||||
return f"沉默了 {silence_hours:.1f} 小时"
|
||||
|
||||
async def _handle_proactive(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
trigger_reason: str,
|
||||
) -> None:
|
||||
"""处理主动思考"""
|
||||
self._stats["proactive_triggered"] += 1
|
||||
|
||||
logger.info(f"[ProactiveThinker] 主动思考触发: user={session.user_id}, reason={trigger_reason}")
|
||||
|
||||
try:
|
||||
# 获取聊天流
|
||||
chat_stream = await self._get_chat_stream(session.stream_id)
|
||||
|
||||
# 加载动作
|
||||
action_executor = ActionExecutor(session.stream_id)
|
||||
await action_executor.load_actions()
|
||||
|
||||
# 计算沉默时长
|
||||
silence_seconds = time.time() - session.last_activity_at
|
||||
if silence_seconds < 3600:
|
||||
silence_duration = f"{silence_seconds / 60:.0f} 分钟"
|
||||
else:
|
||||
silence_duration = f"{silence_seconds / 3600:.1f} 小时"
|
||||
|
||||
# 调用 Replyer
|
||||
response = await generate_response(
|
||||
session=session,
|
||||
user_name=session.user_id,
|
||||
situation_type="proactive",
|
||||
chat_stream=chat_stream,
|
||||
available_actions=action_executor.get_available_actions(),
|
||||
extra_context={
|
||||
"trigger_reason": trigger_reason,
|
||||
"silence_duration": silence_duration,
|
||||
},
|
||||
)
|
||||
|
||||
# 检查是否决定不打扰
|
||||
is_do_nothing = (
|
||||
len(response.actions) == 0 or
|
||||
(len(response.actions) == 1 and response.actions[0].type == "do_nothing")
|
||||
)
|
||||
|
||||
if is_do_nothing:
|
||||
logger.info(f"[ProactiveThinker] 决定不打扰: user={session.user_id}")
|
||||
session.last_proactive_at = time.time()
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
return
|
||||
|
||||
# 执行动作
|
||||
exec_result = await action_executor.execute(response, chat_stream)
|
||||
|
||||
# 记录到 mental_log
|
||||
session.add_bot_planning(
|
||||
thought=response.thought,
|
||||
actions=[a.to_dict() for a in response.actions],
|
||||
expected_reaction=response.expected_reaction,
|
||||
max_wait_seconds=response.max_wait_seconds,
|
||||
)
|
||||
|
||||
# 更新状态
|
||||
session.last_proactive_at = time.time()
|
||||
if response.max_wait_seconds > 0:
|
||||
session.start_waiting(
|
||||
expected_reaction=response.expected_reaction,
|
||||
max_wait_seconds=response.max_wait_seconds,
|
||||
)
|
||||
|
||||
# 保存
|
||||
await self.session_manager.save_session(session.user_id)
|
||||
|
||||
logger.info(
|
||||
f"[ProactiveThinker] 主动发起完成: user={session.user_id}, "
|
||||
f"actions={[a.type for a in response.actions]}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ProactiveThinker] 主动思考失败: {e}")
|
||||
|
||||
async def _get_chat_stream(self, stream_id: str):
|
||||
"""获取聊天流"""
|
||||
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(stream_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"[ProactiveThinker] 获取 chat_stream 失败: {e}")
|
||||
return None
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""获取统计信息"""
|
||||
return {
|
||||
**self._stats,
|
||||
"is_running": self._running,
|
||||
}
|
||||
|
||||
|
||||
# 全局单例
|
||||
_proactive_thinker: Optional[ProactiveThinker] = None
|
||||
|
||||
|
||||
def get_proactive_thinker() -> ProactiveThinker:
|
||||
"""获取全局主动思考器"""
|
||||
global _proactive_thinker
|
||||
if _proactive_thinker is None:
|
||||
_proactive_thinker = ProactiveThinker()
|
||||
return _proactive_thinker
|
||||
|
||||
|
||||
async def start_proactive_thinker() -> ProactiveThinker:
|
||||
"""启动主动思考器"""
|
||||
thinker = get_proactive_thinker()
|
||||
await thinker.start()
|
||||
return thinker
|
||||
|
||||
|
||||
async def stop_proactive_thinker() -> None:
|
||||
"""停止主动思考器"""
|
||||
global _proactive_thinker
|
||||
if _proactive_thinker:
|
||||
await _proactive_thinker.stop()
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Kokoro Flow Chatter V2 - 提示词模块
|
||||
|
||||
使用项目统一的 Prompt 管理系统管理所有提示词模板
|
||||
"""
|
||||
|
||||
# 导入 prompts 模块以注册提示词
|
||||
from . import prompts # noqa: F401
|
||||
from .builder import PromptBuilder, get_prompt_builder
|
||||
from .prompts import PROMPT_NAMES
|
||||
|
||||
__all__ = [
|
||||
"PromptBuilder",
|
||||
"get_prompt_builder",
|
||||
"PROMPT_NAMES",
|
||||
]
|
||||
388
src/plugins/built_in/kokoro_flow_chatter_v2/prompt/builder.py
Normal file
388
src/plugins/built_in/kokoro_flow_chatter_v2/prompt/builder.py
Normal file
@@ -0,0 +1,388 @@
|
||||
"""
|
||||
Kokoro Flow Chatter V2 - 提示词构建器
|
||||
|
||||
使用项目统一的 Prompt 管理系统构建提示词
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from src.chat.utils.prompt import global_prompt_manager
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
|
||||
from ..models import EventType, MentalLogEntry, SessionStatus
|
||||
from ..session import KokoroSession
|
||||
|
||||
# 导入模板注册(确保模板被注册到 global_prompt_manager)
|
||||
from . import prompts as _ # noqa: F401
|
||||
from .prompts import PROMPT_NAMES
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
|
||||
logger = get_logger("kfc_v2_prompt_builder")
|
||||
|
||||
|
||||
class PromptBuilder:
|
||||
"""
|
||||
提示词构建器
|
||||
|
||||
使用统一的 Prompt 管理系统构建提示词:
|
||||
1. 构建活动流(从 mental_log 生成线性叙事)
|
||||
2. 构建当前情况描述
|
||||
3. 使用 global_prompt_manager 格式化最终提示词
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._context_builder = None
|
||||
|
||||
async def build_prompt(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
user_name: str,
|
||||
situation_type: str = "new_message",
|
||||
chat_stream: Optional["ChatStream"] = None,
|
||||
available_actions: Optional[dict] = None,
|
||||
extra_context: Optional[dict] = None,
|
||||
) -> str:
|
||||
"""
|
||||
构建完整的提示词
|
||||
|
||||
Args:
|
||||
session: 会话对象
|
||||
user_name: 用户名称
|
||||
situation_type: 情况类型 (new_message/reply_in_time/reply_late/timeout/proactive)
|
||||
chat_stream: 聊天流对象
|
||||
available_actions: 可用动作字典
|
||||
extra_context: 额外上下文(如 trigger_reason)
|
||||
|
||||
Returns:
|
||||
完整的提示词
|
||||
"""
|
||||
extra_context = extra_context or {}
|
||||
|
||||
# 1. 构建人设块
|
||||
persona_block = self._build_persona_block()
|
||||
|
||||
# 2. 构建关系块
|
||||
relation_block = await self._build_relation_block(user_name, chat_stream)
|
||||
|
||||
# 3. 构建活动流
|
||||
activity_stream = await self._build_activity_stream(session, user_name)
|
||||
|
||||
# 4. 构建当前情况
|
||||
current_situation = await self._build_current_situation(
|
||||
session, user_name, situation_type, extra_context
|
||||
)
|
||||
|
||||
# 5. 构建可用动作
|
||||
actions_block = self._build_actions_block(available_actions)
|
||||
|
||||
# 6. 获取输出格式
|
||||
output_format = await self._get_output_format()
|
||||
|
||||
# 7. 使用统一的 prompt 管理系统格式化
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["main"],
|
||||
user_name=user_name,
|
||||
persona_block=persona_block,
|
||||
relation_block=relation_block,
|
||||
activity_stream=activity_stream or "(这是你们第一次聊天)",
|
||||
current_situation=current_situation,
|
||||
available_actions=actions_block,
|
||||
output_format=output_format,
|
||||
)
|
||||
|
||||
return prompt
|
||||
|
||||
def _build_persona_block(self) -> str:
|
||||
"""构建人设块"""
|
||||
if global_config is None:
|
||||
return "你是一个温暖、真诚的人。"
|
||||
|
||||
personality = global_config.personality
|
||||
parts = []
|
||||
|
||||
if personality.personality_core:
|
||||
parts.append(personality.personality_core)
|
||||
|
||||
if personality.personality_side:
|
||||
parts.append(personality.personality_side)
|
||||
|
||||
if personality.identity:
|
||||
parts.append(personality.identity)
|
||||
|
||||
if personality.reply_style:
|
||||
parts.append(f"\n### 说话风格\n{personality.reply_style}")
|
||||
|
||||
return "\n\n".join(parts) if parts else "你是一个温暖、真诚的人。"
|
||||
|
||||
async def _build_relation_block(
|
||||
self,
|
||||
user_name: str,
|
||||
chat_stream: Optional["ChatStream"],
|
||||
) -> str:
|
||||
"""构建关系块"""
|
||||
if not chat_stream:
|
||||
return f"你与 {user_name} 还不太熟悉,这是早期的交流阶段。"
|
||||
|
||||
try:
|
||||
# 延迟导入上下文构建器
|
||||
if self._context_builder is None:
|
||||
from ..context_builder import KFCContextBuilder
|
||||
self._context_builder = KFCContextBuilder
|
||||
|
||||
builder = self._context_builder(chat_stream)
|
||||
context_data = await builder.build_all_context(
|
||||
sender_name=user_name,
|
||||
target_message="",
|
||||
context=None,
|
||||
)
|
||||
|
||||
relation_info = context_data.get("relation_info", "")
|
||||
if relation_info:
|
||||
return relation_info
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"构建关系块失败: {e}")
|
||||
|
||||
return f"你与 {user_name} 还不太熟悉,这是早期的交流阶段。"
|
||||
|
||||
async def _build_activity_stream(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
user_name: str,
|
||||
) -> str:
|
||||
"""
|
||||
构建活动流
|
||||
|
||||
将 mental_log 中的事件按时间顺序转换为线性叙事
|
||||
使用统一的 prompt 模板
|
||||
"""
|
||||
entries = session.get_recent_entries(limit=30)
|
||||
if not entries:
|
||||
return ""
|
||||
|
||||
parts = []
|
||||
|
||||
for entry in entries:
|
||||
part = await self._format_entry(entry, user_name)
|
||||
if part:
|
||||
parts.append(part)
|
||||
|
||||
return "\n\n".join(parts)
|
||||
|
||||
async def _format_entry(self, entry: MentalLogEntry, user_name: str) -> str:
|
||||
"""格式化单个活动日志条目"""
|
||||
|
||||
if entry.event_type == EventType.USER_MESSAGE:
|
||||
# 用户消息
|
||||
result = await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["entry_user_message"],
|
||||
time=entry.get_time_str(),
|
||||
user_name=entry.user_name or user_name,
|
||||
content=entry.content,
|
||||
)
|
||||
|
||||
# 如果有回复状态元数据,添加说明
|
||||
reply_status = entry.metadata.get("reply_status")
|
||||
if reply_status == "in_time":
|
||||
elapsed = entry.metadata.get("elapsed_seconds", 0) / 60
|
||||
max_wait = entry.metadata.get("max_wait_seconds", 0) / 60
|
||||
result += await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["entry_reply_in_time"],
|
||||
elapsed_minutes=elapsed,
|
||||
max_wait_minutes=max_wait,
|
||||
)
|
||||
elif reply_status == "late":
|
||||
elapsed = entry.metadata.get("elapsed_seconds", 0) / 60
|
||||
max_wait = entry.metadata.get("max_wait_seconds", 0) / 60
|
||||
result += await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["entry_reply_late"],
|
||||
elapsed_minutes=elapsed,
|
||||
max_wait_minutes=max_wait,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
elif entry.event_type == EventType.BOT_PLANNING:
|
||||
# Bot 规划
|
||||
actions_desc = self._format_actions(entry.actions)
|
||||
|
||||
if entry.max_wait_seconds > 0:
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["entry_bot_planning"],
|
||||
thought=entry.thought or "(没有特别的想法)",
|
||||
actions_description=actions_desc,
|
||||
expected_reaction=entry.expected_reaction or "随便怎么回应都行",
|
||||
max_wait_minutes=entry.max_wait_seconds / 60,
|
||||
)
|
||||
else:
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["entry_bot_planning_no_wait"],
|
||||
thought=entry.thought or "(没有特别的想法)",
|
||||
actions_description=actions_desc,
|
||||
)
|
||||
|
||||
elif entry.event_type == EventType.WAITING_UPDATE:
|
||||
# 等待中心理变化
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["entry_waiting_update"],
|
||||
elapsed_minutes=entry.elapsed_seconds / 60,
|
||||
waiting_thought=entry.waiting_thought or "还在等...",
|
||||
)
|
||||
|
||||
elif entry.event_type == EventType.PROACTIVE_TRIGGER:
|
||||
# 主动思考触发
|
||||
silence = entry.metadata.get("silence_duration", "一段时间")
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["entry_proactive_trigger"],
|
||||
silence_duration=silence,
|
||||
)
|
||||
|
||||
return ""
|
||||
|
||||
def _format_actions(self, actions: list[dict]) -> str:
|
||||
"""格式化动作列表为可读描述"""
|
||||
if not actions:
|
||||
return "(无动作)"
|
||||
|
||||
descriptions = []
|
||||
for action in actions:
|
||||
action_type = action.get("type", "unknown")
|
||||
|
||||
if action_type == "reply":
|
||||
content = action.get("content", "")
|
||||
if len(content) > 50:
|
||||
content = content[:50] + "..."
|
||||
descriptions.append(f"发送消息:「{content}」")
|
||||
elif action_type == "poke_user":
|
||||
descriptions.append("戳了戳对方")
|
||||
elif action_type == "do_nothing":
|
||||
descriptions.append("什么都不做")
|
||||
elif action_type == "send_emoji":
|
||||
emoji = action.get("emoji", "")
|
||||
descriptions.append(f"发送表情:{emoji}")
|
||||
else:
|
||||
descriptions.append(f"执行动作:{action_type}")
|
||||
|
||||
return "、".join(descriptions)
|
||||
|
||||
async def _build_current_situation(
|
||||
self,
|
||||
session: KokoroSession,
|
||||
user_name: str,
|
||||
situation_type: str,
|
||||
extra_context: dict,
|
||||
) -> str:
|
||||
"""构建当前情况描述"""
|
||||
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M")
|
||||
|
||||
if situation_type == "new_message":
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["situation_new_message"],
|
||||
current_time=current_time,
|
||||
user_name=user_name,
|
||||
)
|
||||
|
||||
elif situation_type == "reply_in_time":
|
||||
elapsed = session.waiting_config.get_elapsed_seconds()
|
||||
max_wait = session.waiting_config.max_wait_seconds
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["situation_reply_in_time"],
|
||||
current_time=current_time,
|
||||
user_name=user_name,
|
||||
elapsed_minutes=elapsed / 60,
|
||||
max_wait_minutes=max_wait / 60,
|
||||
)
|
||||
|
||||
elif situation_type == "reply_late":
|
||||
elapsed = session.waiting_config.get_elapsed_seconds()
|
||||
max_wait = session.waiting_config.max_wait_seconds
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["situation_reply_late"],
|
||||
current_time=current_time,
|
||||
user_name=user_name,
|
||||
elapsed_minutes=elapsed / 60,
|
||||
max_wait_minutes=max_wait / 60,
|
||||
)
|
||||
|
||||
elif situation_type == "timeout":
|
||||
elapsed = session.waiting_config.get_elapsed_seconds()
|
||||
max_wait = session.waiting_config.max_wait_seconds
|
||||
expected = session.waiting_config.expected_reaction
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["situation_timeout"],
|
||||
current_time=current_time,
|
||||
user_name=user_name,
|
||||
elapsed_minutes=elapsed / 60,
|
||||
max_wait_minutes=max_wait / 60,
|
||||
expected_reaction=expected or "对方能回复点什么",
|
||||
)
|
||||
|
||||
elif situation_type == "proactive":
|
||||
silence = extra_context.get("silence_duration", "一段时间")
|
||||
trigger_reason = extra_context.get("trigger_reason", "")
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["situation_proactive"],
|
||||
current_time=current_time,
|
||||
user_name=user_name,
|
||||
silence_duration=silence,
|
||||
trigger_reason=trigger_reason,
|
||||
)
|
||||
|
||||
# 默认使用 new_message
|
||||
return await global_prompt_manager.format_prompt(
|
||||
PROMPT_NAMES["situation_new_message"],
|
||||
current_time=current_time,
|
||||
user_name=user_name,
|
||||
)
|
||||
|
||||
def _build_actions_block(self, available_actions: Optional[dict]) -> str:
|
||||
"""构建可用动作块"""
|
||||
if not available_actions:
|
||||
return self._get_default_actions_block()
|
||||
|
||||
lines = []
|
||||
for name, info in available_actions.items():
|
||||
desc = getattr(info, "description", "") or f"执行 {name}"
|
||||
lines.append(f"- `{name}`: {desc}")
|
||||
|
||||
return "\n".join(lines) if lines else self._get_default_actions_block()
|
||||
|
||||
def _get_default_actions_block(self) -> str:
|
||||
"""获取默认的动作列表"""
|
||||
return """- `reply`: 发送文字消息(参数:content)
|
||||
- `poke_user`: 戳一戳对方
|
||||
- `do_nothing`: 什么都不做"""
|
||||
|
||||
async def _get_output_format(self) -> str:
|
||||
"""获取输出格式模板"""
|
||||
try:
|
||||
prompt = await global_prompt_manager.get_prompt_async(
|
||||
PROMPT_NAMES["output_format"]
|
||||
)
|
||||
return prompt.template
|
||||
except KeyError:
|
||||
# 如果模板未注册,返回默认格式
|
||||
return """请用 JSON 格式回复:
|
||||
{
|
||||
"thought": "你的想法",
|
||||
"actions": [{"type": "reply", "content": "你的回复"}],
|
||||
"expected_reaction": "期待的反应",
|
||||
"max_wait_seconds": 300
|
||||
}"""
|
||||
|
||||
|
||||
# 全局单例
|
||||
_prompt_builder: Optional[PromptBuilder] = None
|
||||
|
||||
|
||||
def get_prompt_builder() -> PromptBuilder:
|
||||
"""获取全局提示词构建器"""
|
||||
global _prompt_builder
|
||||
if _prompt_builder is None:
|
||||
_prompt_builder = PromptBuilder()
|
||||
return _prompt_builder
|
||||
217
src/plugins/built_in/kokoro_flow_chatter_v2/prompt/prompts.py
Normal file
217
src/plugins/built_in/kokoro_flow_chatter_v2/prompt/prompts.py
Normal file
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
Kokoro Flow Chatter V2 - 提示词模板注册
|
||||
|
||||
使用项目统一的 Prompt 管理系统注册所有 KFC V2 使用的提示词模板
|
||||
"""
|
||||
|
||||
from src.chat.utils.prompt import Prompt
|
||||
|
||||
# =================================================================================================
|
||||
# KFC V2 主提示词模板
|
||||
# =================================================================================================
|
||||
|
||||
KFC_V2_MAIN_PROMPT = Prompt(
|
||||
name="kfc_v2_main",
|
||||
template="""# 你与 {user_name} 的私聊
|
||||
|
||||
## 1. 你是谁
|
||||
{persona_block}
|
||||
|
||||
## 2. 你与 {user_name} 的关系
|
||||
{relation_block}
|
||||
|
||||
## 3. 你们之间发生的事(活动流)
|
||||
以下是你和 {user_name} 最近的互动历史,按时间顺序记录了你们的对话和你的心理活动:
|
||||
|
||||
{activity_stream}
|
||||
|
||||
## 4. 当前情况
|
||||
{current_situation}
|
||||
|
||||
## 5. 你可以做的事情
|
||||
{available_actions}
|
||||
|
||||
## 6. 你的回复格式
|
||||
{output_format}
|
||||
""",
|
||||
)
|
||||
|
||||
# =================================================================================================
|
||||
# 输出格式模板
|
||||
# =================================================================================================
|
||||
|
||||
KFC_V2_OUTPUT_FORMAT = Prompt(
|
||||
name="kfc_v2_output_format",
|
||||
template="""请用以下 JSON 格式回复:
|
||||
```json
|
||||
{{
|
||||
"thought": "你脑子里在想什么,越自然越好",
|
||||
"actions": [
|
||||
{{"type": "reply", "content": "你要说的话"}},
|
||||
{{"type": "其他动作", "参数": "值"}}
|
||||
],
|
||||
"expected_reaction": "你期待对方的反应是什么",
|
||||
"max_wait_seconds": 300
|
||||
}}
|
||||
```
|
||||
|
||||
说明:
|
||||
- `thought`:你的内心独白,记录你此刻的想法和感受
|
||||
- `actions`:你要执行的动作列表,可以组合多个
|
||||
- `expected_reaction`:你期待对方如何回应(用于判断是否需要等待)
|
||||
- `max_wait_seconds`:设定等待时间(秒),0 表示不等待,超时后你会考虑是否要主动说点什么
|
||||
- 即使什么都不想做,也放一个 `{{"type": "do_nothing"}}`""",
|
||||
)
|
||||
|
||||
# =================================================================================================
|
||||
# 情景模板 - 根据不同情境使用不同的当前情况描述
|
||||
# =================================================================================================
|
||||
|
||||
KFC_V2_SITUATION_NEW_MESSAGE = Prompt(
|
||||
name="kfc_v2_situation_new_message",
|
||||
template="""现在是 {current_time}。
|
||||
|
||||
{user_name} 刚刚给你发了消息。这是一次新的对话发起(不是对你之前消息的回复)。
|
||||
|
||||
请决定你要怎么回应。你可以:
|
||||
- 发送文字消息回复
|
||||
- 发表情包
|
||||
- 戳一戳对方
|
||||
- 什么都不做(如果觉得没必要回复)
|
||||
- 或者组合多个动作""",
|
||||
)
|
||||
|
||||
KFC_V2_SITUATION_REPLY_IN_TIME = Prompt(
|
||||
name="kfc_v2_situation_reply_in_time",
|
||||
template="""现在是 {current_time}。
|
||||
|
||||
你之前发了消息后一直在等 {user_name} 的回复。
|
||||
等了大约 {elapsed_minutes:.1f} 分钟(你原本打算最多等 {max_wait_minutes:.1f} 分钟)。
|
||||
现在 {user_name} 回复了!
|
||||
|
||||
请决定你接下来要怎么回应。""",
|
||||
)
|
||||
|
||||
KFC_V2_SITUATION_REPLY_LATE = Prompt(
|
||||
name="kfc_v2_situation_reply_late",
|
||||
template="""现在是 {current_time}。
|
||||
|
||||
你之前发了消息后在等 {user_name} 的回复。
|
||||
你原本打算最多等 {max_wait_minutes:.1f} 分钟,但实际等了 {elapsed_minutes:.1f} 分钟才收到回复。
|
||||
虽然有点迟,但 {user_name} 终于回复了。
|
||||
|
||||
请决定你接下来要怎么回应。(可以选择轻轻抱怨一下迟到,也可以装作没在意)""",
|
||||
)
|
||||
|
||||
KFC_V2_SITUATION_TIMEOUT = Prompt(
|
||||
name="kfc_v2_situation_timeout",
|
||||
template="""现在是 {current_time}。
|
||||
|
||||
你之前发了消息后一直在等 {user_name} 的回复。
|
||||
你原本打算最多等 {max_wait_minutes:.1f} 分钟,现在已经等了 {elapsed_minutes:.1f} 分钟了,对方还是没回。
|
||||
你期待的反应是:"{expected_reaction}"
|
||||
|
||||
你需要决定:
|
||||
1. 继续等待(设置新的 max_wait_seconds)
|
||||
2. 主动说点什么打破沉默
|
||||
3. 做点别的事情(戳一戳、发表情等)
|
||||
4. 算了不等了(max_wait_seconds = 0)""",
|
||||
)
|
||||
|
||||
KFC_V2_SITUATION_PROACTIVE = Prompt(
|
||||
name="kfc_v2_situation_proactive",
|
||||
template="""现在是 {current_time}。
|
||||
|
||||
你和 {user_name} 已经有一段时间没聊天了(沉默了 {silence_duration})。
|
||||
{trigger_reason}
|
||||
|
||||
你在想要不要主动找 {user_name} 聊点什么。
|
||||
|
||||
请决定:
|
||||
1. 主动发起对话(想个话题开场)
|
||||
2. 发个表情或戳一戳试探一下
|
||||
3. 算了,现在不是好时机(do_nothing)
|
||||
|
||||
如果决定发起对话,想想用什么自然的方式开场,不要太突兀。""",
|
||||
)
|
||||
|
||||
# =================================================================================================
|
||||
# 活动流条目模板 - 用于构建 activity_stream
|
||||
# =================================================================================================
|
||||
|
||||
# 用户消息条目
|
||||
KFC_V2_ENTRY_USER_MESSAGE = Prompt(
|
||||
name="kfc_v2_entry_user_message",
|
||||
template="""【{time}】{user_name} 说:
|
||||
"{content}"
|
||||
""",
|
||||
)
|
||||
|
||||
# Bot 规划条目(有等待)
|
||||
KFC_V2_ENTRY_BOT_PLANNING = Prompt(
|
||||
name="kfc_v2_entry_bot_planning",
|
||||
template="""【你的想法】
|
||||
内心:{thought}
|
||||
行动:{actions_description}
|
||||
期待:{expected_reaction}
|
||||
决定等待:最多 {max_wait_minutes:.1f} 分钟
|
||||
""",
|
||||
)
|
||||
|
||||
# Bot 规划条目(无等待)
|
||||
KFC_V2_ENTRY_BOT_PLANNING_NO_WAIT = Prompt(
|
||||
name="kfc_v2_entry_bot_planning_no_wait",
|
||||
template="""【你的想法】
|
||||
内心:{thought}
|
||||
行动:{actions_description}
|
||||
(不打算等对方回复)
|
||||
""",
|
||||
)
|
||||
|
||||
# 等待期间心理变化
|
||||
KFC_V2_ENTRY_WAITING_UPDATE = Prompt(
|
||||
name="kfc_v2_entry_waiting_update",
|
||||
template="""【等待中... {elapsed_minutes:.1f} 分钟过去了】
|
||||
你想:{waiting_thought}
|
||||
""",
|
||||
)
|
||||
|
||||
# 收到及时回复时的标注
|
||||
KFC_V2_ENTRY_REPLY_IN_TIME = Prompt(
|
||||
name="kfc_v2_entry_reply_in_time",
|
||||
template="""→ (对方在你预期时间内回复了,等了 {elapsed_minutes:.1f} 分钟)
|
||||
""",
|
||||
)
|
||||
|
||||
# 收到迟到回复时的标注
|
||||
KFC_V2_ENTRY_REPLY_LATE = Prompt(
|
||||
name="kfc_v2_entry_reply_late",
|
||||
template="""→ (对方回复迟了,你原本只打算等 {max_wait_minutes:.1f} 分钟,实际等了 {elapsed_minutes:.1f} 分钟)
|
||||
""",
|
||||
)
|
||||
|
||||
# 主动思考触发
|
||||
KFC_V2_ENTRY_PROACTIVE_TRIGGER = Prompt(
|
||||
name="kfc_v2_entry_proactive_trigger",
|
||||
template="""【沉默了 {silence_duration}】
|
||||
你开始考虑要不要主动找对方聊点什么...
|
||||
""",
|
||||
)
|
||||
|
||||
# 导出所有模板名称,方便外部引用
|
||||
PROMPT_NAMES = {
|
||||
"main": "kfc_v2_main",
|
||||
"output_format": "kfc_v2_output_format",
|
||||
"situation_new_message": "kfc_v2_situation_new_message",
|
||||
"situation_reply_in_time": "kfc_v2_situation_reply_in_time",
|
||||
"situation_reply_late": "kfc_v2_situation_reply_late",
|
||||
"situation_timeout": "kfc_v2_situation_timeout",
|
||||
"situation_proactive": "kfc_v2_situation_proactive",
|
||||
"entry_user_message": "kfc_v2_entry_user_message",
|
||||
"entry_bot_planning": "kfc_v2_entry_bot_planning",
|
||||
"entry_bot_planning_no_wait": "kfc_v2_entry_bot_planning_no_wait",
|
||||
"entry_waiting_update": "kfc_v2_entry_waiting_update",
|
||||
"entry_reply_in_time": "kfc_v2_entry_reply_in_time",
|
||||
"entry_reply_late": "kfc_v2_entry_reply_late",
|
||||
"entry_proactive_trigger": "kfc_v2_entry_proactive_trigger",
|
||||
}
|
||||
107
src/plugins/built_in/kokoro_flow_chatter_v2/replyer.py
Normal file
107
src/plugins/built_in/kokoro_flow_chatter_v2/replyer.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Kokoro Flow Chatter V2 - Replyer
|
||||
|
||||
简化的回复生成模块,使用插件系统的 llm_api
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.apis import llm_api
|
||||
from src.utils.json_parser import extract_and_parse_json
|
||||
|
||||
from .models import LLMResponse
|
||||
from .prompt.builder import get_prompt_builder
|
||||
from .session import KokoroSession
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
|
||||
logger = get_logger("kfc_v2_replyer")
|
||||
|
||||
|
||||
async def generate_response(
|
||||
session: KokoroSession,
|
||||
user_name: str,
|
||||
situation_type: str = "new_message",
|
||||
chat_stream: Optional["ChatStream"] = None,
|
||||
available_actions: Optional[dict] = None,
|
||||
extra_context: Optional[dict] = None,
|
||||
) -> LLMResponse:
|
||||
"""
|
||||
生成回复
|
||||
|
||||
Args:
|
||||
session: 会话对象
|
||||
user_name: 用户名称
|
||||
situation_type: 情况类型
|
||||
chat_stream: 聊天流对象
|
||||
available_actions: 可用动作字典
|
||||
extra_context: 额外上下文
|
||||
|
||||
Returns:
|
||||
LLMResponse 对象
|
||||
"""
|
||||
try:
|
||||
# 1. 构建提示词
|
||||
prompt_builder = get_prompt_builder()
|
||||
prompt = await prompt_builder.build_prompt(
|
||||
session=session,
|
||||
user_name=user_name,
|
||||
situation_type=situation_type,
|
||||
chat_stream=chat_stream,
|
||||
available_actions=available_actions,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
logger.debug(f"[KFC Replyer] 构建的提示词:\n{prompt}")
|
||||
|
||||
# 2. 获取模型配置并调用 LLM
|
||||
models = llm_api.get_available_models()
|
||||
replyer_config = models.get("replyer")
|
||||
|
||||
if not replyer_config:
|
||||
logger.error("[KFC Replyer] 未找到 replyer 模型配置")
|
||||
return LLMResponse.create_error_response("未找到 replyer 模型配置")
|
||||
|
||||
success, raw_response, reasoning, model_name = await llm_api.generate_with_model(
|
||||
prompt=prompt,
|
||||
model_config=replyer_config,
|
||||
request_type="kokoro_flow_chatter_v2",
|
||||
)
|
||||
|
||||
if not success:
|
||||
logger.error(f"[KFC Replyer] LLM 调用失败: {raw_response}")
|
||||
return LLMResponse.create_error_response(raw_response)
|
||||
|
||||
logger.debug(f"[KFC Replyer] LLM 响应 (model={model_name}):\n{raw_response}")
|
||||
|
||||
# 3. 解析响应
|
||||
return _parse_response(raw_response)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[KFC Replyer] 生成失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return LLMResponse.create_error_response(str(e))
|
||||
|
||||
|
||||
def _parse_response(raw_response: str) -> LLMResponse:
|
||||
"""解析 LLM 响应"""
|
||||
data = extract_and_parse_json(raw_response, strict=False)
|
||||
|
||||
if not data or not isinstance(data, dict):
|
||||
logger.warning(f"[KFC Replyer] 无法解析 JSON: {raw_response[:200]}...")
|
||||
return LLMResponse.create_error_response("无法解析响应格式")
|
||||
|
||||
response = LLMResponse.from_dict(data)
|
||||
|
||||
if response.thought:
|
||||
logger.info(
|
||||
f"[KFC Replyer] 解析成功: thought={response.thought[:50]}..., "
|
||||
f"actions={[a.type for a in response.actions]}"
|
||||
)
|
||||
else:
|
||||
logger.warning("[KFC Replyer] 响应缺少 thought")
|
||||
|
||||
return response
|
||||
386
src/plugins/built_in/kokoro_flow_chatter_v2/session.py
Normal file
386
src/plugins/built_in/kokoro_flow_chatter_v2/session.py
Normal file
@@ -0,0 +1,386 @@
|
||||
"""
|
||||
Kokoro Flow Chatter V2 - 会话管理
|
||||
|
||||
极简的会话状态管理:
|
||||
- Session 只有 IDLE 和 WAITING 两种状态
|
||||
- 包含 mental_log(心理活动历史)
|
||||
- 包含 waiting_config(等待配置)
|
||||
"""
|
||||
|
||||
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 (
|
||||
EventType,
|
||||
MentalLogEntry,
|
||||
SessionStatus,
|
||||
WaitingConfig,
|
||||
)
|
||||
|
||||
logger = get_logger("kfc_v2_session")
|
||||
|
||||
|
||||
class KokoroSession:
|
||||
"""
|
||||
Kokoro Flow Chatter V2 会话
|
||||
|
||||
为每个私聊用户维护一个独立的会话,包含:
|
||||
- 基本信息(user_id, stream_id)
|
||||
- 状态(只有 IDLE 和 WAITING)
|
||||
- 心理活动历史(mental_log)
|
||||
- 等待配置(waiting_config)
|
||||
"""
|
||||
|
||||
# 心理活动日志最大保留条数
|
||||
MAX_MENTAL_LOG_SIZE = 50
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: str,
|
||||
stream_id: str,
|
||||
):
|
||||
self.user_id = user_id
|
||||
self.stream_id = stream_id
|
||||
|
||||
# 状态(只有 IDLE 和 WAITING)
|
||||
self._status: SessionStatus = SessionStatus.IDLE
|
||||
|
||||
# 心理活动历史
|
||||
self.mental_log: list[MentalLogEntry] = []
|
||||
|
||||
# 等待配置
|
||||
self.waiting_config: WaitingConfig = WaitingConfig()
|
||||
|
||||
# 时间戳
|
||||
self.created_at: float = time.time()
|
||||
self.last_activity_at: float = time.time()
|
||||
|
||||
# 统计
|
||||
self.total_interactions: int = 0
|
||||
|
||||
# 上次主动思考时间
|
||||
self.last_proactive_at: Optional[float] = None
|
||||
|
||||
@property
|
||||
def status(self) -> SessionStatus:
|
||||
return self._status
|
||||
|
||||
@status.setter
|
||||
def status(self, value: SessionStatus) -> None:
|
||||
old_status = self._status
|
||||
self._status = value
|
||||
if old_status != value:
|
||||
logger.debug(f"Session {self.user_id} 状态变更: {old_status} → {value}")
|
||||
|
||||
def add_entry(self, entry: MentalLogEntry) -> None:
|
||||
"""添加心理活动日志条目"""
|
||||
self.mental_log.append(entry)
|
||||
self.last_activity_at = time.time()
|
||||
|
||||
# 保持日志在合理大小
|
||||
if len(self.mental_log) > self.MAX_MENTAL_LOG_SIZE:
|
||||
self.mental_log = self.mental_log[-self.MAX_MENTAL_LOG_SIZE:]
|
||||
|
||||
def add_user_message(
|
||||
self,
|
||||
content: str,
|
||||
user_name: str,
|
||||
user_id: str,
|
||||
timestamp: Optional[float] = None,
|
||||
) -> MentalLogEntry:
|
||||
"""添加用户消息事件"""
|
||||
entry = MentalLogEntry(
|
||||
event_type=EventType.USER_MESSAGE,
|
||||
timestamp=timestamp or time.time(),
|
||||
content=content,
|
||||
user_name=user_name,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# 如果之前在等待,记录收到回复的情况
|
||||
if self.status == SessionStatus.WAITING and self.waiting_config.is_active():
|
||||
elapsed = self.waiting_config.get_elapsed_seconds()
|
||||
max_wait = self.waiting_config.max_wait_seconds
|
||||
|
||||
if elapsed <= max_wait:
|
||||
entry.metadata["reply_status"] = "in_time"
|
||||
entry.metadata["elapsed_seconds"] = elapsed
|
||||
entry.metadata["max_wait_seconds"] = max_wait
|
||||
else:
|
||||
entry.metadata["reply_status"] = "late"
|
||||
entry.metadata["elapsed_seconds"] = elapsed
|
||||
entry.metadata["max_wait_seconds"] = max_wait
|
||||
|
||||
self.add_entry(entry)
|
||||
return entry
|
||||
|
||||
def add_bot_planning(
|
||||
self,
|
||||
thought: str,
|
||||
actions: list[dict],
|
||||
expected_reaction: str = "",
|
||||
max_wait_seconds: int = 0,
|
||||
timestamp: Optional[float] = None,
|
||||
) -> MentalLogEntry:
|
||||
"""添加 Bot 规划事件"""
|
||||
entry = MentalLogEntry(
|
||||
event_type=EventType.BOT_PLANNING,
|
||||
timestamp=timestamp or time.time(),
|
||||
thought=thought,
|
||||
actions=actions,
|
||||
expected_reaction=expected_reaction,
|
||||
max_wait_seconds=max_wait_seconds,
|
||||
)
|
||||
self.add_entry(entry)
|
||||
self.total_interactions += 1
|
||||
return entry
|
||||
|
||||
def add_waiting_update(
|
||||
self,
|
||||
waiting_thought: str,
|
||||
mood: str = "",
|
||||
timestamp: Optional[float] = None,
|
||||
) -> MentalLogEntry:
|
||||
"""添加等待期间的心理变化"""
|
||||
entry = MentalLogEntry(
|
||||
event_type=EventType.WAITING_UPDATE,
|
||||
timestamp=timestamp or time.time(),
|
||||
waiting_thought=waiting_thought,
|
||||
mood=mood,
|
||||
elapsed_seconds=self.waiting_config.get_elapsed_seconds(),
|
||||
)
|
||||
self.add_entry(entry)
|
||||
return entry
|
||||
|
||||
def start_waiting(
|
||||
self,
|
||||
expected_reaction: str,
|
||||
max_wait_seconds: int,
|
||||
) -> None:
|
||||
"""开始等待"""
|
||||
if max_wait_seconds <= 0:
|
||||
# 不等待,直接进入 IDLE
|
||||
self.status = SessionStatus.IDLE
|
||||
self.waiting_config.reset()
|
||||
return
|
||||
|
||||
self.status = SessionStatus.WAITING
|
||||
self.waiting_config = WaitingConfig(
|
||||
expected_reaction=expected_reaction,
|
||||
max_wait_seconds=max_wait_seconds,
|
||||
started_at=time.time(),
|
||||
last_thinking_at=0.0,
|
||||
thinking_count=0,
|
||||
)
|
||||
logger.debug(
|
||||
f"Session {self.user_id} 开始等待: "
|
||||
f"max_wait={max_wait_seconds}s, expected={expected_reaction[:30]}..."
|
||||
)
|
||||
|
||||
def end_waiting(self) -> None:
|
||||
"""结束等待"""
|
||||
self.status = SessionStatus.IDLE
|
||||
self.waiting_config.reset()
|
||||
|
||||
def get_recent_entries(self, limit: int = 20) -> list[MentalLogEntry]:
|
||||
"""获取最近的心理活动日志"""
|
||||
return self.mental_log[-limit:] if self.mental_log else []
|
||||
|
||||
def get_last_bot_message(self) -> Optional[str]:
|
||||
"""获取最后一条 Bot 发送的消息"""
|
||||
for entry in reversed(self.mental_log):
|
||||
if entry.event_type == EventType.BOT_PLANNING:
|
||||
for action in entry.actions:
|
||||
if action.get("type") in ("reply", "respond"):
|
||||
return action.get("content", "")
|
||||
return None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""转换为字典(用于持久化)"""
|
||||
return {
|
||||
"user_id": self.user_id,
|
||||
"stream_id": self.stream_id,
|
||||
"status": str(self.status),
|
||||
"mental_log": [e.to_dict() for e in self.mental_log],
|
||||
"waiting_config": self.waiting_config.to_dict(),
|
||||
"created_at": self.created_at,
|
||||
"last_activity_at": self.last_activity_at,
|
||||
"total_interactions": self.total_interactions,
|
||||
"last_proactive_at": self.last_proactive_at,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "KokoroSession":
|
||||
"""从字典创建会话"""
|
||||
session = cls(
|
||||
user_id=data.get("user_id", ""),
|
||||
stream_id=data.get("stream_id", ""),
|
||||
)
|
||||
|
||||
# 状态
|
||||
status_str = data.get("status", "idle")
|
||||
try:
|
||||
session._status = SessionStatus(status_str)
|
||||
except ValueError:
|
||||
session._status = SessionStatus.IDLE
|
||||
|
||||
# 心理活动历史
|
||||
mental_log_data = data.get("mental_log", [])
|
||||
session.mental_log = [MentalLogEntry.from_dict(e) for e in mental_log_data]
|
||||
|
||||
# 等待配置
|
||||
waiting_data = data.get("waiting_config", {})
|
||||
session.waiting_config = WaitingConfig.from_dict(waiting_data)
|
||||
|
||||
# 时间戳
|
||||
session.created_at = data.get("created_at", time.time())
|
||||
session.last_activity_at = data.get("last_activity_at", time.time())
|
||||
session.total_interactions = data.get("total_interactions", 0)
|
||||
session.last_proactive_at = data.get("last_proactive_at")
|
||||
|
||||
return session
|
||||
|
||||
|
||||
class SessionManager:
|
||||
"""
|
||||
会话管理器
|
||||
|
||||
负责会话的创建、获取、保存和清理
|
||||
"""
|
||||
|
||||
_instance: Optional["SessionManager"] = None
|
||||
|
||||
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_v2/sessions",
|
||||
max_session_age_days: int = 30,
|
||||
):
|
||||
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._sessions: dict[str, KokoroSession] = {}
|
||||
self._locks: dict[str, asyncio.Lock] = {}
|
||||
|
||||
# 确保数据目录存在
|
||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.info(f"SessionManager 初始化完成: {self.data_dir}")
|
||||
|
||||
def _get_lock(self, user_id: str) -> asyncio.Lock:
|
||||
"""获取用户级别的锁"""
|
||||
if user_id not in self._locks:
|
||||
self._locks[user_id] = asyncio.Lock()
|
||||
return self._locks[user_id]
|
||||
|
||||
def _get_file_path(self, user_id: str) -> Path:
|
||||
"""获取会话文件路径"""
|
||||
safe_id = "".join(c if c.isalnum() or c in "-_" else "_" for c in user_id)
|
||||
return self.data_dir / f"{safe_id}.json"
|
||||
|
||||
async def get_session(self, user_id: str, stream_id: str) -> KokoroSession:
|
||||
"""获取或创建会话"""
|
||||
async with self._get_lock(user_id):
|
||||
# 检查内存缓存
|
||||
if user_id in self._sessions:
|
||||
session = self._sessions[user_id]
|
||||
session.stream_id = stream_id # 更新 stream_id
|
||||
return session
|
||||
|
||||
# 尝试从文件加载
|
||||
session = await self._load_from_file(user_id)
|
||||
if session:
|
||||
session.stream_id = stream_id
|
||||
self._sessions[user_id] = session
|
||||
return session
|
||||
|
||||
# 创建新会话
|
||||
session = KokoroSession(user_id=user_id, stream_id=stream_id)
|
||||
self._sessions[user_id] = session
|
||||
logger.info(f"创建新会话: {user_id}")
|
||||
return session
|
||||
|
||||
async def _load_from_file(self, user_id: str) -> Optional[KokoroSession]:
|
||||
"""从文件加载会话"""
|
||||
file_path = self._get_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 Exception as e:
|
||||
logger.error(f"加载会话失败 {user_id}: {e}")
|
||||
return None
|
||||
|
||||
async def save_session(self, user_id: str) -> bool:
|
||||
"""保存会话到文件"""
|
||||
async with self._get_lock(user_id):
|
||||
if user_id not in self._sessions:
|
||||
return False
|
||||
|
||||
session = self._sessions[user_id]
|
||||
file_path = self._get_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)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"保存会话失败 {user_id}: {e}")
|
||||
return False
|
||||
|
||||
async def save_all(self) -> int:
|
||||
"""保存所有会话"""
|
||||
count = 0
|
||||
for user_id in list(self._sessions.keys()):
|
||||
if await self.save_session(user_id):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
async def get_waiting_sessions(self) -> list[KokoroSession]:
|
||||
"""获取所有处于等待状态的会话"""
|
||||
return [s for s in self._sessions.values() if s.status == SessionStatus.WAITING]
|
||||
|
||||
async def get_all_sessions(self) -> list[KokoroSession]:
|
||||
"""获取所有会话"""
|
||||
return list(self._sessions.values())
|
||||
|
||||
def get_session_sync(self, user_id: str) -> Optional[KokoroSession]:
|
||||
"""同步获取会话(仅从内存)"""
|
||||
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
|
||||
Reference in New Issue
Block a user