- Updated imports and references from ChatStream to BotChatSession across multiple files. - Adjusted method signatures and internal logic to accommodate the new session management. - Ensured compatibility with existing functionality while improving code clarity and maintainability.
458 lines
16 KiB
Python
458 lines
16 KiB
Python
"""
|
||
发送API模块
|
||
|
||
专门负责发送各种类型的消息,采用标准Python包设计模式
|
||
|
||
使用方式:
|
||
from src.plugin_system.apis import send_api
|
||
|
||
# 方式1:直接使用stream_id(推荐)
|
||
await send_api.text_to_stream("hello", stream_id)
|
||
await send_api.emoji_to_stream(emoji_base64, stream_id)
|
||
await send_api.custom_to_stream("video", video_data, stream_id)
|
||
|
||
# 方式2:使用群聊/私聊指定函数
|
||
await send_api.text_to_group("hello", "123456")
|
||
await send_api.text_to_user("hello", "987654")
|
||
|
||
# 方式3:使用通用custom_message函数
|
||
await send_api.custom_message("video", video_data, "123456", True)
|
||
"""
|
||
|
||
import traceback
|
||
import time
|
||
from typing import Optional, Union, Dict, List, TYPE_CHECKING, Tuple
|
||
|
||
from src.common.logger import get_logger
|
||
from src.common.data_models.message_data_model import ReplyContentType
|
||
from src.config.config import global_config
|
||
from src.chat.message_receive.chat_manager import chat_manager as _chat_manager
|
||
from src.chat.message_receive.uni_message_sender import UniversalMessageSender
|
||
from maim_message import Seg
|
||
|
||
from src.common.data_models.mai_message_data_model import MaiMessage, UserInfo
|
||
from src.chat.message_receive.message import MessageSending
|
||
|
||
if TYPE_CHECKING:
|
||
from src.common.data_models.database_data_model import DatabaseMessages
|
||
from src.common.data_models.message_data_model import ReplySetModel, ReplyContent, ForwardNode
|
||
|
||
logger = get_logger("send_api")
|
||
|
||
|
||
# =============================================================================
|
||
# 内部实现函数(不暴露给外部)
|
||
# =============================================================================
|
||
|
||
|
||
async def _send_to_target(
|
||
message_segment: Seg,
|
||
stream_id: str,
|
||
display_message: str = "",
|
||
typing: bool = False,
|
||
set_reply: bool = False,
|
||
reply_message: Optional["DatabaseMessages"] = None,
|
||
storage_message: bool = True,
|
||
show_log: bool = True,
|
||
selected_expressions: Optional[List[int]] = None,
|
||
) -> bool:
|
||
"""向指定目标发送消息的内部实现
|
||
|
||
Args:
|
||
message_segment:
|
||
stream_id: 目标流ID
|
||
display_message: 显示消息
|
||
typing: 是否模拟打字等待。
|
||
reply_to: 回复消息,格式为"发送者:消息内容"
|
||
storage_message: 是否存储消息到数据库
|
||
show_log: 发送是否显示日志
|
||
|
||
Returns:
|
||
bool: 是否发送成功
|
||
"""
|
||
try:
|
||
if set_reply and not reply_message:
|
||
logger.warning("[SendAPI] 使用引用回复,但未提供回复消息")
|
||
return False
|
||
|
||
if show_log:
|
||
logger.debug(f"[SendAPI] 发送{message_segment.type}消息到 {stream_id}")
|
||
|
||
# 查找目标聊天流
|
||
target_stream = _chat_manager.get_session_by_session_id(stream_id)
|
||
if not target_stream:
|
||
logger.error(f"[SendAPI] 未找到聊天流: {stream_id}")
|
||
return False
|
||
|
||
# 创建发送器
|
||
message_sender = UniversalMessageSender()
|
||
|
||
# 生成消息ID
|
||
current_time = time.time()
|
||
message_id = f"send_api_{int(current_time * 1000)}"
|
||
|
||
# 构建机器人用户信息
|
||
bot_user_info = UserInfo(
|
||
user_id=global_config.bot.qq_account,
|
||
user_nickname=global_config.bot.nickname,
|
||
)
|
||
|
||
reply_to_platform_id = ""
|
||
anchor_message: Optional[MaiMessage] = None
|
||
if reply_message:
|
||
anchor_message = db_message_to_mai_message(reply_message)
|
||
if anchor_message:
|
||
logger.debug(f"[SendAPI] 找到匹配的回复消息,发送者: {anchor_message.message_info.user_info.user_id}")
|
||
reply_to_platform_id = (
|
||
f"{anchor_message.platform}:{anchor_message.message_info.user_info.user_id}"
|
||
)
|
||
|
||
# 构建 sender_info(私聊时为接收者信息)
|
||
sender_info = None
|
||
if target_stream.context and target_stream.context.message:
|
||
sender_info = target_stream.context.message.message_info.user_info
|
||
|
||
# 构建发送消息对象
|
||
bot_message = MessageSending(
|
||
message_id=message_id,
|
||
session=target_stream,
|
||
bot_user_info=bot_user_info,
|
||
sender_info=sender_info,
|
||
message_segment=message_segment,
|
||
display_message=display_message,
|
||
reply=anchor_message,
|
||
is_head=True,
|
||
is_emoji=(message_segment.type == "emoji"),
|
||
thinking_start_time=current_time,
|
||
reply_to=reply_to_platform_id,
|
||
selected_expressions=selected_expressions,
|
||
)
|
||
|
||
# 发送消息
|
||
sent_msg = await message_sender.send_message(
|
||
bot_message,
|
||
typing=typing,
|
||
set_reply=set_reply,
|
||
storage_message=storage_message,
|
||
show_log=show_log,
|
||
)
|
||
|
||
if sent_msg:
|
||
logger.debug(f"[SendAPI] 成功发送消息到 {stream_id}")
|
||
return True
|
||
else:
|
||
logger.error("[SendAPI] 发送消息失败")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"[SendAPI] 发送消息时出错: {e}")
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
|
||
def db_message_to_mai_message(message_obj: "DatabaseMessages") -> Optional[MaiMessage]:
|
||
"""将数据库消息重建为 MaiMessage 对象,用于回复引用。
|
||
|
||
Args:
|
||
message_obj: 插件系统的 DatabaseMessages 数据对象
|
||
|
||
Returns:
|
||
Optional[MaiMessage]: 构建的消息对象,如果信息不足则返回 None
|
||
"""
|
||
from datetime import datetime
|
||
from src.common.data_models.mai_message_data_model import GroupInfo, MessageInfo
|
||
from src.common.data_models.message_component_data_model import MessageSequence
|
||
|
||
user_info = UserInfo(
|
||
user_id=message_obj.user_info.user_id or "",
|
||
user_nickname=message_obj.user_info.user_nickname or "",
|
||
user_cardname=message_obj.user_info.user_cardname,
|
||
)
|
||
|
||
group_info = None
|
||
if message_obj.chat_info.group_info:
|
||
group_info = GroupInfo(
|
||
group_id=message_obj.chat_info.group_info.group_id or "",
|
||
group_name=message_obj.chat_info.group_info.group_name or "",
|
||
)
|
||
|
||
msg = MaiMessage(
|
||
message_id=message_obj.message_id,
|
||
timestamp=datetime.fromtimestamp(message_obj.time) if message_obj.time else datetime.now(),
|
||
)
|
||
msg.message_info = MessageInfo(user_info=user_info, group_info=group_info)
|
||
msg.platform = message_obj.chat_info.platform or ""
|
||
msg.session_id = message_obj.chat_info.stream_id or ""
|
||
msg.processed_plain_text = message_obj.processed_plain_text
|
||
msg.raw_message = MessageSequence(components=[])
|
||
msg.initialized = True
|
||
return msg
|
||
|
||
|
||
# =============================================================================
|
||
# 公共API函数 - 预定义类型的发送函数
|
||
# =============================================================================
|
||
|
||
|
||
async def text_to_stream(
|
||
text: str,
|
||
stream_id: str,
|
||
typing: bool = False,
|
||
set_reply: bool = False,
|
||
reply_message: Optional["DatabaseMessages"] = None,
|
||
storage_message: bool = True,
|
||
selected_expressions: Optional[List[int]] = None,
|
||
) -> bool:
|
||
"""向指定流发送文本消息
|
||
|
||
Args:
|
||
text: 要发送的文本内容
|
||
stream_id: 聊天流ID
|
||
typing: 是否显示正在输入
|
||
reply_to: 回复消息,格式为"发送者:消息内容"
|
||
storage_message: 是否存储消息到数据库
|
||
|
||
Returns:
|
||
bool: 是否发送成功
|
||
"""
|
||
return await _send_to_target(
|
||
message_segment=Seg(type="text", data=text),
|
||
stream_id=stream_id,
|
||
display_message="",
|
||
typing=typing,
|
||
set_reply=set_reply,
|
||
reply_message=reply_message,
|
||
storage_message=storage_message,
|
||
selected_expressions=selected_expressions,
|
||
)
|
||
|
||
|
||
async def emoji_to_stream(
|
||
emoji_base64: str,
|
||
stream_id: str,
|
||
storage_message: bool = True,
|
||
set_reply: bool = False,
|
||
reply_message: Optional["DatabaseMessages"] = None,
|
||
) -> bool:
|
||
"""向指定流发送表情包
|
||
|
||
Args:
|
||
emoji_base64: 表情包的base64编码
|
||
stream_id: 聊天流ID
|
||
storage_message: 是否存储消息到数据库
|
||
|
||
Returns:
|
||
bool: 是否发送成功
|
||
"""
|
||
return await _send_to_target(
|
||
message_segment=Seg(type="emoji", data=emoji_base64),
|
||
stream_id=stream_id,
|
||
display_message="",
|
||
typing=False,
|
||
storage_message=storage_message,
|
||
set_reply=set_reply,
|
||
reply_message=reply_message,
|
||
)
|
||
|
||
|
||
async def image_to_stream(
|
||
image_base64: str,
|
||
stream_id: str,
|
||
storage_message: bool = True,
|
||
set_reply: bool = False,
|
||
reply_message: Optional["DatabaseMessages"] = None,
|
||
) -> bool:
|
||
"""向指定流发送图片
|
||
|
||
Args:
|
||
image_base64: 图片的base64编码
|
||
stream_id: 聊天流ID
|
||
storage_message: 是否存储消息到数据库
|
||
|
||
Returns:
|
||
bool: 是否发送成功
|
||
"""
|
||
return await _send_to_target(
|
||
message_segment=Seg(type="image", data=image_base64),
|
||
stream_id=stream_id,
|
||
display_message="",
|
||
typing=False,
|
||
storage_message=storage_message,
|
||
set_reply=set_reply,
|
||
reply_message=reply_message,
|
||
)
|
||
|
||
|
||
async def command_to_stream(
|
||
command: Union[str, dict],
|
||
stream_id: str,
|
||
storage_message: bool = True,
|
||
display_message: str = "",
|
||
) -> bool:
|
||
"""向指定流发送命令
|
||
|
||
Args:
|
||
command: 命令
|
||
stream_id: 聊天流ID
|
||
storage_message: 是否存储消息到数据库
|
||
display_message: 显示消息
|
||
|
||
Returns:
|
||
bool: 是否发送成功
|
||
"""
|
||
return await _send_to_target(
|
||
message_segment=Seg(type="command", data=command), # type: ignore
|
||
stream_id=stream_id,
|
||
display_message=display_message,
|
||
typing=False,
|
||
storage_message=storage_message,
|
||
set_reply=False,
|
||
)
|
||
|
||
|
||
async def custom_to_stream(
|
||
message_type: str,
|
||
content: str | Dict,
|
||
stream_id: str,
|
||
display_message: str = "",
|
||
typing: bool = False,
|
||
reply_message: Optional["DatabaseMessages"] = None,
|
||
set_reply: bool = False,
|
||
storage_message: bool = True,
|
||
show_log: bool = True,
|
||
) -> bool:
|
||
"""向指定流发送自定义类型消息
|
||
|
||
Args:
|
||
message_type: 消息类型,如"text"、"image"、"emoji"、"video"、"file"等
|
||
content: 消息内容(通常是base64编码或文本)
|
||
stream_id: 聊天流ID
|
||
display_message: 显示消息
|
||
typing: 是否显示正在输入
|
||
reply_to: 回复消息,格式为"发送者:消息内容"
|
||
storage_message: 是否存储消息到数据库
|
||
show_log: 是否显示日志
|
||
Returns:
|
||
bool: 是否发送成功
|
||
"""
|
||
return await _send_to_target(
|
||
message_segment=Seg(type=message_type, data=content), # type: ignore
|
||
stream_id=stream_id,
|
||
display_message=display_message,
|
||
typing=typing,
|
||
reply_message=reply_message,
|
||
set_reply=set_reply,
|
||
storage_message=storage_message,
|
||
show_log=show_log,
|
||
)
|
||
|
||
|
||
async def custom_reply_set_to_stream(
|
||
reply_set: "ReplySetModel",
|
||
stream_id: str,
|
||
display_message: str = "", # 基本没用
|
||
typing: bool = False,
|
||
reply_message: Optional["DatabaseMessages"] = None,
|
||
set_reply: bool = False,
|
||
storage_message: bool = True,
|
||
show_log: bool = True,
|
||
) -> bool:
|
||
"""
|
||
向指定流发送混合型消息集
|
||
|
||
Args:
|
||
reply_set: ReplySetModel 对象,包含多个 ReplyContent
|
||
stream_id: 聊天流ID
|
||
display_message: 显示消息
|
||
typing: 是否显示正在输入
|
||
reply_to: 回复消息,格式为"发送者:消息内容"
|
||
storage_message: 是否存储消息到数据库
|
||
show_log: 是否显示日志
|
||
"""
|
||
flag: bool = True
|
||
for reply_content in reply_set.reply_data:
|
||
status: bool = False
|
||
message_seg, need_typing = _parse_content_to_seg(reply_content)
|
||
status = await _send_to_target(
|
||
message_segment=message_seg,
|
||
stream_id=stream_id,
|
||
display_message=display_message,
|
||
typing=bool(need_typing and typing),
|
||
reply_message=reply_message,
|
||
set_reply=set_reply,
|
||
storage_message=storage_message,
|
||
show_log=show_log,
|
||
)
|
||
if not status:
|
||
flag = False
|
||
logger.error(
|
||
f"[SendAPI] 发送{repr(reply_content.content_type)}消息失败,消息内容:{str(reply_content.content)[:100]}"
|
||
)
|
||
|
||
return flag
|
||
|
||
|
||
def _parse_content_to_seg(reply_content: "ReplyContent") -> Tuple[Seg, bool]:
|
||
"""
|
||
把 ReplyContent 转换为 Seg 结构 (Forward 中仅递归一次)
|
||
Args:
|
||
reply_content: ReplyContent 对象
|
||
Returns:
|
||
Tuple[Seg, bool]: 转换后的 Seg 结构和是否需要typing的标志
|
||
"""
|
||
content_type = reply_content.content_type
|
||
if content_type == ReplyContentType.TEXT:
|
||
text_data: str = reply_content.content # type: ignore
|
||
return Seg(type="text", data=text_data), True
|
||
elif content_type == ReplyContentType.IMAGE:
|
||
return Seg(type="image", data=reply_content.content), False # type: ignore
|
||
elif content_type == ReplyContentType.EMOJI:
|
||
return Seg(type="emoji", data=reply_content.content), False # type: ignore
|
||
elif content_type == ReplyContentType.COMMAND:
|
||
return Seg(type="command", data=reply_content.content), False # type: ignore
|
||
elif content_type == ReplyContentType.VOICE:
|
||
return Seg(type="voice", data=reply_content.content), False # type: ignore
|
||
elif content_type == ReplyContentType.HYBRID:
|
||
hybrid_message_list_data: List[ReplyContent] = reply_content.content # type: ignore
|
||
assert isinstance(hybrid_message_list_data, list), "混合类型内容必须是列表"
|
||
sub_seg_list: List[Seg] = []
|
||
for sub_content in hybrid_message_list_data:
|
||
sub_content_type = sub_content.content_type
|
||
sub_content_data = sub_content.content
|
||
|
||
if sub_content_type == ReplyContentType.TEXT:
|
||
sub_seg_list.append(Seg(type="text", data=sub_content_data)) # type: ignore
|
||
elif sub_content_type == ReplyContentType.IMAGE:
|
||
sub_seg_list.append(Seg(type="image", data=sub_content_data)) # type: ignore
|
||
elif sub_content_type == ReplyContentType.EMOJI:
|
||
sub_seg_list.append(Seg(type="emoji", data=sub_content_data)) # type: ignore
|
||
else:
|
||
logger.warning(f"[SendAPI] 混合类型中不支持的子内容类型: {repr(sub_content_type)}")
|
||
continue
|
||
return Seg(type="seglist", data=sub_seg_list), True
|
||
elif content_type == ReplyContentType.FORWARD:
|
||
forward_message_list_data: List["ForwardNode"] = reply_content.content # type: ignore
|
||
assert isinstance(forward_message_list_data, list), "转发类型内容必须是列表"
|
||
forward_message_list: List[Dict] = []
|
||
for forward_node in forward_message_list_data:
|
||
message_segment = Seg(type="id", data=forward_node.content) # type: ignore
|
||
user_info: Optional[UserInfo] = None
|
||
if forward_node.user_id and forward_node.user_nickname:
|
||
assert isinstance(forward_node.content, list), "转发节点内容必须是列表"
|
||
user_info = UserInfo(user_id=forward_node.user_id, user_nickname=forward_node.user_nickname)
|
||
single_node_content: List[Seg] = []
|
||
for sub_content in forward_node.content:
|
||
if sub_content.content_type != ReplyContentType.FORWARD:
|
||
sub_seg, _ = _parse_content_to_seg(sub_content)
|
||
single_node_content.append(sub_seg)
|
||
message_segment = Seg(type="seglist", data=single_node_content)
|
||
forward_message_list.append(
|
||
MessageBase(
|
||
message_segment=message_segment, message_info=BaseMessageInfo(user_info=user_info)
|
||
).to_dict()
|
||
)
|
||
return Seg(type="forward", data=forward_message_list), False # type: ignore
|
||
else:
|
||
message_type_in_str = content_type.value if isinstance(content_type, ReplyContentType) else str(content_type)
|
||
return Seg(type=message_type_in_str, data=reply_content.content), True # type: ignore
|