This commit is contained in:
SengokuCola
2025-06-14 11:41:44 +08:00
5 changed files with 402 additions and 346 deletions

View File

@@ -1,261 +1,262 @@
""" """
Normal Chat Expressor Normal Chat Expressor
为Normal Chat专门设计的表达器不需要经过LLM风格化处理 为Normal Chat专门设计的表达器不需要经过LLM风格化处理
直接发送消息,主要用于插件动作中需要发送消息的场景。 直接发送消息,主要用于插件动作中需要发送消息的场景。
""" """
import time import time
from typing import List, Optional, Tuple, Dict, Any from typing import List, Optional, Tuple, Dict, Any
from src.chat.message_receive.message import MessageRecv, MessageSending, MessageThinking, Seg from src.chat.message_receive.message import MessageRecv, MessageSending, MessageThinking, Seg
from src.chat.message_receive.message import UserInfo from src.chat.message_receive.message import UserInfo
from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager
from src.chat.message_receive.message_sender import message_manager from src.chat.message_receive.message_sender import message_manager
from src.config.config import global_config from src.config.config import global_config
from src.common.logger import get_logger from src.common.logger import get_logger
logger = get_logger("normal_chat_expressor") logger = get_logger("normal_chat_expressor")
class NormalChatExpressor: class NormalChatExpressor:
"""Normal Chat专用表达器 """Normal Chat专用表达器
特点: 特点:
1. 不经过LLM风格化直接发送消息 1. 不经过LLM风格化直接发送消息
2. 支持文本和表情包发送 2. 支持文本和表情包发送
3. 为插件动作提供简化的消息发送接口 3. 为插件动作提供简化的消息发送接口
4. 保持与focus_chat expressor相似的API但去掉复杂的风格化流程 4. 保持与focus_chat expressor相似的API但去掉复杂的风格化流程
""" """
def __init__(self, chat_stream: ChatStream): def __init__(self, chat_stream: ChatStream):
"""初始化Normal Chat表达器 """初始化Normal Chat表达器
Args: Args:
chat_stream: 聊天流对象 chat_stream: 聊天流对象
stream_name: 流名称 stream_name: 流名称
""" """
self.chat_stream = chat_stream self.chat_stream = chat_stream
self.stream_name = get_chat_manager().get_stream_name(self.chat_stream.stream_id) or self.chat_stream.stream_id self.stream_name = get_chat_manager().get_stream_name(self.chat_stream.stream_id) or self.chat_stream.stream_id
self.log_prefix = f"[{self.stream_name}]Normal表达器" self.log_prefix = f"[{self.stream_name}]Normal表达器"
logger.debug(f"{self.log_prefix} 初始化完成") logger.debug(f"{self.log_prefix} 初始化完成")
async def create_thinking_message( async def create_thinking_message(
self, anchor_message: Optional[MessageRecv], thinking_id: str self, anchor_message: Optional[MessageRecv], thinking_id: str
) -> Optional[MessageThinking]: ) -> Optional[MessageThinking]:
"""创建思考消息 """创建思考消息
Args: Args:
anchor_message: 锚点消息 anchor_message: 锚点消息
thinking_id: 思考ID thinking_id: 思考ID
Returns: Returns:
MessageThinking: 创建的思考消息如果失败返回None MessageThinking: 创建的思考消息如果失败返回None
""" """
if not anchor_message or not anchor_message.chat_stream: if not anchor_message or not anchor_message.chat_stream:
logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流") logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流")
return None return None
messageinfo = anchor_message.message_info messageinfo = anchor_message.message_info
thinking_time_point = time.time() thinking_time_point = time.time()
bot_user_info = UserInfo( bot_user_info = UserInfo(
user_id=global_config.bot.qq_account, user_id=global_config.bot.qq_account,
user_nickname=global_config.bot.nickname, user_nickname=global_config.bot.nickname,
platform=messageinfo.platform, platform=messageinfo.platform,
) )
thinking_message = MessageThinking( thinking_message = MessageThinking(
message_id=thinking_id, message_id=thinking_id,
chat_stream=self.chat_stream, chat_stream=self.chat_stream,
bot_user_info=bot_user_info, bot_user_info=bot_user_info,
reply=anchor_message, reply=anchor_message,
thinking_start_time=thinking_time_point, thinking_start_time=thinking_time_point,
) )
await message_manager.add_message(thinking_message) await message_manager.add_message(thinking_message)
logger.debug(f"{self.log_prefix} 创建思考消息: {thinking_id}") logger.debug(f"{self.log_prefix} 创建思考消息: {thinking_id}")
return thinking_message return thinking_message
async def send_response_messages( async def send_response_messages(
self, self,
anchor_message: Optional[MessageRecv], anchor_message: Optional[MessageRecv],
response_set: List[Tuple[str, str]], response_set: List[Tuple[str, str]],
thinking_id: str = "", thinking_id: str = "",
display_message: str = "", display_message: str = "",
) -> Optional[MessageSending]: ) -> Optional[MessageSending]:
"""发送回复消息 """发送回复消息
Args: Args:
anchor_message: 锚点消息 anchor_message: 锚点消息
response_set: 回复内容集合,格式为 [(type, content), ...] response_set: 回复内容集合,格式为 [(type, content), ...]
thinking_id: 思考ID thinking_id: 思考ID
display_message: 显示消息 display_message: 显示消息
Returns: Returns:
MessageSending: 发送的第一条消息如果失败返回None MessageSending: 发送的第一条消息如果失败返回None
""" """
try: try:
if not response_set: if not response_set:
logger.warning(f"{self.log_prefix} 回复内容为空") logger.warning(f"{self.log_prefix} 回复内容为空")
return None return None
# 如果没有thinking_id生成一个 # 如果没有thinking_id生成一个
if not thinking_id: if not thinking_id:
thinking_time_point = round(time.time(), 2) thinking_time_point = round(time.time(), 2)
thinking_id = "mt" + str(thinking_time_point) thinking_id = "mt" + str(thinking_time_point)
# 创建思考消息 # 创建思考消息
if anchor_message: if anchor_message:
await self.create_thinking_message(anchor_message, thinking_id) await self.create_thinking_message(anchor_message, thinking_id)
# 创建消息集 # 创建消息集
first_bot_msg = None mark_head = False
mark_head = False is_emoji = False
is_emoji = False if len(response_set) == 0:
if len(response_set) == 0: return None
return None message_id = f"{thinking_id}_{len(response_set)}"
message_id = f"{thinking_id}_{len(response_set)}" response_type, content = response_set[0]
response_type, content = response_set[0] if len(response_set) > 1:
if len(response_set) > 1: message_segment = Seg(type="seglist", data=[Seg(type=t, data=c) for t, c in response_set])
message_segment = Seg(type="seglist", data=[Seg(type=t, data=c) for t, c in response_set]) else:
else: message_segment = Seg(type=response_type, data=content)
message_segment = Seg(type=response_type, data=content) if response_type == "emoji":
if response_type == "emoji": is_emoji = True
is_emoji = True
bot_msg = await self._build_sending_message(
bot_msg = await self._build_sending_message( message_id=message_id,
message_id=message_id, message_segment=message_segment,
message_segment=message_segment, thinking_id=thinking_id,
thinking_id=thinking_id, anchor_message=anchor_message,
anchor_message=anchor_message, thinking_start_time=time.time(),
thinking_start_time=time.time(), reply_to=mark_head,
reply_to=mark_head, is_emoji=is_emoji,
is_emoji=is_emoji, display_message=display_message,
display_message=display_message, )
) logger.debug(f"{self.log_prefix} 添加{response_type}类型消息: {content}")
logger.debug(f"{self.log_prefix} 添加{response_type}类型消息: {content}")
# 提交消息集
# 提交消息集 if bot_msg:
if bot_msg: await message_manager.add_message(bot_msg)
await message_manager.add_message(bot_msg) logger.info(
logger.info(f"{self.log_prefix} 成功发送 {response_type}类型消息: {content}") f"{self.log_prefix} 成功发送 {response_type}类型消息: {str(content)[:200] + '...' if len(str(content)) > 200 else content}"
container = await message_manager.get_container(self.chat_stream.stream_id) # 使用 self.stream_id )
for msg in container.messages[:]: container = await message_manager.get_container(self.chat_stream.stream_id) # 使用 self.stream_id
if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id: for msg in container.messages[:]:
container.messages.remove(msg) if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id:
logger.debug(f"[{self.stream_name}] 已移除未产生回复的思考消息 {thinking_id}") container.messages.remove(msg)
break logger.debug(f"[{self.stream_name}] 已移除未产生回复的思考消息 {thinking_id}")
return first_bot_msg break
else: return bot_msg
logger.warning(f"{self.log_prefix} 没有有效的消息被创建") else:
return None logger.warning(f"{self.log_prefix} 没有有效的消息被创建")
return None
except Exception as e:
logger.error(f"{self.log_prefix} 发送消息失败: {e}") except Exception as e:
import traceback logger.error(f"{self.log_prefix} 发送消息失败: {e}")
import traceback
traceback.print_exc()
return None traceback.print_exc()
return None
async def _build_sending_message(
self, async def _build_sending_message(
message_id: str, self,
message_segment: Seg, message_id: str,
thinking_id: str, message_segment: Seg,
anchor_message: Optional[MessageRecv], thinking_id: str,
thinking_start_time: float, anchor_message: Optional[MessageRecv],
reply_to: bool = False, thinking_start_time: float,
is_emoji: bool = False, reply_to: bool = False,
display_message: str = "", is_emoji: bool = False,
) -> MessageSending: display_message: str = "",
"""构建发送消息 ) -> MessageSending:
"""构建发送消息
Args:
message_id: 消息ID Args:
message_segment: 消息 message_id: 消息ID
thinking_id: 思考ID message_segment: 消息段
anchor_message: 锚点消息 thinking_id: 思考ID
thinking_start_time: 思考开始时间 anchor_message: 锚点消息
reply_to: 是否回复 thinking_start_time: 思考开始时间
is_emoji: 是否为表情包 reply_to: 是否回复
is_emoji: 是否为表情包
Returns:
MessageSending: 构建的发送消息 Returns:
""" MessageSending: 构建的发送消息
bot_user_info = UserInfo( """
user_id=global_config.bot.qq_account, bot_user_info = UserInfo(
user_nickname=global_config.bot.nickname, user_id=global_config.bot.qq_account,
platform=anchor_message.message_info.platform if anchor_message else "unknown", user_nickname=global_config.bot.nickname,
) platform=anchor_message.message_info.platform if anchor_message else "unknown",
)
message_sending = MessageSending(
message_id=message_id, message_sending = MessageSending(
chat_stream=self.chat_stream, message_id=message_id,
bot_user_info=bot_user_info, chat_stream=self.chat_stream,
message_segment=message_segment, bot_user_info=bot_user_info,
sender_info=self.chat_stream.user_info, message_segment=message_segment,
reply=anchor_message if reply_to else None, sender_info=self.chat_stream.user_info,
thinking_start_time=thinking_start_time, reply=anchor_message if reply_to else None,
is_emoji=is_emoji, thinking_start_time=thinking_start_time,
display_message=display_message, is_emoji=is_emoji,
) display_message=display_message,
)
return message_sending
return message_sending
async def deal_reply(
self, async def deal_reply(
cycle_timers: dict, self,
action_data: Dict[str, Any], cycle_timers: dict,
reasoning: str, action_data: Dict[str, Any],
anchor_message: MessageRecv, reasoning: str,
thinking_id: str, anchor_message: MessageRecv,
) -> Tuple[bool, Optional[str]]: thinking_id: str,
"""处理回复动作 - 兼容focus_chat expressor API ) -> Tuple[bool, Optional[str]]:
"""处理回复动作 - 兼容focus_chat expressor API
Args:
cycle_timers: 周期计时器normal_chat中不使用 Args:
action_data: 动作数据包含text、target、emojis等 cycle_timers: 周期计时器normal_chat中不使用
reasoning: 推理说明 action_data: 动作数据包含text、target、emojis等
anchor_message: 锚点消息 reasoning: 推理说明
thinking_id: 思考ID anchor_message: 锚点消息
thinking_id: 思考ID
Returns:
Tuple[bool, Optional[str]]: (是否成功, 回复文本) Returns:
""" Tuple[bool, Optional[str]]: (是否成功, 回复文本)
try: """
response_set = [] try:
response_set = []
# 处理文本内容
text_content = action_data.get("text", "") # 处理文本内容
if text_content: text_content = action_data.get("text", "")
response_set.append(("text", text_content)) if text_content:
response_set.append(("text", text_content))
# 处理表情包
emoji_content = action_data.get("emojis", "") # 处理表情包
if emoji_content: emoji_content = action_data.get("emojis", "")
response_set.append(("emoji", emoji_content)) if emoji_content:
response_set.append(("emoji", emoji_content))
if not response_set:
logger.warning(f"{self.log_prefix} deal_reply: 没有有效的回复内容") if not response_set:
return False, None logger.warning(f"{self.log_prefix} deal_reply: 没有有效的回复内容")
return False, None
# 发送消息
result = await self.send_response_messages( # 发送消息
anchor_message=anchor_message, result = await self.send_response_messages(
response_set=response_set, anchor_message=anchor_message,
thinking_id=thinking_id, response_set=response_set,
) thinking_id=thinking_id,
)
if result:
return True, text_content if text_content else "发送成功" if result:
else: return True, text_content if text_content else "发送成功"
return False, None else:
return False, None
except Exception as e:
logger.error(f"{self.log_prefix} deal_reply执行失败: {e}") except Exception as e:
import traceback logger.error(f"{self.log_prefix} deal_reply执行失败: {e}")
import traceback
traceback.print_exc()
return False, None traceback.print_exc()
return False, None

View File

@@ -1 +0,0 @@
from . import tts_action # noqa

View File

@@ -0,0 +1,16 @@
# 文字转语音插件配置文件
[plugin]
name = "tts_plugin"
version = "0.1.0"
enabled = true
description = "文字转语音插件"
# 组件启用控制
[components]
enable_tarots = true
# 日志配置
[logging]
level = "INFO"
prefix = "[TTS]"

View File

@@ -1,84 +1,124 @@
from src.common.logger import get_logger from src.plugin_system.base.base_plugin import BasePlugin, register_plugin
from src.plugin_system.base.base_action import ActionActivationType from src.plugin_system.base.component_types import ComponentInfo
from src.plugin_system.base.base_action import BaseAction as PluginAction, register_action from src.common.logger import get_logger
from typing import Tuple from src.plugin_system.base.base_action import BaseAction, ActionActivationType, ChatMode
from typing import Tuple, List, Type
logger = get_logger("tts_action")
logger = get_logger("tts")
@register_action
class TTSAction(PluginAction): class TTSAction(BaseAction):
"""TTS语音转换动作处理类""" """TTS语音转换动作处理类"""
action_name = "tts_action" action_name = "tts"
action_description = "将文本转换为语音进行播放,适用于需要语音输出的场景" action_description = "将文本转换为语音进行播放,适用于需要语音输出的场景"
action_parameters = { action_parameters = {
"text": "需要转换为语音的文本内容,必填,内容应当适合语音播报,语句流畅、清晰", "text": "需要转换为语音的文本内容,必填,内容应当适合语音播报,语句流畅、清晰",
} }
action_require = [ action_require = [
"当需要发送语音信息时使用", "当需要发送语音信息时使用",
"当用户明确要求使用语音功能时使用", "当用户明确要求使用语音功能时使用",
"当表达内容更适合用语音而不是文字传达时使用", "当表达内容更适合用语音而不是文字传达时使用",
"当用户想听到语音回答而非阅读文本时使用", "当用户想听到语音回答而非阅读文本时使用",
] ]
enable_plugin = True # 启用插件 enable_plugin = True # 启用插件
associated_types = ["tts_text"] associated_types = ["tts_text"]
focus_activation_type = ActionActivationType.LLM_JUDGE # 模式和并行控制
normal_activation_type = ActionActivationType.KEYWORD mode_enable = ChatMode.ALL
parallel_action = False
# 关键词配置 - Normal模式下使用关键词触发
activation_keywords = ["语音", "tts", "播报", "读出来", "语音播放", "", "朗读"] focus_activation_type = ActionActivationType.LLM_JUDGE
keyword_case_sensitive = False normal_activation_type = ActionActivationType.KEYWORD
# 并行执行设置 - TTS可以与回复并行执行不覆盖回复内容 # 关键词配置 - Normal模式下使用关键词触发
parallel_action = False activation_keywords = ["语音", "tts", "播报", "读出来", "语音播放", "", "朗读"]
keyword_case_sensitive = False
async def process(self) -> Tuple[bool, str]:
"""处理TTS文本转语音动作""" # 并行执行设置 - TTS可以与回复并行执行不覆盖回复内容
logger.info(f"{self.log_prefix} 执行TTS动作: {self.reasoning}") parallel_action = False
# 获取要转换的文本 async def execute(self) -> Tuple[bool, str]:
text = self.action_data.get("text") """处理TTS文本转语音动作"""
logger.info(f"{self.log_prefix} 执行TTS动作: {self.reasoning}")
if not text:
logger.error(f"{self.log_prefix} 执行TTS动作时未提供文本内容") # 获取要转换的文本
return False, "执行TTS动作失败未提供文本内容" text = self.action_data.get("text")
# 确保文本适合TTS使用 if not text:
processed_text = self._process_text_for_tts(text) logger.error(f"{self.log_prefix} 执行TTS动作时未提供文本内容")
return False, "执行TTS动作失败未提供文本内容"
try:
# 发送TTS消息 # 确保文本适合TTS使用
await self.send_message(type="tts_text", data=processed_text) processed_text = self._process_text_for_tts(text)
logger.info(f"{self.log_prefix} TTS动作执行成功文本长度: {len(processed_text)}") try:
return True, "TTS动作执行成功" # 发送TTS消息
await self.send_type(type="tts_text", text=processed_text)
except Exception as e:
logger.error(f"{self.log_prefix} 执行TTS动作时出错: {e}") logger.info(f"{self.log_prefix} TTS动作执行成功,文本长度: {len(processed_text)}")
return False, f"执行TTS动作时出错: {e}" return True, "TTS动作执行成功"
def _process_text_for_tts(self, text: str) -> str: except Exception as e:
""" logger.error(f"{self.log_prefix} 执行TTS动作时出错: {e}")
处理文本使其更适合TTS使用 return False, f"执行TTS动作时出错: {e}"
- 移除不必要的特殊字符和表情符号
- 修正标点符号以提高语音质量 def _process_text_for_tts(self, text: str) -> str:
- 优化文本结构使语音更流畅 """
""" 处理文本使其更适合TTS使用
# 这里可以添加文本处理逻辑 - 移除不必要的特殊字符和表情符号
# 例如:移除多余的标点、表情符号,优化语句结构等 - 修正标点符号以提高语音质量
- 优化文本结构使语音更流畅
# 简单示例实现 """
processed_text = text # 这里可以添加文本处理逻辑
# 例如:移除多余的标点、表情符号,优化语句结构等
# 移除多余的标点符号
import re # 简单示例实现
processed_text = text
processed_text = re.sub(r"([!?,.;:。!?,、;:])\1+", r"\1", processed_text)
# 移除多余的标点符号
# 确保句子结尾有合适的标点 import re
if not any(processed_text.endswith(end) for end in [".", "?", "!", "", "", ""]):
processed_text = processed_text + "" processed_text = re.sub(r"([!?,.;:。!?,、;:])\1+", r"\1", processed_text)
return processed_text # 确保句子结尾有合适的标点
if not any(processed_text.endswith(end) for end in [".", "?", "!", "", "", ""]):
processed_text = processed_text + ""
return processed_text
@register_plugin
class TTSPlugin(BasePlugin):
"""TTS插件
- 这是文字转语音插件
- Normal模式下依靠关键词触发
- Focus模式下由LLM判断触发
- 具有一定的文本预处理能力
"""
# 插件基本信息
plugin_name = "tts_plugin"
plugin_description = "文字转语音插件"
plugin_version = "0.1.0"
plugin_author = "MaiBot开发团队"
enable_plugin = True
config_file_name = "config.toml"
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的组件列表"""
# 从配置获取组件启用状态
enable_tts = self.get_config("components.enable_tts", True)
components = []
# 添加Action组件
if enable_tts:
components.append(
(
TTSAction.get_action_info(name="tarots_action", description="文字转语音插件"),
TTSAction,
)
)
return components