refc:重构插件api,补全文档,合并expressor和replyer,分离reply和sender,新log浏览器

This commit is contained in:
SengokuCola
2025-06-19 20:20:34 +08:00
parent 7e05ede846
commit ab28b94e33
63 changed files with 5285 additions and 8316 deletions

View File

@@ -1,27 +0,0 @@
# 核心动作插件配置文件
[plugin]
name = "core_actions"
description = "系统核心动作插件"
version = "0.2"
author = "built-in"
enabled = true
[no_reply]
# 等待新消息的超时时间(秒)
waiting_timeout = 1200
[emoji]
# 表情动作配置
enabled = true
# 在Normal模式下的随机激活概率
random_probability = 0.1
# 是否启用智能表情选择
smart_selection = true
# LLM判断相关配置
[emoji.llm_judge]
# 是否启用LLM智能判断
enabled = true
# 自定义判断提示词(可选)
custom_prompt = ""

View File

@@ -5,18 +5,18 @@
这是系统的内置插件,提供基础的聊天交互功能
"""
import re
from typing import List, Tuple, Type, Optional
import time
from typing import List, Tuple, Type
# 导入新插件系统
from src.plugin_system import BasePlugin, register_plugin, BaseAction, ComponentInfo, ActionActivationType, ChatMode
from src.plugin_system.base.base_command import BaseCommand
from src.plugin_system.base.config_types import ConfigField
# 导入依赖的系统组件
from src.common.logger import get_logger
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
from src.chat.focus_chat.hfc_utils import create_empty_anchor_message
# 导入API模块 - 标准Python包方式
from src.plugin_system.apis import emoji_api, generator_api, message_api
logger = get_logger("core_actions")
@@ -35,11 +35,11 @@ class ReplyAction(BaseAction):
# 动作基本信息
action_name = "reply"
action_description = "参与聊天回复,处理文本和表情的发送"
action_description = "参与聊天回复,发送文本进行表达"
# 动作参数定义
action_parameters = {
"reply_to": "如果是明确回复某个人的发言请在reply_to参数中指定格式用户名:发言内容如果不是reply_to的值设为none"
"reply_to": "你要回复的对方的发言内容,格式:(用户名:发言内容),可以为none"
}
# 动作使用场景
@@ -52,40 +52,52 @@ class ReplyAction(BaseAction):
"""执行回复动作"""
logger.info(f"{self.log_prefix} 决定回复: {self.reasoning}")
start_time = self.action_data.get("loop_start_time", time.time())
try:
# 获取聊天观察
chatting_observation = self._get_chatting_observation()
if not chatting_observation:
return False, "未找到聊天观察"
# 处理回复目标
anchor_message = await self._resolve_reply_target(chatting_observation)
# 获取回复器服务
replyer = self.api.get_service("replyer")
if not replyer:
logger.error(f"{self.log_prefix} 未找到回复器服务")
return False, "回复器服务不可用"
# 执行回复
success, reply_set = await replyer.deal_reply(
cycle_timers=self.cycle_timers,
success, reply_set = await generator_api.generate_reply(
chat_stream=self.chat_stream,
action_data=self.action_data,
anchor_message=anchor_message,
reasoning=self.reasoning,
thinking_id=self.thinking_id,
platform=self.platform,
chat_id=self.chat_id,
is_group=self.is_group
)
# 检查从start_time以来的新消息数量
# 获取动作触发时间或使用默认值
current_time = time.time()
new_message_count = message_api.count_new_messages(
chat_id=self.chat_id,
start_time=start_time,
end_time=current_time
)
# 根据新消息数量决定是否使用reply_to
need_reply = new_message_count >= 4
logger.info(f"{self.log_prefix}{start_time}{current_time}共有{new_message_count}条新消息,{'使用' if need_reply else '不使用'}reply_to")
# 构建回复文本
reply_text = self._build_reply_text(reply_set)
reply_text = ""
first_reply = False
for reply_seg in reply_set:
data = reply_seg[1]
if not first_reply and need_reply:
await self.send_text(
content=data,
reply_to=self.action_data.get("reply_to", "")
)
else:
await self.send_text(content=data)
first_reply = True
reply_text += data
# 存储动作记录
await self.api.store_action_info(
await self.store_action_info(
action_build_into_prompt=False,
action_prompt_display=reply_text,
action_done=True,
thinking_id=self.thinking_id,
action_data=self.action_data,
)
# 重置NoReplyAction的连续计数器
@@ -97,47 +109,6 @@ class ReplyAction(BaseAction):
logger.error(f"{self.log_prefix} 回复动作执行失败: {e}")
return False, f"回复失败: {str(e)}"
def _get_chatting_observation(self) -> Optional[ChattingObservation]:
"""获取聊天观察对象"""
observations = self.api.get_service("observations") or []
for obs in observations:
if isinstance(obs, ChattingObservation):
return obs
return None
async def _resolve_reply_target(self, chatting_observation: ChattingObservation):
"""解析回复目标消息"""
reply_to = self.action_data.get("reply_to", "none")
if ":" in reply_to or "" in reply_to:
# 解析回复目标格式:用户名:消息内容
parts = re.split(pattern=r"[:]", string=reply_to, maxsplit=1)
if len(parts) == 2:
target = parts[1].strip()
anchor_message = chatting_observation.search_message_by_text(target)
if anchor_message:
chat_stream = self.api.get_service("chat_stream")
if chat_stream:
anchor_message.update_chat_stream(chat_stream)
return anchor_message
# 创建空锚点消息
logger.info(f"{self.log_prefix} 未找到锚点消息,创建占位符")
chat_stream = self.api.get_service("chat_stream")
if chat_stream:
return await create_empty_anchor_message(chat_stream.platform, chat_stream.group_info, chat_stream)
return None
def _build_reply_text(self, reply_set) -> str:
"""构建回复文本"""
reply_text = ""
if reply_set:
for reply in reply_set:
reply_type = reply[0]
data = reply[1]
if reply_type in ["text", "emoji"]:
reply_text += data
return reply_text
class NoReplyAction(BaseAction):
@@ -178,30 +149,26 @@ class NoReplyAction(BaseAction):
count = NoReplyAction._consecutive_count
# 计算本次等待时间
timeout = self._calculate_waiting_time(count)
if count <= len(self._waiting_stages):
# 前3次使用预设时间
stage_time = self._waiting_stages[count - 1]
# 如果WAITING_TIME_THRESHOLD更小则使用它
timeout = min(stage_time, self.waiting_timeout)
else:
# 第4次及以后使用WAITING_TIME_THRESHOLD
timeout = self.waiting_timeout
logger.info(f"{self.log_prefix} 选择不回复(第{count}次连续),等待新消息中... (超时: {timeout}秒)")
# 等待新消息或达到时间上限
result = await self.api.wait_for_new_message(timeout)
result = await self.wait_for_new_message(timeout)
# 如果有新消息或者超时都不重置计数器因为可能还会继续no_reply
return result
except Exception as e:
logger.error(f"{self.log_prefix} 不回复动作执行失败: {e}")
return False, f"不回复动作执行失败: {e}"
def _calculate_waiting_time(self, consecutive_count: int) -> int:
"""根据连续次数计算等待时间"""
if consecutive_count <= len(self._waiting_stages):
# 前3次使用预设时间
stage_time = self._waiting_stages[consecutive_count - 1]
# 如果WAITING_TIME_THRESHOLD更小则使用它
return min(stage_time, self.waiting_timeout)
else:
# 第4次及以后使用WAITING_TIME_THRESHOLD
return self.waiting_timeout
return False, f"不回复动作执行失败: {e}"
@classmethod
def reset_consecutive_count(cls):
@@ -248,56 +215,33 @@ class EmojiAction(BaseAction):
logger.info(f"{self.log_prefix} 决定发送表情")
try:
# 创建空锚点消息
anchor_message = await self._create_anchor_message()
if not anchor_message:
return False, "无法创建锚点消息"
# 获取回复器服务
replyer = self.api.get_service("replyer")
if not replyer:
logger.error(f"{self.log_prefix} 未找到回复器服务")
return False, "回复器服务不可用"
# 执行表情处理
success, reply_set = await replyer.deal_emoji(
cycle_timers=self.cycle_timers,
action_data=self.action_data,
anchor_message=anchor_message,
thinking_id=self.thinking_id,
)
# 构建回复文本
reply_text = self._build_reply_text(reply_set)
# 1. 根据描述选择表情包
description = self.action_data.get("description", "")
emoji_result = await emoji_api.get_by_description(description)
if not emoji_result:
logger.warning(f"{self.log_prefix} 未找到匹配描述 '{description}' 的表情包")
return False, f"未找到匹配 '{description}' 的表情包"
emoji_base64, emoji_description, matched_emotion = emoji_result
logger.info(f"{self.log_prefix} 找到表情包: {emoji_description}, 匹配情感: {matched_emotion}")
# 使用BaseAction的便捷方法发送表情包
success = await self.send_emoji(emoji_base64)
if not success:
logger.error(f"{self.log_prefix} 表情包发送失败")
return False, "表情包发送失败"
# 重置NoReplyAction的连续计数器
NoReplyAction.reset_consecutive_count()
return success, reply_text
return True, f"发送表情包: {emoji_description}"
except Exception as e:
logger.error(f"{self.log_prefix} 表情动作执行失败: {e}")
return False, f"表情发送失败: {str(e)}"
async def _create_anchor_message(self):
"""创建锚点消息"""
chat_stream = self.api.get_service("chat_stream")
if chat_stream:
logger.info(f"{self.log_prefix} 为表情包创建占位符")
return await create_empty_anchor_message(chat_stream.platform, chat_stream.group_info, chat_stream)
return None
def _build_reply_text(self, reply_set) -> str:
"""构建回复文本"""
reply_text = ""
if reply_set:
for reply in reply_set:
reply_type = reply[0]
data = reply[1]
if reply_type in ["text", "emoji"]:
reply_text += data
return reply_text
class ChangeToFocusChatAction(BaseAction):
"""切换到专注聊天动作 - 从普通模式切换到专注模式"""
@@ -314,6 +258,7 @@ class ChangeToFocusChatAction(BaseAction):
# 动作参数定义
action_parameters = {}
apex = 111
# 动作使用场景
action_require = [
"你想要进入专注聊天模式",
@@ -437,8 +382,6 @@ class CoreActionsPlugin(BasePlugin):
"enable_emoji": ConfigField(type=bool, default=True, description="是否启用'表情'动作"),
"enable_change_to_focus": ConfigField(type=bool, default=True, description="是否启用'切换到专注模式'动作"),
"enable_exit_focus": ConfigField(type=bool, default=True, description="是否启用'退出专注模式'动作"),
"enable_ping_command": ConfigField(type=bool, default=True, description="是否启用'/ping'测试命令"),
"enable_log_command": ConfigField(type=bool, default=True, description="是否启用'/log'日志命令"),
},
"no_reply": {
"waiting_timeout": ConfigField(
@@ -482,73 +425,137 @@ class CoreActionsPlugin(BasePlugin):
components.append((ExitFocusChatAction.get_action_info(), ExitFocusChatAction))
if self.get_config("components.enable_change_to_focus", True):
components.append((ChangeToFocusChatAction.get_action_info(), ChangeToFocusChatAction))
if self.get_config("components.enable_ping_command", True):
components.append(
(PingCommand.get_command_info(name="ping", description="测试机器人响应,拦截后续处理"), PingCommand)
)
if self.get_config("components.enable_log_command", True):
components.append(
(LogCommand.get_command_info(name="log", description="记录消息到日志,不拦截后续处理"), LogCommand)
)
# components.append((DeepReplyAction.get_action_info(), DeepReplyAction))
return components
# ===== 示例Command组件 =====
class PingCommand(BaseCommand):
"""Ping命令 - 测试响应,拦截消息处理"""
command_pattern = r"^/ping(\s+(?P<message>.+))?$"
command_help = "测试机器人响应 - 拦截后续处理"
command_examples = ["/ping", "/ping 测试消息"]
intercept_message = True # 拦截消息,不继续处理
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行ping命令"""
try:
message = self.matched_groups.get("message", "")
reply_text = f"🏓 Pong! {message}" if message else "🏓 Pong!"
await self.send_text(reply_text)
return True, f"发送ping响应: {reply_text}"
except Exception as e:
logger.error(f"Ping命令执行失败: {e}")
return False, f"执行失败: {str(e)}"
class LogCommand(BaseCommand):
"""日志命令 - 记录消息但不拦截后续处理"""
# class DeepReplyAction(BaseAction):
# """回复动作 - 参与聊天回复"""
command_pattern = r"^/log(\s+(?P<level>debug|info|warn|error))?$"
command_help = "记录当前消息到日志 - 不拦截后续处理"
command_examples = ["/log", "/log info", "/log debug"]
intercept_message = False # 不拦截消息,继续后续处理
# # 激活设置
# focus_activation_type = ActionActivationType.ALWAYS
# normal_activation_type = ActionActivationType.NEVER
# mode_enable = ChatMode.FOCUS
# parallel_action = False
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行日志命令"""
try:
level = self.matched_groups.get("level", "info")
user_nickname = self.message.message_info.user_info.user_nickname
content = self.message.processed_plain_text
# # 动作基本信息
# action_name = "deep_reply"
# action_description = "参与聊天回复,关注某个话题,对聊天内容进行深度思考,给出回复"
log_message = f"[{level.upper()}] 用户 {user_nickname}: {content}"
# # 动作参数定义
# action_parameters = {
# "topic": "想要思考的话题"
# }
# 根据级别记录日志
if level == "debug":
logger.debug(log_message)
elif level == "warn":
logger.warning(log_message)
elif level == "error":
logger.error(log_message)
else:
logger.info(log_message)
# # 动作使用场景
# action_require = ["有些问题需要深度思考", "某个问题可能涉及多个方面", "某个问题涉及专业领域或者需要专业知识","这个问题讨论的很激烈,需要深度思考"]
# 不发送回复,让消息继续处理
return True, f"已记录到{level}级别日志"
# # 关联类型
# associated_types = ["text"]
except Exception as e:
logger.error(f"Log命令执行失败: {e}")
return False, f"执行失败: {str(e)}"
# async def execute(self) -> Tuple[bool, str]:
# """执行回复动作"""
# logger.info(f"{self.log_prefix} 决定深度思考")
# try:
# # 获取聊天观察
# chatting_observation = self._get_chatting_observation()
# if not chatting_observation:
# return False, "未找到聊天观察"
# talking_message_str = chatting_observation.talking_message_str
# # 处理回复目标
# chat_stream = self.api.get_service("chat_stream")
# anchor_message = await create_empty_anchor_message(chat_stream.platform, chat_stream.group_info, chat_stream)
# llm_model = self.api.get_available_models().replyer_1
# prompt = f"""
# {talking_message_str}
# 在上面的聊天中,你对{self.action_data.get("topic", "")}感兴趣,形成深刻观点,请你思考,总结成一份学术论文,APA标准格式
# """
# success, response, reasoning, model_name = await self.api.generate_with_model(prompt, llm_model)
# print(prompt)
# print(f"DeepReplyAction: {response}")
# # prompt = f"""
# # {talking_message_str}
# # 在上面的聊天中,你对{self.action_data.get("topic", "")}感兴趣,请你思考
# # """
# extra_info_block = self.action_data.get("extra_info_block", "")
# extra_info_block += response
# # extra_info_block += f"\n--------------------------------\n注意这是最重要的内容你现在可以用比较长的篇幅来表达你的观点不要只回复一个字或者几个字\n由于你进入了深度思考模式所以下方聊天记录的回复要求不再适用请你自由的表达不论字数长短限制\n\n--------------------------------\n注意你现在可以用比较长的篇幅来表达你的观点不要只回复一个字或者几个字\n由于你进入了深度思考模式所以下方聊天记录的回复要求不再适用请你自由的表达不论字数长短限制\n"
# # extra_info_block += f"\n--------------------------------\n注意优先关注这句你现在可以用比较长的篇幅来表达你的观点不要只回复一个字或者几个字\n由于你进入了深度思考模式所以下方聊天记录的回复要求不再适用请你自由的表达不论字数长短限制\n\n--------------------------------\n注意你现在可以用比较长的篇幅来表达你的观点不要只回复一个字或者几个字\n由于你进入了深度思考模式所以其他的回复要求不再适用请你自由的表达不论字数长短限制\n"
# self.action_data["extra_info_block"] = extra_info_block
# # 获取回复器服务
# # replyer = self.api.get_service("replyer")
# # if not replyer:
# # logger.error(f"{self.log_prefix} 未找到回复器服务")
# # return False, "回复器服务不可用"
# # await self.send_message_by_expressor(extra_info_block)
# await self.send_text(extra_info_block)
# # 执行回复
# # success, reply_set = await replyer.deal_reply(
# # cycle_timers=self.cycle_timers,
# # action_data=self.action_data,
# # anchor_message=anchor_message,
# # reasoning=self.reasoning,
# # thinking_id=self.thinking_id,
# # )
# # 构建回复文本
# reply_text = "self._build_reply_text(reply_set)"
# # 存储动作记录
# await self.api.store_action_info(
# action_build_into_prompt=False,
# action_prompt_display=reply_text,
# action_done=True,
# thinking_id=self.thinking_id,
# action_data=self.action_data,
# )
# # 重置NoReplyAction的连续计数器
# NoReplyAction.reset_consecutive_count()
# return success, reply_text
# except Exception as e:
# logger.error(f"{self.log_prefix} 回复动作执行失败: {e}")
# return False, f"回复失败: {str(e)}"
# def _get_chatting_observation(self) -> Optional[ChattingObservation]:
# """获取聊天观察对象"""
# observations = self.api.get_service("observations") or []
# for obs in observations:
# if isinstance(obs, ChattingObservation):
# return obs
# return None
# def _build_reply_text(self, reply_set) -> str:
# """构建回复文本"""
# reply_text = ""
# if reply_set:
# for reply in reply_set:
# data = reply[1]
# reply_text += data
# return reply_text