feat: 添加KFC V2专属动作模块及相关功能,优化回复机制

This commit is contained in:
Windpicker-owo
2025-11-30 15:04:00 +08:00
parent c68bf4ad4f
commit 1750004ffa
13 changed files with 266 additions and 512 deletions

View File

@@ -1,13 +1,10 @@
import asyncio import asyncio
import time
import traceback import traceback
from typing import Any, TYPE_CHECKING from typing import Any, TYPE_CHECKING
from src.chat.message_receive.chat_stream import get_chat_manager from src.chat.message_receive.chat_stream import get_chat_manager
from src.common.data_models.database_data_model import DatabaseMessages from src.common.data_models.database_data_model import DatabaseMessages
from src.common.logger import get_logger 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.base_action import BaseAction
from src.plugin_system.base.component_types import ActionInfo, ComponentType from src.plugin_system.base.component_types import ActionInfo, ComponentType
from src.plugin_system.core.component_registry import component_registry from src.plugin_system.core.component_registry import component_registry
@@ -20,23 +17,19 @@ logger = get_logger("action_manager")
class ChatterActionManager: class ChatterActionManager:
""" """
动作管理器,用于管理各种类型的动作 动作管理器,用于管理和执行动作
现在统一使用新插件系统,简化了原有的新旧兼容逻辑。 职责:
- 加载和管理可用动作集
- 创建动作实例
- 执行动作(所有动作逻辑在 Action.execute() 中实现)
""" """
def __init__(self): def __init__(self):
"""初始化动作管理器""" """初始化动作管理器"""
# 当前正在使用的动作集合,在规划开始时加载
self._using_actions: dict[str, ActionInfo] = {} self._using_actions: dict[str, ActionInfo] = {}
self.chat_id: str | None = None self.chat_id: str | None = None
self.log_prefix: str = "ChatterActionManager" 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): async def load_actions(self, stream_id: str | None):
"""根据 stream_id 加载当前可用的动作""" """根据 stream_id 加载当前可用的动作"""
@@ -44,8 +37,6 @@ class ChatterActionManager:
self._using_actions = component_registry.get_default_actions(stream_id) 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())}") logger.debug(f"已为 stream '{stream_id}' 加载 {len(self._using_actions)} 个可用动作: {list(self._using_actions.keys())}")
# === 执行Action方法 ===
@staticmethod @staticmethod
def create_action( def create_action(
action_name: str, action_name: str,
@@ -70,12 +61,13 @@ class ChatterActionManager:
chat_stream: 聊天流 chat_stream: 聊天流
log_prefix: 日志前缀 log_prefix: 日志前缀
shutting_down: 是否正在关闭 shutting_down: 是否正在关闭
action_message: 目标消息
Returns: Returns:
Optional[BaseAction]: 创建的动作处理器实例如果动作名称未注册则返回None BaseAction | None: 创建的动作处理器实例
""" """
try: try:
# 获取组件类 - 明确指定查询Action类型 # 获取组件类
component_class: type[BaseAction] = component_registry.get_component_class( component_class: type[BaseAction] = component_registry.get_component_class(
action_name, ComponentType.ACTION action_name, ComponentType.ACTION
) # type: ignore ) # type: ignore
@@ -110,8 +102,6 @@ class ChatterActionManager:
except Exception as e: except Exception as e:
logger.error(f"创建Action实例失败 {action_name}: {e}") logger.error(f"创建Action实例失败 {action_name}: {e}")
import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return None return None
@@ -119,17 +109,8 @@ class ChatterActionManager:
"""获取当前正在使用的动作集合""" """获取当前正在使用的动作集合"""
return self._using_actions.copy() return self._using_actions.copy()
# === Modify相关方法 ===
def remove_action_from_using(self, action_name: str) -> bool: def remove_action_from_using(self, action_name: str) -> bool:
""" """从当前使用的动作集中移除指定动作"""
从当前使用的动作集中移除指定动作
Args:
action_name: 动作名称
Returns:
bool: 移除是否成功
"""
if action_name not in self._using_actions: if action_name not in self._using_actions:
logger.warning(f"移除失败: 动作 {action_name} 不在当前使用的动作集中") logger.warning(f"移除失败: 动作 {action_name} 不在当前使用的动作集中")
return False return False
@@ -141,7 +122,6 @@ class ChatterActionManager:
async def restore_actions(self) -> None: async def restore_actions(self) -> None:
"""恢复到当前 stream_id 的默认动作集""" """恢复到当前 stream_id 的默认动作集"""
actions_to_restore = list(self._using_actions.keys()) actions_to_restore = list(self._using_actions.keys())
# 使用 self.chat_id 来恢复当前上下文的动作
await self.load_actions(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())}") 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, clear_unread_messages: bool = True,
) -> Any: ) -> Any:
""" """
执行单个动作的通用函数 执行单个动作
所有动作(包括 reply/respond都通过 BaseAction.execute() 执行 所有动作逻辑都在 BaseAction.execute() 中实现
Args: Args:
action_name: 动作名称 action_name: 动作名称
chat_id: 聊天id chat_id: 聊天ID
target_message: 目标消息 target_message: 目标消息
reasoning: 执行理由 reasoning: 执行理由
action_data: 动作数据 action_data: 动作数据
@@ -172,16 +152,16 @@ class ChatterActionManager:
clear_unread_messages: 是否清除未读消息 clear_unread_messages: 是否清除未读消息
Returns: Returns:
执行结果 执行结果字典
""" """
chat_stream = None chat_stream = None
try: try:
# 通过chat_id获取chat_stream # 获取 chat_stream
chat_manager = get_chat_manager() chat_manager = get_chat_manager()
chat_stream = await chat_manager.get_stream(chat_id) chat_stream = await chat_manager.get_stream(chat_id)
if not chat_stream: 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 { return {
"action_type": action_name, "action_type": action_name,
"success": False, "success": False,
@@ -189,66 +169,75 @@ class ChatterActionManager:
"error": "chat_stream not found", "error": "chat_stream not found",
} }
# 设置正在回复的状态 # 设置正在处理的状态
chat_stream.context.is_replying = True chat_stream.context.is_replying = True
# no_action 特殊处理 # no_action 特殊处理
if action_name == "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( action_handler = self.create_action(
chat_stream, action_name=action_name,
action_name, action_data=action_data or {},
reasoning, reasoning=reasoning,
action_data or {}, cycle_timers={},
{}, # cycle_timers thinking_id=thinking_id or "",
thinking_id, chat_stream=chat_stream,
target_message, 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: if success:
asyncio.create_task(self._record_action_to_message(chat_stream, action_name, target_message, action_data)) asyncio.create_task(self._record_action_to_message(chat_stream, action_name, target_message, action_data))
# 重置打断计数 asyncio.create_task(self._reset_interruption_count(chat_stream.stream_id))
await self._reset_interruption_count_after_action(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 { return {
"action_type": action_name, "action_type": action_name,
"success": success, "success": success,
"reply_text": reply_text, "reply_text": reply_text,
"command": command,
} }
except Exception as e: except Exception as e:
logger.error(f"{log_prefix} 执行动作时出错: {e}") logger.error(f"{log_prefix} 执行动作时出错: {e}")
logger.error(f"{log_prefix} 错误信息: {traceback.format_exc()}") logger.error(traceback.format_exc())
return { return {
"action_type": action_name, "action_type": action_name,
"success": False, "success": False,
"reply_text": "", "reply_text": "",
"loop_info": None,
"error": str(e), "error": str(e),
} }
finally: finally:
# 确保重置正在回复的状态
if chat_stream: if chat_stream:
chat_stream.context.is_replying = False chat_stream.context.is_replying = False
async def _record_action_to_message(self, chat_stream, action_name, target_message, action_data): async def _record_action_to_message(self, chat_stream, action_name: str, target_message, action_data: dict | None):
""" """记录执行的动作到目标消息"""
记录执行的动作到目标消息中
Args:
chat_stream: ChatStream实例
action_name: 动作名称
target_message: 目标消息
action_data: 动作数据
"""
try: try:
from src.chat.message_manager.message_manager import message_manager from src.chat.message_manager.message_manager import message_manager
# 获取目标消息ID
target_message_id = None target_message_id = None
if target_message: if target_message:
target_message_id = target_message.message_id target_message_id = target_message.message_id
@@ -256,362 +245,66 @@ class ChatterActionManager:
target_message_id = action_data.get("target_message_id") target_message_id = action_data.get("target_message_id")
if not target_message_id: if not target_message_id:
logger.debug(f"无法获取目标消息ID动作: {action_name}")
return return
# 通过message_manager更新消息的动作记录并刷新focus_energy
await message_manager.add_action( 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: except Exception as e:
logger.error(f"记录动作到消息失败: {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: try:
from src.plugin_system.apis.chat_api import get_chat_manager
chat_manager = get_chat_manager() chat_manager = get_chat_manager()
chat_stream = await chat_manager.get_stream(stream_id) chat_stream = await chat_manager.get_stream(stream_id)
if chat_stream: if chat_stream and chat_stream.context.interruption_count > 0:
context = chat_stream.context old_count = chat_stream.context.interruption_count
if context.interruption_count > 0: await chat_stream.context.reset_interruption_count()
old_count = context.interruption_count logger.debug(f"重置打断计数: {old_count} -> 0")
# old_afc_adjustment = context.context.get_afc_threshold_adjustment()
await context.reset_interruption_count()
logger.debug(
f"动作执行成功,重置聊天流 {stream_id} 的打断计数: {old_count} -> 0"
)
except Exception as e: except Exception as e:
logger.warning(f"重置打断计数时出错: {e}") logger.warning(f"重置打断计数时出错: {e}")
async def _handle_action( async def _store_action_info(
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(
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]]:
"""
发送并存储回复信息
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信息
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,
)
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:
await database_api.store_action_info(
chat_stream=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",
)
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, self,
action_handler: BaseAction,
action_name: str, action_name: str,
action_data: dict, reply_text: str,
thinking_id: str = "", target_message: DatabaseMessages | None,
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: try:
logger.info(f"开始批量存储 {len(self._pending_actions)} 个动作记录") from src.person_info.person_info import get_person_info_manager
from src.plugin_system.apis import database_api
# 批量存储所有动作 # 构建 action_prompt_display
stored_count = 0 action_prompt_display = ""
for action_data in self._pending_actions: if reply_text:
try: person_info_manager = get_person_info_manager()
result = await database_api.store_action_info( if target_message:
chat_stream=chat_stream, platform = target_message.chat_info.platform
action_name=action_data.get("action_name", ""), user_id = target_message.user_info.user_id
action_data=action_data.get("action_data", {}), person_id = person_info_manager.get_person_id(platform, user_id)
action_done=action_data.get("action_done", True), person_name = await person_info_manager.get_value(person_id, "person_name")
action_build_into_prompt=action_data.get("action_build_into_prompt", False), action_prompt_display = f"你对{person_name}进行了回复:{reply_text}"
action_prompt_display=action_data.get("action_prompt_display", ""), else:
thinking_id=action_data.get("thinking_id", ""), action_prompt_display = f"统一回应:{reply_text}"
)
if result:
stored_count += 1
except Exception as e:
logger.error(f"存储单个动作记录失败: {e}")
logger.info(f"批量存储完成: 成功存储 {stored_count}/{len(self._pending_actions)} 个动作记录") # 存储动作信息
await database_api.store_action_info(
# 清空待处理列表 chat_stream=action_handler.chat_stream,
self._pending_actions.clear() action_build_into_prompt=False,
action_prompt_display=action_prompt_display,
action_done=True,
thinking_id=action_handler.thinking_id,
action_data={"reply_text": reply_text} if reply_text else action_handler.action_data,
action_name=action_name,
)
logger.debug(f"已存储动作信息: {action_name}")
except Exception as e: except Exception as e:
logger.error(f"批量存储动作记录时发生错误: {e}") logger.error(f"存储动作信息失败: {e}")

View File

@@ -187,7 +187,7 @@ class ActionModifier:
logger.debug(f"{self.log_prefix}阶段三移除动作: {action_name},原因: {reason}") 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 = "" removals_summary: str = ""
if all_removals: if all_removals:
removals_summary = " | ".join([f"{name}({reason})" for name, reason in all_removals]) removals_summary = " | ".join([f"{name}({reason})" for name, reason in all_removals])

View File

@@ -0,0 +1,7 @@
"""
AFC 专属动作模块
"""
from .reply import ReplyAction, RespondAction
__all__ = ["ReplyAction", "RespondAction"]

View File

@@ -1,21 +1,23 @@
""" """
回复动作模块 AFC 回复动作模块
定义了种回复相关动作 定义了种回复相关动作
- reply: 针对单条消息的深度回复使用 s4u 模板 - reply: 针对单条消息的深度回复使用 s4u 模板
- respond: 对未读消息的统一回应使用 normal 模板 - respond: 对未读消息的统一回应使用 normal 模板
- no_reply: 选择不回复
这些动作是 AffinityFlowChatter 的专属动作
""" """
import asyncio import asyncio
from typing import ClassVar from typing import ClassVar
from src.common.data_models.database_data_model import DatabaseMessages
from src.common.logger import get_logger from src.common.logger import get_logger
from src.config.config import global_config from src.config.config import global_config
from src.plugin_system import ActionActivationType, BaseAction, ChatMode 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): class ReplyAction(BaseAction):
@@ -63,8 +65,11 @@ class ReplyAction(BaseAction):
async def execute(self) -> tuple[bool, str]: async def execute(self) -> tuple[bool, str]:
"""执行reply动作 - 完整的回复流程""" """执行reply动作 - 完整的回复流程"""
try: 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): if not getattr(global_config.chat, "allow_reply_to_emoji", True):
logger.info(f"{self.log_prefix} 目标消息为表情包且配置不允许回复,跳过") logger.info(f"{self.log_prefix} 目标消息为表情包且配置不允许回复,跳过")
return True, "" return True, ""
@@ -76,9 +81,9 @@ class ReplyAction(BaseAction):
# 生成回复 # 生成回复
success, response_set, _ = await generator_api.generate_reply( success, response_set, _ = await generator_api.generate_reply(
chat_stream=self.chat_stream, chat_stream=self.chat_stream,
reply_message=self.action_message, reply_message=reply_message,
action_data=action_data, 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, enable_tool=global_config.tool.enable_tool,
request_type="chat.replyer", request_type="chat.replyer",
from_plugin=False, from_plugin=False,
@@ -91,9 +96,6 @@ class ReplyAction(BaseAction):
# 发送回复 # 发送回复
reply_text = await self._send_response(response_set) reply_text = await self._send_response(response_set)
# 存储动作信息
await self._store_action_info(reply_text)
logger.info(f"{self.log_prefix} reply 动作执行成功") logger.info(f"{self.log_prefix} reply 动作执行成功")
return True, reply_text return True, reply_text
@@ -112,6 +114,9 @@ class ReplyAction(BaseAction):
should_quote = self.action_data.get("should_quote_reply", False) should_quote = self.action_data.get("should_quote_reply", False)
first_sent = 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: for reply_seg in response_set:
# 处理元组格式 # 处理元组格式
if isinstance(reply_seg, tuple) and len(reply_seg) >= 2: if isinstance(reply_seg, tuple) and len(reply_seg) >= 2:
@@ -129,8 +134,8 @@ class ReplyAction(BaseAction):
await send_api.text_to_stream( await send_api.text_to_stream(
text=data, text=data,
stream_id=self.chat_stream.stream_id, stream_id=self.chat_stream.stream_id,
reply_to_message=self.action_message, reply_to_message=reply_message,
set_reply=should_quote and bool(self.action_message), set_reply=should_quote and bool(reply_message),
typing=False, typing=False,
) )
first_sent = True first_sent = True
@@ -144,33 +149,6 @@ class ReplyAction(BaseAction):
) )
return reply_text 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): class RespondAction(BaseAction):
@@ -220,12 +198,15 @@ class RespondAction(BaseAction):
action_data = self.action_data.copy() action_data = self.action_data.copy()
action_data["prompt_mode"] = "normal" 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( success, response_set, _ = await generator_api.generate_reply(
chat_stream=self.chat_stream, chat_stream=self.chat_stream,
reply_message=self.action_message, reply_message=reply_message,
action_data=action_data, 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, enable_tool=global_config.tool.enable_tool,
request_type="chat.replyer", request_type="chat.replyer",
from_plugin=False, from_plugin=False,
@@ -238,9 +219,6 @@ class RespondAction(BaseAction):
# 发送回复respond 默认不引用) # 发送回复respond 默认不引用)
reply_text = await self._send_response(response_set) reply_text = await self._send_response(response_set)
# 存储动作信息
await self._store_action_info(reply_text)
logger.info(f"{self.log_prefix} respond 动作执行成功") logger.info(f"{self.log_prefix} respond 动作执行成功")
return True, reply_text return True, reply_text
@@ -288,15 +266,3 @@ class RespondAction(BaseAction):
) )
return reply_text 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",
)

View File

@@ -66,13 +66,6 @@ class ChatterPlanExecutor:
action_types = [action.action_type for action in plan.decided_actions] action_types = [action.action_type for action in plan.decided_actions]
logger.info(f"选择动作: {', '.join(action_types) if action_types else ''}") 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 = [] execution_results = []
reply_actions = [] reply_actions = []
other_actions = [] other_actions = []
@@ -109,9 +102,6 @@ class ChatterPlanExecutor:
f"规划执行完成: 总数={len(plan.decided_actions)}, 成功={successful_count}, 失败={len(execution_results) - successful_count}" f"规划执行完成: 总数={len(plan.decided_actions)}, 成功={successful_count}, 失败={len(execution_results) - successful_count}"
) )
# 批量存储所有待处理的动作
await self._flush_action_manager_batch_storage(plan)
return { return {
"executed_count": len(plan.decided_actions), "executed_count": len(plan.decided_actions),
"successful_count": successful_count, "successful_count": successful_count,
@@ -530,25 +520,3 @@ class ChatterPlanExecutor:
} }
for i, time_val in enumerate(recent_times) 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()

View File

@@ -86,4 +86,20 @@ class AffinityChatterPlugin(BasePlugin):
except Exception as e: except Exception as e:
logger.error(f"加载 ProactiveThinkingMessageHandler 时出错: {e}") logger.error(f"加载 ProactiveThinkingMessageHandler 时出错: {e}")
try:
# 延迟导入 ReplyActionAFC 专属动作)
from .actions.reply import ReplyAction
components.append((ReplyAction.get_action_info(), ReplyAction))
except Exception as e:
logger.error(f"加载 ReplyAction 时出错: {e}")
try:
# 延迟导入 RespondActionAFC 专属动作)
from .actions.reply import RespondAction
components.append((RespondAction.get_action_info(), RespondAction))
except Exception as e:
logger.error(f"加载 RespondAction 时出错: {e}")
return components return components

View File

@@ -219,8 +219,7 @@ class EmojiAction(BaseAction):
) )
emoji_base64, emoji_description = random.choice(all_emojis_data) emoji_base64, emoji_description = random.choice(all_emojis_data)
assert global_config is not None elif global_config.emoji.emoji_selection_mode == "description":
if global_config.emoji.emoji_selection_mode == "description":
# --- 详细描述选择模式 --- # --- 详细描述选择模式 ---
# 获取最近的5条消息内容用于判断 # 获取最近的5条消息内容用于判断
recent_messages = await message_api.get_recent_messages(chat_id=self.chat_id, limit=20) recent_messages = await message_api.get_recent_messages(chat_id=self.chat_id, limit=20)

View File

@@ -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包方式 # 导入API模块 - 标准Python包方式
from src.plugins.built_in.core_actions.emoji import EmojiAction 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") logger = get_logger("core_actions")
@@ -26,11 +27,11 @@ class CoreActionsPlugin(BasePlugin):
"""核心动作插件 """核心动作插件
系统内置插件,提供基础的聊天交互功能: 系统内置插件,提供基础的聊天交互功能:
- Reply: 回复动作
- NoReply: 不回复动作
- Emoji: 表情动作 - 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="配置文件版本"), "config_version": ConfigField(type=str, default="0.6.0", description="配置文件版本"),
}, },
"components": { "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="是否启用发送表情/图片动作"), "enable_emoji": ConfigField(type=bool, default=True, description="是否启用发送表情/图片动作"),
}, },
} }
@@ -65,14 +64,6 @@ class CoreActionsPlugin(BasePlugin):
# --- 根据配置注册组件 --- # --- 根据配置注册组件 ---
components: ClassVar = [] 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 动作 # 注册 emoji 动作
if self.get_config("components.enable_emoji", True): if self.get_config("components.enable_emoji", True):
components.append((EmojiAction.get_action_info(), EmojiAction)) components.append((EmojiAction.get_action_info(), EmojiAction))

View File

@@ -33,6 +33,18 @@ from .config import (
reload_config, reload_config,
) )
from .plugin import KokoroFlowChatterV2Plugin 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__ = [ __all__ = [
# Models # Models
@@ -61,4 +73,5 @@ __all__ = [
"reload_config", "reload_config",
# Plugin # Plugin
"KokoroFlowChatterV2Plugin", "KokoroFlowChatterV2Plugin",
"__plugin_meta__",
] ]

View File

@@ -0,0 +1,7 @@
"""
KFC V2 专属动作模块
"""
from .reply import KFCReplyAction
__all__ = ["KFCReplyAction"]

View 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, ""

View File

@@ -9,7 +9,7 @@ from typing import Any, ClassVar
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system.base.base_plugin import BasePlugin from src.plugin_system.base.base_plugin import BasePlugin
from src.plugin_system.base.component_types import ChatterInfo 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 .chatter import KokoroFlowChatterV2
from .config import get_config from .config import get_config
@@ -84,7 +84,19 @@ class KokoroFlowChatterV2Plugin(BasePlugin):
)) ))
logger.debug("[KFC V2] 成功加载 KokoroFlowChatterV2 组件") logger.debug("[KFC V2] 成功加载 KokoroFlowChatterV2 组件")
except Exception as e: 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 return components

View File

@@ -41,7 +41,7 @@ class NapcatAdapter(BaseAdapter):
adapter_description = "基于 MoFox-Bus 的 Napcat/OneBot 11 适配器" adapter_description = "基于 MoFox-Bus 的 Napcat/OneBot 11 适配器"
platform = "qq" platform = "qq"
run_in_subprocess = True run_in_subprocess = False
def __init__(self, core_sink: CoreSink, plugin: Optional[BasePlugin] = None, **kwargs): def __init__(self, core_sink: CoreSink, plugin: Optional[BasePlugin] = None, **kwargs):
"""初始化 Napcat 适配器""" """初始化 Napcat 适配器"""