feat: 添加KFC V2专属动作模块及相关功能,优化回复机制
This commit is contained in:
@@ -1,13 +1,10 @@
|
||||
import asyncio
|
||||
import time
|
||||
import traceback
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.common.data_models.database_data_model import DatabaseMessages
|
||||
from src.common.logger import get_logger
|
||||
from src.person_info.person_info import get_person_info_manager
|
||||
from src.plugin_system.apis import database_api
|
||||
from src.plugin_system.base.base_action import BaseAction
|
||||
from src.plugin_system.base.component_types import ActionInfo, ComponentType
|
||||
from src.plugin_system.core.component_registry import component_registry
|
||||
@@ -20,23 +17,19 @@ logger = get_logger("action_manager")
|
||||
|
||||
class ChatterActionManager:
|
||||
"""
|
||||
动作管理器,用于管理各种类型的动作
|
||||
动作管理器,用于管理和执行动作
|
||||
|
||||
现在统一使用新插件系统,简化了原有的新旧兼容逻辑。
|
||||
职责:
|
||||
- 加载和管理可用动作集
|
||||
- 创建动作实例
|
||||
- 执行动作(所有动作逻辑在 Action.execute() 中实现)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化动作管理器"""
|
||||
|
||||
# 当前正在使用的动作集合,在规划开始时加载
|
||||
self._using_actions: dict[str, ActionInfo] = {}
|
||||
self.chat_id: str | None = None
|
||||
|
||||
self.log_prefix: str = "ChatterActionManager"
|
||||
# 批量存储支持
|
||||
self._batch_storage_enabled = False
|
||||
self._pending_actions = []
|
||||
self._current_chat_id = None
|
||||
|
||||
async def load_actions(self, stream_id: str | None):
|
||||
"""根据 stream_id 加载当前可用的动作"""
|
||||
@@ -44,8 +37,6 @@ class ChatterActionManager:
|
||||
self._using_actions = component_registry.get_default_actions(stream_id)
|
||||
logger.debug(f"已为 stream '{stream_id}' 加载 {len(self._using_actions)} 个可用动作: {list(self._using_actions.keys())}")
|
||||
|
||||
# === 执行Action方法 ===
|
||||
|
||||
@staticmethod
|
||||
def create_action(
|
||||
action_name: str,
|
||||
@@ -70,12 +61,13 @@ class ChatterActionManager:
|
||||
chat_stream: 聊天流
|
||||
log_prefix: 日志前缀
|
||||
shutting_down: 是否正在关闭
|
||||
action_message: 目标消息
|
||||
|
||||
Returns:
|
||||
Optional[BaseAction]: 创建的动作处理器实例,如果动作名称未注册则返回None
|
||||
BaseAction | None: 创建的动作处理器实例
|
||||
"""
|
||||
try:
|
||||
# 获取组件类 - 明确指定查询Action类型
|
||||
# 获取组件类
|
||||
component_class: type[BaseAction] = component_registry.get_component_class(
|
||||
action_name, ComponentType.ACTION
|
||||
) # type: ignore
|
||||
@@ -110,8 +102,6 @@ class ChatterActionManager:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建Action实例失败 {action_name}: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
@@ -119,17 +109,8 @@ class ChatterActionManager:
|
||||
"""获取当前正在使用的动作集合"""
|
||||
return self._using_actions.copy()
|
||||
|
||||
# === Modify相关方法 ===
|
||||
def remove_action_from_using(self, action_name: str) -> bool:
|
||||
"""
|
||||
从当前使用的动作集中移除指定动作
|
||||
|
||||
Args:
|
||||
action_name: 动作名称
|
||||
|
||||
Returns:
|
||||
bool: 移除是否成功
|
||||
"""
|
||||
"""从当前使用的动作集中移除指定动作"""
|
||||
if action_name not in self._using_actions:
|
||||
logger.warning(f"移除失败: 动作 {action_name} 不在当前使用的动作集中")
|
||||
return False
|
||||
@@ -141,7 +122,6 @@ class ChatterActionManager:
|
||||
async def restore_actions(self) -> None:
|
||||
"""恢复到当前 stream_id 的默认动作集"""
|
||||
actions_to_restore = list(self._using_actions.keys())
|
||||
# 使用 self.chat_id 来恢复当前上下文的动作
|
||||
await self.load_actions(self.chat_id)
|
||||
logger.debug(f"恢复动作集: 从 {actions_to_restore} 恢复到 stream '{self.chat_id}' 的默认动作集 {list(self._using_actions.keys())}")
|
||||
|
||||
@@ -157,13 +137,13 @@ class ChatterActionManager:
|
||||
clear_unread_messages: bool = True,
|
||||
) -> Any:
|
||||
"""
|
||||
执行单个动作的通用函数
|
||||
执行单个动作
|
||||
|
||||
所有动作(包括 reply/respond)都通过 BaseAction.execute() 执行
|
||||
所有动作逻辑都在 BaseAction.execute() 中实现
|
||||
|
||||
Args:
|
||||
action_name: 动作名称
|
||||
chat_id: 聊天id
|
||||
chat_id: 聊天ID
|
||||
target_message: 目标消息
|
||||
reasoning: 执行理由
|
||||
action_data: 动作数据
|
||||
@@ -172,16 +152,16 @@ class ChatterActionManager:
|
||||
clear_unread_messages: 是否清除未读消息
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
执行结果字典
|
||||
"""
|
||||
chat_stream = None
|
||||
try:
|
||||
# 通过chat_id获取chat_stream
|
||||
# 获取 chat_stream
|
||||
chat_manager = get_chat_manager()
|
||||
chat_stream = await chat_manager.get_stream(chat_id)
|
||||
|
||||
if not chat_stream:
|
||||
logger.error(f"{log_prefix} 无法找到chat_id对应的chat_stream: {chat_id}")
|
||||
logger.error(f"{log_prefix} 无法找到 chat_stream: {chat_id}")
|
||||
return {
|
||||
"action_type": action_name,
|
||||
"success": False,
|
||||
@@ -189,66 +169,75 @@ class ChatterActionManager:
|
||||
"error": "chat_stream not found",
|
||||
}
|
||||
|
||||
# 设置正在回复的状态
|
||||
# 设置正在处理的状态
|
||||
chat_stream.context.is_replying = True
|
||||
|
||||
# no_action 特殊处理
|
||||
if action_name == "no_action":
|
||||
return {"action_type": "no_action", "success": True, "reply_text": "", "command": ""}
|
||||
return {"action_type": "no_action", "success": True, "reply_text": ""}
|
||||
|
||||
# 统一通过 _handle_action 执行所有动作
|
||||
success, reply_text, command = await self._handle_action(
|
||||
chat_stream,
|
||||
action_name,
|
||||
reasoning,
|
||||
action_data or {},
|
||||
{}, # cycle_timers
|
||||
thinking_id,
|
||||
target_message,
|
||||
# 创建并执行动作
|
||||
action_handler = self.create_action(
|
||||
action_name=action_name,
|
||||
action_data=action_data or {},
|
||||
reasoning=reasoning,
|
||||
cycle_timers={},
|
||||
thinking_id=thinking_id or "",
|
||||
chat_stream=chat_stream,
|
||||
log_prefix=log_prefix or self.log_prefix,
|
||||
action_message=target_message,
|
||||
)
|
||||
|
||||
# 记录执行的动作到目标消息
|
||||
if not action_handler:
|
||||
logger.error(f"{log_prefix} 创建动作处理器失败: {action_name}")
|
||||
return {
|
||||
"action_type": action_name,
|
||||
"success": False,
|
||||
"reply_text": "",
|
||||
"error": f"Failed to create action handler: {action_name}",
|
||||
}
|
||||
|
||||
# 执行动作
|
||||
success, reply_text = await action_handler.handle_action()
|
||||
|
||||
# 记录动作到消息并存储动作信息
|
||||
if success:
|
||||
asyncio.create_task(self._record_action_to_message(chat_stream, action_name, target_message, action_data))
|
||||
# 重置打断计数
|
||||
await self._reset_interruption_count_after_action(chat_stream.stream_id)
|
||||
asyncio.create_task(self._reset_interruption_count(chat_stream.stream_id))
|
||||
# 统一存储动作信息
|
||||
asyncio.create_task(
|
||||
self._store_action_info(
|
||||
action_handler=action_handler,
|
||||
action_name=action_name,
|
||||
reply_text=reply_text,
|
||||
target_message=target_message,
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"action_type": action_name,
|
||||
"success": success,
|
||||
"reply_text": reply_text,
|
||||
"command": command,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{log_prefix} 执行动作时出错: {e}")
|
||||
logger.error(f"{log_prefix} 错误信息: {traceback.format_exc()}")
|
||||
logger.error(traceback.format_exc())
|
||||
return {
|
||||
"action_type": action_name,
|
||||
"success": False,
|
||||
"reply_text": "",
|
||||
"loop_info": None,
|
||||
"error": str(e),
|
||||
}
|
||||
finally:
|
||||
# 确保重置正在回复的状态
|
||||
if chat_stream:
|
||||
chat_stream.context.is_replying = False
|
||||
|
||||
async def _record_action_to_message(self, chat_stream, action_name, target_message, action_data):
|
||||
"""
|
||||
记录执行的动作到目标消息中
|
||||
|
||||
Args:
|
||||
chat_stream: ChatStream实例
|
||||
action_name: 动作名称
|
||||
target_message: 目标消息
|
||||
action_data: 动作数据
|
||||
"""
|
||||
async def _record_action_to_message(self, chat_stream, action_name: str, target_message, action_data: dict | None):
|
||||
"""记录执行的动作到目标消息"""
|
||||
try:
|
||||
from src.chat.message_manager.message_manager import message_manager
|
||||
|
||||
# 获取目标消息ID
|
||||
target_message_id = None
|
||||
if target_message:
|
||||
target_message_id = target_message.message_id
|
||||
@@ -256,362 +245,66 @@ class ChatterActionManager:
|
||||
target_message_id = action_data.get("target_message_id")
|
||||
|
||||
if not target_message_id:
|
||||
logger.debug(f"无法获取目标消息ID,动作: {action_name}")
|
||||
return
|
||||
|
||||
# 通过message_manager更新消息的动作记录并刷新focus_energy
|
||||
await message_manager.add_action(
|
||||
stream_id=chat_stream.stream_id, message_id=target_message_id, action=action_name
|
||||
stream_id=chat_stream.stream_id,
|
||||
message_id=target_message_id,
|
||||
action=action_name,
|
||||
)
|
||||
logger.debug(f"已记录动作 {action_name} 到消息 {target_message_id} 并更新focus_energy")
|
||||
logger.debug(f"已记录动作 {action_name} 到消息 {target_message_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"记录动作到消息失败: {e}")
|
||||
# 不抛出异常,避免影响主要功能
|
||||
|
||||
async def _reset_interruption_count_after_action(self, stream_id: str):
|
||||
"""在动作执行成功后重置打断计数"""
|
||||
|
||||
async def _reset_interruption_count(self, stream_id: str):
|
||||
"""重置打断计数"""
|
||||
try:
|
||||
from src.plugin_system.apis.chat_api import get_chat_manager
|
||||
|
||||
chat_manager = get_chat_manager()
|
||||
chat_stream = await chat_manager.get_stream(stream_id)
|
||||
if chat_stream:
|
||||
context = chat_stream.context
|
||||
if context.interruption_count > 0:
|
||||
old_count = context.interruption_count
|
||||
# old_afc_adjustment = context.context.get_afc_threshold_adjustment()
|
||||
await context.reset_interruption_count()
|
||||
logger.debug(
|
||||
f"动作执行成功,重置聊天流 {stream_id} 的打断计数: {old_count} -> 0"
|
||||
)
|
||||
if chat_stream and chat_stream.context.interruption_count > 0:
|
||||
old_count = chat_stream.context.interruption_count
|
||||
await chat_stream.context.reset_interruption_count()
|
||||
logger.debug(f"重置打断计数: {old_count} -> 0")
|
||||
except Exception as e:
|
||||
logger.warning(f"重置打断计数时出错: {e}")
|
||||
|
||||
async def _handle_action(
|
||||
self, chat_stream, action, reasoning, action_data, cycle_timers, thinking_id, action_message
|
||||
) -> tuple[bool, str, str]:
|
||||
"""
|
||||
处理具体的动作执行
|
||||
|
||||
Args:
|
||||
chat_stream: ChatStream实例
|
||||
action: 动作名称
|
||||
reasoning: 执行理由
|
||||
action_data: 动作数据
|
||||
cycle_timers: 循环计时器
|
||||
thinking_id: 思考ID
|
||||
action_message: 动作消息
|
||||
|
||||
Returns:
|
||||
tuple: (执行是否成功, 回复文本, 命令文本)
|
||||
|
||||
功能说明:
|
||||
- 创建对应的动作处理器
|
||||
- 执行动作并捕获异常
|
||||
- 返回执行结果供上级方法整合
|
||||
"""
|
||||
if not chat_stream:
|
||||
return False, "", ""
|
||||
try:
|
||||
# 创建动作处理器
|
||||
action_handler = self.create_action(
|
||||
action_name=action,
|
||||
action_data=action_data,
|
||||
reasoning=reasoning,
|
||||
cycle_timers=cycle_timers,
|
||||
thinking_id=thinking_id,
|
||||
chat_stream=chat_stream,
|
||||
log_prefix=self.log_prefix,
|
||||
action_message=action_message,
|
||||
)
|
||||
if not action_handler:
|
||||
# 动作处理器创建失败,尝试回退机制
|
||||
logger.warning(f"{self.log_prefix} 创建动作处理器失败: {action},尝试回退方案")
|
||||
|
||||
# 获取当前可用的动作
|
||||
available_actions = self.get_using_actions()
|
||||
fallback_action = None
|
||||
|
||||
# 回退优先级:reply > 第一个可用动作
|
||||
if "reply" in available_actions:
|
||||
fallback_action = "reply"
|
||||
elif available_actions:
|
||||
fallback_action = next(iter(available_actions.keys()))
|
||||
|
||||
if fallback_action and fallback_action != action:
|
||||
logger.info(f"{self.log_prefix} 使用回退动作: {fallback_action}")
|
||||
action_handler = self.create_action(
|
||||
action_name=fallback_action,
|
||||
action_data=action_data,
|
||||
reasoning=f"原动作'{action}'不可用,自动回退。{reasoning}",
|
||||
cycle_timers=cycle_timers,
|
||||
thinking_id=thinking_id,
|
||||
chat_stream=chat_stream,
|
||||
log_prefix=self.log_prefix,
|
||||
action_message=action_message,
|
||||
)
|
||||
|
||||
if not action_handler:
|
||||
logger.error(f"{self.log_prefix} 回退方案也失败,无法创建任何动作处理器")
|
||||
return False, "", ""
|
||||
|
||||
# 执行动作
|
||||
success, reply_text = await action_handler.handle_action()
|
||||
return success, reply_text, ""
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 处理{action}时出错: {e}")
|
||||
traceback.print_exc()
|
||||
return False, "", ""
|
||||
|
||||
async def _send_and_store_reply(
|
||||
async def _store_action_info(
|
||||
self,
|
||||
chat_stream: "ChatStream",
|
||||
response_set,
|
||||
loop_start_time,
|
||||
action_message,
|
||||
cycle_timers: dict[str, float],
|
||||
thinking_id,
|
||||
actions,
|
||||
should_quote_reply: bool | None = None,
|
||||
) -> tuple[str, dict[str, float]]:
|
||||
"""
|
||||
发送并存储回复信息
|
||||
action_handler: BaseAction,
|
||||
action_name: str,
|
||||
reply_text: str,
|
||||
target_message: DatabaseMessages | None,
|
||||
):
|
||||
"""统一存储动作信息到数据库"""
|
||||
try:
|
||||
from src.person_info.person_info import get_person_info_manager
|
||||
from src.plugin_system.apis import database_api
|
||||
|
||||
Args:
|
||||
chat_stream: ChatStream实例
|
||||
response_set: 回复内容集合
|
||||
loop_start_time: 循环开始时间
|
||||
action_message: 动作消息
|
||||
cycle_timers: 循环计时器
|
||||
thinking_id: 思考ID
|
||||
actions: 动作列表
|
||||
should_quote_reply: 是否应该引用回复原消息,None表示自动决定
|
||||
|
||||
Returns:
|
||||
Tuple[Dict[str, Any], str, Dict[str, float]]: 循环信息, 回复文本, 循环计时器
|
||||
"""
|
||||
# 发送回复
|
||||
with Timer("回复发送", cycle_timers):
|
||||
reply_text = await self.send_response(
|
||||
chat_stream, response_set, loop_start_time, action_message, should_quote_reply
|
||||
)
|
||||
|
||||
# 存储reply action信息
|
||||
# 构建 action_prompt_display
|
||||
action_prompt_display = ""
|
||||
if reply_text:
|
||||
person_info_manager = get_person_info_manager()
|
||||
|
||||
# 获取 platform,如果不存在则从 chat_stream 获取,如果还是 None 则使用默认值
|
||||
if action_message:
|
||||
platform = action_message.chat_info.platform
|
||||
user_id = action_message.user_info.user_id
|
||||
else:
|
||||
platform = getattr(chat_stream, "platform", "unknown")
|
||||
user_id = ""
|
||||
|
||||
# 获取用户信息并生成回复提示
|
||||
person_id = person_info_manager.get_person_id(
|
||||
platform,
|
||||
user_id,
|
||||
)
|
||||
if target_message:
|
||||
platform = target_message.chat_info.platform
|
||||
user_id = target_message.user_info.user_id
|
||||
person_id = person_info_manager.get_person_id(platform, user_id)
|
||||
person_name = await person_info_manager.get_value(person_id, "person_name")
|
||||
action_prompt_display = f"你对{person_name}进行了回复:{reply_text}"
|
||||
|
||||
# 存储动作信息到数据库(支持批量存储)
|
||||
if self._batch_storage_enabled:
|
||||
self.add_action_to_batch(
|
||||
action_name="reply",
|
||||
action_data={"reply_text": reply_text},
|
||||
thinking_id=thinking_id or "",
|
||||
action_done=True,
|
||||
action_build_into_prompt=False,
|
||||
action_prompt_display=action_prompt_display,
|
||||
)
|
||||
else:
|
||||
action_prompt_display = f"统一回应:{reply_text}"
|
||||
|
||||
# 存储动作信息
|
||||
await database_api.store_action_info(
|
||||
chat_stream=chat_stream,
|
||||
chat_stream=action_handler.chat_stream,
|
||||
action_build_into_prompt=False,
|
||||
action_prompt_display=action_prompt_display,
|
||||
action_done=True,
|
||||
thinking_id=thinking_id,
|
||||
action_data={"reply_text": reply_text},
|
||||
action_name="reply",
|
||||
thinking_id=action_handler.thinking_id,
|
||||
action_data={"reply_text": reply_text} if reply_text else action_handler.action_data,
|
||||
action_name=action_name,
|
||||
)
|
||||
|
||||
return reply_text, cycle_timers
|
||||
|
||||
async def send_response(
|
||||
self, chat_stream, reply_set, thinking_start_time, message_data, should_quote_reply: bool | None = None
|
||||
) -> str:
|
||||
"""
|
||||
发送回复内容的具体实现
|
||||
|
||||
Args:
|
||||
chat_stream: ChatStream实例
|
||||
reply_set: 回复内容集合,包含多个回复段
|
||||
thinking_start_time: 思考开始时间
|
||||
message_data: 消息数据
|
||||
should_quote_reply: 是否应该引用回复原消息,None表示自动决定
|
||||
|
||||
Returns:
|
||||
str: 完整的回复文本
|
||||
|
||||
功能说明:
|
||||
- 检查是否有新消息需要回复
|
||||
- 处理主动思考的"沉默"决定
|
||||
- 根据消息数量决定是否添加回复引用
|
||||
- 逐段发送回复内容,支持打字效果
|
||||
- 正确处理元组格式的回复段
|
||||
"""
|
||||
current_time = time.time()
|
||||
# 计算新消息数量
|
||||
await message_api.count_new_messages(
|
||||
chat_id=chat_stream.stream_id, start_time=thinking_start_time, end_time=current_time
|
||||
)
|
||||
|
||||
# 根据新消息数量决定是否需要引用回复
|
||||
reply_text = ""
|
||||
# 检查是否为主动思考消息
|
||||
if message_data:
|
||||
is_proactive_thinking = getattr(message_data, "message_type", None) == "proactive_thinking"
|
||||
else:
|
||||
is_proactive_thinking = True
|
||||
|
||||
logger.debug(f"[send_response] message_data: {message_data}")
|
||||
|
||||
first_replied = False
|
||||
for reply_seg in reply_set:
|
||||
# 调试日志:验证reply_seg的格式
|
||||
logger.debug(f"Processing reply_seg type: {type(reply_seg)}, content: {reply_seg}")
|
||||
|
||||
# 修正:正确处理元组格式 (格式为: (type, content))
|
||||
if isinstance(reply_seg, tuple) and len(reply_seg) >= 2:
|
||||
_, data = reply_seg
|
||||
else:
|
||||
# 向下兼容:如果已经是字符串,则直接使用
|
||||
data = str(reply_seg)
|
||||
|
||||
if isinstance(data, list):
|
||||
data = "".join(map(str, data))
|
||||
reply_text += data
|
||||
|
||||
# 如果是主动思考且内容为"沉默",则不发送
|
||||
if is_proactive_thinking and data.strip() == "沉默":
|
||||
logger.info(f"{self.log_prefix} 主动思考决定保持沉默,不发送消息")
|
||||
continue
|
||||
|
||||
# 发送第一段回复
|
||||
if not first_replied:
|
||||
# 决定是否引用回复
|
||||
is_private_chat = not bool(chat_stream.group_info)
|
||||
|
||||
# 如果明确指定了should_quote_reply,则使用指定值
|
||||
if should_quote_reply is not None:
|
||||
set_reply_flag = should_quote_reply and bool(message_data)
|
||||
logger.debug(
|
||||
f"📤 [ActionManager] 使用planner指定的引用设置: should_quote_reply={should_quote_reply}"
|
||||
)
|
||||
else:
|
||||
# 否则使用默认逻辑:默认不引用,让对话更流畅自然
|
||||
set_reply_flag = False
|
||||
logger.debug(
|
||||
f"📤 [ActionManager] 使用默认引用逻辑: 默认不引用(is_private={is_private_chat})"
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"📤 [ActionManager] 准备发送第一段回复。message_data: {message_data}, set_reply: {set_reply_flag}"
|
||||
)
|
||||
await send_api.text_to_stream(
|
||||
text=data,
|
||||
stream_id=chat_stream.stream_id,
|
||||
reply_to_message=message_data,
|
||||
set_reply=set_reply_flag,
|
||||
typing=False,
|
||||
)
|
||||
first_replied = True
|
||||
else:
|
||||
# 发送后续回复
|
||||
await send_api.text_to_stream(
|
||||
text=data,
|
||||
stream_id=chat_stream.stream_id,
|
||||
reply_to_message=None,
|
||||
set_reply=False,
|
||||
typing=True,
|
||||
)
|
||||
|
||||
return reply_text
|
||||
|
||||
def enable_batch_storage(self, chat_id: str):
|
||||
"""启用批量存储模式"""
|
||||
self._batch_storage_enabled = True
|
||||
self._current_chat_id = chat_id
|
||||
self._pending_actions.clear()
|
||||
logger.debug(f"已启用批量存储模式,chat_id: {chat_id}")
|
||||
|
||||
def disable_batch_storage(self):
|
||||
"""禁用批量存储模式"""
|
||||
self._batch_storage_enabled = False
|
||||
self._current_chat_id = None
|
||||
self._pending_actions = [] # 清空队列
|
||||
logger.debug("已禁用批量存储模式")
|
||||
|
||||
def add_action_to_batch(
|
||||
self,
|
||||
action_name: str,
|
||||
action_data: dict,
|
||||
thinking_id: str = "",
|
||||
action_done: bool = True,
|
||||
action_build_into_prompt: bool = False,
|
||||
action_prompt_display: str = "",
|
||||
):
|
||||
"""添加动作到批量存储列表"""
|
||||
if not self._batch_storage_enabled:
|
||||
return False
|
||||
|
||||
action_record = {
|
||||
"action_name": action_name,
|
||||
"action_data": action_data,
|
||||
"thinking_id": thinking_id,
|
||||
"action_done": action_done,
|
||||
"action_build_into_prompt": action_build_into_prompt,
|
||||
"action_prompt_display": action_prompt_display,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
self._pending_actions.append(action_record)
|
||||
logger.debug(f"已添加动作到批量存储列表: {action_name} (当前待处理: {len(self._pending_actions)} 个)")
|
||||
return True
|
||||
|
||||
async def flush_batch_storage(self, chat_stream):
|
||||
"""批量存储所有待处理的动作记录"""
|
||||
if not self._pending_actions:
|
||||
logger.debug("没有待处理的动作需要批量存储")
|
||||
return
|
||||
|
||||
try:
|
||||
logger.info(f"开始批量存储 {len(self._pending_actions)} 个动作记录")
|
||||
|
||||
# 批量存储所有动作
|
||||
stored_count = 0
|
||||
for action_data in self._pending_actions:
|
||||
try:
|
||||
result = await database_api.store_action_info(
|
||||
chat_stream=chat_stream,
|
||||
action_name=action_data.get("action_name", ""),
|
||||
action_data=action_data.get("action_data", {}),
|
||||
action_done=action_data.get("action_done", True),
|
||||
action_build_into_prompt=action_data.get("action_build_into_prompt", False),
|
||||
action_prompt_display=action_data.get("action_prompt_display", ""),
|
||||
thinking_id=action_data.get("thinking_id", ""),
|
||||
)
|
||||
if result:
|
||||
stored_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"存储单个动作记录失败: {e}")
|
||||
|
||||
logger.info(f"批量存储完成: 成功存储 {stored_count}/{len(self._pending_actions)} 个动作记录")
|
||||
|
||||
# 清空待处理列表
|
||||
self._pending_actions.clear()
|
||||
logger.debug(f"已存储动作信息: {action_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"批量存储动作记录时发生错误: {e}")
|
||||
logger.error(f"存储动作信息失败: {e}")
|
||||
|
||||
@@ -187,7 +187,7 @@ class ActionModifier:
|
||||
logger.debug(f"{self.log_prefix}阶段三移除动作: {action_name},原因: {reason}")
|
||||
|
||||
# === 统一日志记录 ===
|
||||
all_removals = chat_type_removals + removals_s1 + removals_s2 + removals_s3
|
||||
all_removals = removals_s0 + removals_s1 + removals_s2 + removals_s3
|
||||
removals_summary: str = ""
|
||||
if all_removals:
|
||||
removals_summary = " | ".join([f"{name}({reason})" for name, reason in all_removals])
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
AFC 专属动作模块
|
||||
"""
|
||||
|
||||
from .reply import ReplyAction, RespondAction
|
||||
|
||||
__all__ = ["ReplyAction", "RespondAction"]
|
||||
@@ -1,21 +1,23 @@
|
||||
"""
|
||||
回复动作模块
|
||||
AFC 回复动作模块
|
||||
|
||||
定义了三种回复相关动作:
|
||||
定义了两种回复相关动作:
|
||||
- reply: 针对单条消息的深度回复(使用 s4u 模板)
|
||||
- respond: 对未读消息的统一回应(使用 normal 模板)
|
||||
- no_reply: 选择不回复
|
||||
|
||||
这些动作是 AffinityFlowChatter 的专属动作。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import ClassVar
|
||||
|
||||
from src.common.data_models.database_data_model import DatabaseMessages
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.plugin_system import ActionActivationType, BaseAction, ChatMode
|
||||
from src.plugin_system.apis import database_api, generator_api, send_api
|
||||
from src.plugin_system.apis import generator_api, send_api
|
||||
|
||||
logger = get_logger("reply_actions")
|
||||
logger = get_logger("afc_reply_actions")
|
||||
|
||||
|
||||
class ReplyAction(BaseAction):
|
||||
@@ -63,8 +65,11 @@ class ReplyAction(BaseAction):
|
||||
async def execute(self) -> tuple[bool, str]:
|
||||
"""执行reply动作 - 完整的回复流程"""
|
||||
try:
|
||||
# 确保 action_message 是 DatabaseMessages 类型,否则使用 None
|
||||
reply_message = self.action_message if isinstance(self.action_message, DatabaseMessages) else None
|
||||
|
||||
# 检查目标消息是否为表情包
|
||||
if self.action_message and getattr(self.action_message, "is_emoji", False):
|
||||
if reply_message and getattr(reply_message, "is_emoji", False):
|
||||
if not getattr(global_config.chat, "allow_reply_to_emoji", True):
|
||||
logger.info(f"{self.log_prefix} 目标消息为表情包且配置不允许回复,跳过")
|
||||
return True, ""
|
||||
@@ -76,9 +81,9 @@ class ReplyAction(BaseAction):
|
||||
# 生成回复
|
||||
success, response_set, _ = await generator_api.generate_reply(
|
||||
chat_stream=self.chat_stream,
|
||||
reply_message=self.action_message,
|
||||
reply_message=reply_message,
|
||||
action_data=action_data,
|
||||
available_actions={self.action_name: None},
|
||||
available_actions={self.action_name: self.get_action_info()},
|
||||
enable_tool=global_config.tool.enable_tool,
|
||||
request_type="chat.replyer",
|
||||
from_plugin=False,
|
||||
@@ -91,9 +96,6 @@ class ReplyAction(BaseAction):
|
||||
# 发送回复
|
||||
reply_text = await self._send_response(response_set)
|
||||
|
||||
# 存储动作信息
|
||||
await self._store_action_info(reply_text)
|
||||
|
||||
logger.info(f"{self.log_prefix} reply 动作执行成功")
|
||||
return True, reply_text
|
||||
|
||||
@@ -112,6 +114,9 @@ class ReplyAction(BaseAction):
|
||||
should_quote = self.action_data.get("should_quote_reply", False)
|
||||
first_sent = False
|
||||
|
||||
# 确保 action_message 是 DatabaseMessages 类型
|
||||
reply_message = self.action_message if isinstance(self.action_message, DatabaseMessages) else None
|
||||
|
||||
for reply_seg in response_set:
|
||||
# 处理元组格式
|
||||
if isinstance(reply_seg, tuple) and len(reply_seg) >= 2:
|
||||
@@ -129,8 +134,8 @@ class ReplyAction(BaseAction):
|
||||
await send_api.text_to_stream(
|
||||
text=data,
|
||||
stream_id=self.chat_stream.stream_id,
|
||||
reply_to_message=self.action_message,
|
||||
set_reply=should_quote and bool(self.action_message),
|
||||
reply_to_message=reply_message,
|
||||
set_reply=should_quote and bool(reply_message),
|
||||
typing=False,
|
||||
)
|
||||
first_sent = True
|
||||
@@ -145,33 +150,6 @@ class ReplyAction(BaseAction):
|
||||
|
||||
return reply_text
|
||||
|
||||
async def _store_action_info(self, reply_text: str):
|
||||
"""存储动作信息到数据库"""
|
||||
from src.person_info.person_info import get_person_info_manager
|
||||
|
||||
person_info_manager = get_person_info_manager()
|
||||
|
||||
if self.action_message:
|
||||
platform = self.action_message.chat_info.platform
|
||||
user_id = self.action_message.user_info.user_id
|
||||
else:
|
||||
platform = getattr(self.chat_stream, "platform", "unknown")
|
||||
user_id = ""
|
||||
|
||||
person_id = person_info_manager.get_person_id(platform, user_id)
|
||||
person_name = await person_info_manager.get_value(person_id, "person_name")
|
||||
action_prompt_display = f"你对{person_name}进行了回复:{reply_text}"
|
||||
|
||||
await database_api.store_action_info(
|
||||
chat_stream=self.chat_stream,
|
||||
action_build_into_prompt=False,
|
||||
action_prompt_display=action_prompt_display,
|
||||
action_done=True,
|
||||
thinking_id=self.thinking_id,
|
||||
action_data={"reply_text": reply_text},
|
||||
action_name="reply",
|
||||
)
|
||||
|
||||
|
||||
class RespondAction(BaseAction):
|
||||
"""Respond动作 - 对未读消息的统一回应
|
||||
@@ -220,12 +198,15 @@ class RespondAction(BaseAction):
|
||||
action_data = self.action_data.copy()
|
||||
action_data["prompt_mode"] = "normal"
|
||||
|
||||
# 确保 action_message 是 DatabaseMessages 类型,否则使用 None
|
||||
reply_message = self.action_message if isinstance(self.action_message, DatabaseMessages) else None
|
||||
|
||||
# 生成回复
|
||||
success, response_set, _ = await generator_api.generate_reply(
|
||||
chat_stream=self.chat_stream,
|
||||
reply_message=self.action_message,
|
||||
reply_message=reply_message,
|
||||
action_data=action_data,
|
||||
available_actions={self.action_name: None},
|
||||
available_actions={self.action_name: self.get_action_info()},
|
||||
enable_tool=global_config.tool.enable_tool,
|
||||
request_type="chat.replyer",
|
||||
from_plugin=False,
|
||||
@@ -238,9 +219,6 @@ class RespondAction(BaseAction):
|
||||
# 发送回复(respond 默认不引用)
|
||||
reply_text = await self._send_response(response_set)
|
||||
|
||||
# 存储动作信息
|
||||
await self._store_action_info(reply_text)
|
||||
|
||||
logger.info(f"{self.log_prefix} respond 动作执行成功")
|
||||
return True, reply_text
|
||||
|
||||
@@ -288,15 +266,3 @@ class RespondAction(BaseAction):
|
||||
)
|
||||
|
||||
return reply_text
|
||||
|
||||
async def _store_action_info(self, reply_text: str):
|
||||
"""存储动作信息到数据库"""
|
||||
await database_api.store_action_info(
|
||||
chat_stream=self.chat_stream,
|
||||
action_build_into_prompt=False,
|
||||
action_prompt_display=f"统一回应:{reply_text}",
|
||||
action_done=True,
|
||||
thinking_id=self.thinking_id,
|
||||
action_data={"reply_text": reply_text},
|
||||
action_name="respond",
|
||||
)
|
||||
@@ -66,13 +66,6 @@ class ChatterPlanExecutor:
|
||||
action_types = [action.action_type for action in plan.decided_actions]
|
||||
logger.info(f"选择动作: {', '.join(action_types) if action_types else '无'}")
|
||||
|
||||
# 根据配置决定是否启用批量存储模式
|
||||
if global_config.database.batch_action_storage_enabled:
|
||||
self.action_manager.enable_batch_storage(plan.chat_id)
|
||||
logger.debug("已启用批量存储模式")
|
||||
else:
|
||||
logger.debug("批量存储功能已禁用,使用立即存储模式")
|
||||
|
||||
execution_results = []
|
||||
reply_actions = []
|
||||
other_actions = []
|
||||
@@ -109,9 +102,6 @@ class ChatterPlanExecutor:
|
||||
f"规划执行完成: 总数={len(plan.decided_actions)}, 成功={successful_count}, 失败={len(execution_results) - successful_count}"
|
||||
)
|
||||
|
||||
# 批量存储所有待处理的动作
|
||||
await self._flush_action_manager_batch_storage(plan)
|
||||
|
||||
return {
|
||||
"executed_count": len(plan.decided_actions),
|
||||
"successful_count": successful_count,
|
||||
@@ -530,25 +520,3 @@ class ChatterPlanExecutor:
|
||||
}
|
||||
for i, time_val in enumerate(recent_times)
|
||||
]
|
||||
|
||||
async def _flush_action_manager_batch_storage(self, plan: Plan):
|
||||
"""使用 action_manager 的批量存储功能存储所有待处理的动作"""
|
||||
try:
|
||||
# 通过 chat_id 获取真实的 chat_stream 对象
|
||||
from src.plugin_system.apis.chat_api import get_chat_manager
|
||||
|
||||
chat_manager = get_chat_manager()
|
||||
chat_stream = await chat_manager.get_stream(plan.chat_id)
|
||||
|
||||
if chat_stream:
|
||||
# 调用 action_manager 的批量存储
|
||||
await self.action_manager.flush_batch_storage(chat_stream)
|
||||
logger.info("批量存储完成:通过 action_manager 存储所有动作记录")
|
||||
|
||||
# 禁用批量存储模式
|
||||
self.action_manager.disable_batch_storage()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"批量存储动作记录时发生错误: {e}")
|
||||
# 确保在出错时也禁用批量存储模式
|
||||
self.action_manager.disable_batch_storage()
|
||||
|
||||
@@ -86,4 +86,20 @@ class AffinityChatterPlugin(BasePlugin):
|
||||
except Exception as e:
|
||||
logger.error(f"加载 ProactiveThinkingMessageHandler 时出错: {e}")
|
||||
|
||||
try:
|
||||
# 延迟导入 ReplyAction(AFC 专属动作)
|
||||
from .actions.reply import ReplyAction
|
||||
|
||||
components.append((ReplyAction.get_action_info(), ReplyAction))
|
||||
except Exception as e:
|
||||
logger.error(f"加载 ReplyAction 时出错: {e}")
|
||||
|
||||
try:
|
||||
# 延迟导入 RespondAction(AFC 专属动作)
|
||||
from .actions.reply import RespondAction
|
||||
|
||||
components.append((RespondAction.get_action_info(), RespondAction))
|
||||
except Exception as e:
|
||||
logger.error(f"加载 RespondAction 时出错: {e}")
|
||||
|
||||
return components
|
||||
|
||||
@@ -219,8 +219,7 @@ class EmojiAction(BaseAction):
|
||||
)
|
||||
emoji_base64, emoji_description = random.choice(all_emojis_data)
|
||||
|
||||
assert global_config is not None
|
||||
if global_config.emoji.emoji_selection_mode == "description":
|
||||
elif global_config.emoji.emoji_selection_mode == "description":
|
||||
# --- 详细描述选择模式 ---
|
||||
# 获取最近的5条消息内容用于判断
|
||||
recent_messages = await message_api.get_recent_messages(chat_id=self.chat_id, limit=20)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
核心动作插件
|
||||
|
||||
将系统核心动作(reply、no_reply、emoji)转换为新插件系统格式
|
||||
将系统核心动作(emoji)转换为新插件系统格式
|
||||
这是系统的内置插件,提供基础的聊天交互功能
|
||||
|
||||
注意:reply 和 respond 动作已移至 AffinityFlowChatter 插件
|
||||
"""
|
||||
|
||||
# 导入依赖的系统组件
|
||||
@@ -16,7 +18,6 @@ from src.plugin_system.base.config_types import ConfigField
|
||||
|
||||
# 导入API模块 - 标准Python包方式
|
||||
from src.plugins.built_in.core_actions.emoji import EmojiAction
|
||||
from src.plugins.built_in.core_actions.reply import ReplyAction, RespondAction
|
||||
|
||||
logger = get_logger("core_actions")
|
||||
|
||||
@@ -26,11 +27,11 @@ class CoreActionsPlugin(BasePlugin):
|
||||
"""核心动作插件
|
||||
|
||||
系统内置插件,提供基础的聊天交互功能:
|
||||
- Reply: 回复动作
|
||||
- NoReply: 不回复动作
|
||||
- Emoji: 表情动作
|
||||
|
||||
注意:插件基本信息优先从_manifest.json文件中读取
|
||||
注意:
|
||||
- reply 和 respond 动作已移至 AffinityFlowChatter 插件
|
||||
- 插件基本信息优先从_manifest.json文件中读取
|
||||
"""
|
||||
|
||||
# 插件基本信息
|
||||
@@ -53,8 +54,6 @@ class CoreActionsPlugin(BasePlugin):
|
||||
"config_version": ConfigField(type=str, default="0.6.0", description="配置文件版本"),
|
||||
},
|
||||
"components": {
|
||||
"enable_reply": ConfigField(type=bool, default=True, description="是否启用 reply 动作(s4u模板)"),
|
||||
"enable_respond": ConfigField(type=bool, default=True, description="是否启用 respond 动作(normal模板)"),
|
||||
"enable_emoji": ConfigField(type=bool, default=True, description="是否启用发送表情/图片动作"),
|
||||
},
|
||||
}
|
||||
@@ -65,14 +64,6 @@ class CoreActionsPlugin(BasePlugin):
|
||||
# --- 根据配置注册组件 ---
|
||||
components: ClassVar = []
|
||||
|
||||
# 注册 reply 动作
|
||||
if self.get_config("components.enable_reply", True):
|
||||
components.append((ReplyAction.get_action_info(), ReplyAction))
|
||||
|
||||
# 注册 respond 动作
|
||||
if self.get_config("components.enable_respond", True):
|
||||
components.append((RespondAction.get_action_info(), RespondAction))
|
||||
|
||||
# 注册 emoji 动作
|
||||
if self.get_config("components.enable_emoji", True):
|
||||
components.append((EmojiAction.get_action_info(), EmojiAction))
|
||||
|
||||
@@ -33,6 +33,18 @@ from .config import (
|
||||
reload_config,
|
||||
)
|
||||
from .plugin import KokoroFlowChatterV2Plugin
|
||||
from src.plugin_system.base.plugin_metadata import PluginMetadata
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="Kokoro Flow Chatter",
|
||||
description="专为私聊设计的深度情感交互处理器,实现心理状态驱动的对话体验",
|
||||
usage="在私聊场景中自动启用,可通过 [kokoro_flow_chatter].enable 配置开关",
|
||||
version="2.0.0",
|
||||
author="MoFox",
|
||||
keywords=["chatter", "kokoro", "private", "emotional", "narrative"],
|
||||
categories=["Chat", "AI", "Emotional"],
|
||||
extra={"is_built_in": True, "chat_type": "private"},
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Models
|
||||
@@ -61,4 +73,5 @@ __all__ = [
|
||||
"reload_config",
|
||||
# Plugin
|
||||
"KokoroFlowChatterV2Plugin",
|
||||
"__plugin_meta__",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
KFC V2 专属动作模块
|
||||
"""
|
||||
|
||||
from .reply import KFCReplyAction
|
||||
|
||||
__all__ = ["KFCReplyAction"]
|
||||
82
src/plugins/built_in/kokoro_flow_chatter_v2/actions/reply.py
Normal file
82
src/plugins/built_in/kokoro_flow_chatter_v2/actions/reply.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
KFC V2 回复动作模块
|
||||
|
||||
KFC 的 reply 动作与 AFC 不同:
|
||||
- 不调用 LLM 生成回复,content 由 Replyer 提前生成
|
||||
- 动作本身只负责发送 content 参数中的内容
|
||||
"""
|
||||
|
||||
from typing import ClassVar
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system import ActionActivationType, BaseAction, ChatMode
|
||||
from src.plugin_system.apis import send_api
|
||||
|
||||
logger = get_logger("kfc_reply_action")
|
||||
|
||||
|
||||
class KFCReplyAction(BaseAction):
|
||||
"""KFC Reply 动作 - 发送已生成的回复内容
|
||||
|
||||
特点:
|
||||
- 不调用 LLM,直接发送 content 参数中的内容
|
||||
- content 由 Replyer 提前生成
|
||||
- 仅限 KokoroFlowChatterV2 使用
|
||||
"""
|
||||
|
||||
# 动作基本信息
|
||||
action_name = "reply"
|
||||
action_description = "发送回复消息。content 参数包含要发送的内容。"
|
||||
|
||||
# 激活设置
|
||||
activation_type = ActionActivationType.ALWAYS
|
||||
mode_enable = ChatMode.ALL
|
||||
parallel_action = False
|
||||
|
||||
# Chatter 限制:仅允许 KokoroFlowChatterV2 使用
|
||||
chatter_allow: ClassVar[list[str]] = ["KokoroFlowChatterV2"]
|
||||
|
||||
# 动作参数定义
|
||||
action_parameters: ClassVar = {
|
||||
"content": "要发送的回复内容(必需,由 Replyer 生成)",
|
||||
"should_quote_reply": "是否引用原消息(可选,true/false,默认 false)",
|
||||
}
|
||||
|
||||
# 动作使用场景
|
||||
action_require: ClassVar = [
|
||||
"发送回复消息时使用",
|
||||
"content 参数必须包含要发送的内容",
|
||||
]
|
||||
|
||||
# 关联类型
|
||||
associated_types: ClassVar[list[str]] = ["text"]
|
||||
|
||||
async def execute(self) -> tuple[bool, str]:
|
||||
"""执行 reply 动作 - 发送 content 中的内容"""
|
||||
try:
|
||||
# 获取要发送的内容
|
||||
content = self.action_data.get("content", "")
|
||||
if not content:
|
||||
logger.warning(f"{self.log_prefix} content 为空,跳过发送")
|
||||
return True, ""
|
||||
|
||||
# 获取是否引用
|
||||
should_quote = self.action_data.get("should_quote_reply", False)
|
||||
|
||||
# 发送消息
|
||||
await send_api.text_to_stream(
|
||||
text=content,
|
||||
stream_id=self.chat_stream.stream_id,
|
||||
reply_to_message=self.action_message,
|
||||
set_reply=should_quote and bool(self.action_message),
|
||||
typing=False,
|
||||
)
|
||||
|
||||
logger.info(f"{self.log_prefix} KFC reply 动作执行成功")
|
||||
return True, content
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} KFC reply 动作执行失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False, ""
|
||||
@@ -9,7 +9,7 @@ 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 src.plugin_system import register_plugin
|
||||
|
||||
from .chatter import KokoroFlowChatterV2
|
||||
from .config import get_config
|
||||
@@ -84,7 +84,19 @@ class KokoroFlowChatterV2Plugin(BasePlugin):
|
||||
))
|
||||
logger.debug("[KFC V2] 成功加载 KokoroFlowChatterV2 组件")
|
||||
except Exception as e:
|
||||
logger.error(f"[KFC V2] 加载组件失败: {e}")
|
||||
logger.error(f"[KFC V2] 加载 Chatter 组件失败: {e}")
|
||||
|
||||
try:
|
||||
# 注册 KFC 专属 Reply 动作
|
||||
from .actions.reply import KFCReplyAction
|
||||
|
||||
components.append((
|
||||
KFCReplyAction.get_action_info(),
|
||||
KFCReplyAction,
|
||||
))
|
||||
logger.debug("[KFC V2] 成功加载 KFCReplyAction 组件")
|
||||
except Exception as e:
|
||||
logger.error(f"[KFC V2] 加载 Reply 动作失败: {e}")
|
||||
|
||||
return components
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class NapcatAdapter(BaseAdapter):
|
||||
adapter_description = "基于 MoFox-Bus 的 Napcat/OneBot 11 适配器"
|
||||
platform = "qq"
|
||||
|
||||
run_in_subprocess = True
|
||||
run_in_subprocess = False
|
||||
|
||||
def __init__(self, core_sink: CoreSink, plugin: Optional[BasePlugin] = None, **kwargs):
|
||||
"""初始化 Napcat 适配器"""
|
||||
|
||||
Reference in New Issue
Block a user