feat(satori): 添加对合并转发消息功能的支持 (#3050)
* Update satori_event.py * Update satori_event.py * Update satori_event.py * Update satori_adapter.py * style: format code for better readability in satori_adapter.py and satori_event.py --------- Co-authored-by: Soulter <905617992@qq.com>
This commit is contained in:
@@ -499,10 +499,36 @@ class SatoriPlatformAdapter(Platform):
|
||||
}
|
||||
|
||||
return None
|
||||
except ET.ParseError as e:
|
||||
logger.warning(f"XML解析失败,使用正则提取: {e}")
|
||||
return await self._extract_quote_with_regex(content)
|
||||
except Exception as e:
|
||||
logger.error(f"提取<quote>标签时发生错误: {e}")
|
||||
return None
|
||||
|
||||
async def _extract_quote_with_regex(self, content: str) -> Optional[dict]:
|
||||
"""使用正则表达式提取quote标签信息"""
|
||||
import re
|
||||
|
||||
quote_pattern = r"<quote\s+([^>]*)>(.*?)</quote>"
|
||||
match = re.search(quote_pattern, content, re.DOTALL)
|
||||
|
||||
if not match:
|
||||
return None
|
||||
|
||||
attrs_str = match.group(1)
|
||||
inner_content = match.group(2)
|
||||
|
||||
id_match = re.search(r'id\s*=\s*["\']([^"\']*)["\']', attrs_str)
|
||||
quote_id = id_match.group(1) if id_match else ""
|
||||
content_without_quote = content.replace(match.group(0), "")
|
||||
content_without_quote = content_without_quote.strip()
|
||||
|
||||
return {
|
||||
"quote": {"id": quote_id, "content": inner_content},
|
||||
"content_without_quote": content_without_quote,
|
||||
}
|
||||
|
||||
async def _convert_quote_message(self, quote: dict) -> Optional[AstrBotMessage]:
|
||||
"""转换引用消息"""
|
||||
try:
|
||||
@@ -574,7 +600,7 @@ class SatoriPlatformAdapter(Platform):
|
||||
root = ET.fromstring(processed_content)
|
||||
await self._parse_xml_node(root, elements)
|
||||
except ET.ParseError as e:
|
||||
logger.error(f"解析 Satori 元素时发生解析错误: {e}, 错误内容: {content}")
|
||||
logger.warning(f"解析 Satori 元素时发生解析错误: {e}, 错误内容: {content}")
|
||||
# 如果解析失败,将整个内容当作纯文本
|
||||
if content.strip():
|
||||
elements.append(Plain(text=content))
|
||||
|
||||
@@ -2,7 +2,18 @@ from typing import TYPE_CHECKING
|
||||
from astrbot.api import logger
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
|
||||
from astrbot.api.message_components import Plain, Image, At, File, Record, Video, Reply
|
||||
from astrbot.api.message_components import (
|
||||
Plain,
|
||||
Image,
|
||||
At,
|
||||
File,
|
||||
Record,
|
||||
Video,
|
||||
Reply,
|
||||
Forward,
|
||||
Node,
|
||||
Nodes,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .satori_adapter import SatoriPlatformAdapter
|
||||
@@ -48,55 +59,24 @@ class SatoriPlatformEvent(AstrMessageEvent):
|
||||
content_parts = []
|
||||
|
||||
for component in message.chain:
|
||||
if isinstance(component, Plain):
|
||||
text = (
|
||||
component.text.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
)
|
||||
content_parts.append(text)
|
||||
component_content = await cls._convert_component_to_satori_static(
|
||||
component
|
||||
)
|
||||
if component_content:
|
||||
content_parts.append(component_content)
|
||||
|
||||
elif isinstance(component, At):
|
||||
if component.qq:
|
||||
content_parts.append(f'<at id="{component.qq}"/>')
|
||||
elif component.name:
|
||||
content_parts.append(f'<at name="{component.name}"/>')
|
||||
# 特殊处理 Node 和 Nodes 组件
|
||||
if isinstance(component, Node):
|
||||
# 单个转发节点
|
||||
node_content = await cls._convert_node_to_satori_static(component)
|
||||
if node_content:
|
||||
content_parts.append(node_content)
|
||||
|
||||
elif isinstance(component, Image):
|
||||
try:
|
||||
image_base64 = await component.convert_to_base64()
|
||||
if image_base64:
|
||||
content_parts.append(
|
||||
f'<img src="data:image/jpeg;base64,{image_base64}"/>'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"图片转换为base64失败: {e}")
|
||||
|
||||
elif isinstance(component, File):
|
||||
content_parts.append(
|
||||
f'<file src="{component.file}" name="{component.name or "文件"}"/>'
|
||||
)
|
||||
|
||||
elif isinstance(component, Record):
|
||||
try:
|
||||
record_base64 = await component.convert_to_base64()
|
||||
if record_base64:
|
||||
content_parts.append(
|
||||
f'<audio src="data:audio/wav;base64,{record_base64}"/>'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"语音转换为base64失败: {e}")
|
||||
|
||||
elif isinstance(component, Reply):
|
||||
content_parts.append(f'<reply id="{component.id}"/>')
|
||||
|
||||
elif isinstance(component, Video):
|
||||
try:
|
||||
video_path_url = await component.convert_to_file_path()
|
||||
if video_path_url:
|
||||
content_parts.append(f'<video src="{video_path_url}"/>')
|
||||
except Exception as e:
|
||||
logger.error(f"视频文件转换失败: {e}")
|
||||
elif isinstance(component, Nodes):
|
||||
# 合并转发消息
|
||||
node_content = await cls._convert_nodes_to_satori_static(component)
|
||||
if node_content:
|
||||
content_parts.append(node_content)
|
||||
|
||||
content = "".join(content_parts)
|
||||
channel_id = session_id
|
||||
@@ -138,55 +118,22 @@ class SatoriPlatformEvent(AstrMessageEvent):
|
||||
content_parts = []
|
||||
|
||||
for component in message.chain:
|
||||
if isinstance(component, Plain):
|
||||
text = (
|
||||
component.text.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
)
|
||||
content_parts.append(text)
|
||||
component_content = await self._convert_component_to_satori(component)
|
||||
if component_content:
|
||||
content_parts.append(component_content)
|
||||
|
||||
elif isinstance(component, At):
|
||||
if component.qq:
|
||||
content_parts.append(f'<at id="{component.qq}"/>')
|
||||
elif component.name:
|
||||
content_parts.append(f'<at name="{component.name}"/>')
|
||||
# 特殊处理 Node 和 Nodes 组件
|
||||
if isinstance(component, Node):
|
||||
# 单个转发节点
|
||||
node_content = await self._convert_node_to_satori(component)
|
||||
if node_content:
|
||||
content_parts.append(node_content)
|
||||
|
||||
elif isinstance(component, Image):
|
||||
try:
|
||||
image_base64 = await component.convert_to_base64()
|
||||
if image_base64:
|
||||
content_parts.append(
|
||||
f'<img src="data:image/jpeg;base64,{image_base64}"/>'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"图片转换为base64失败: {e}")
|
||||
|
||||
elif isinstance(component, File):
|
||||
content_parts.append(
|
||||
f'<file src="{component.file}" name="{component.name or "文件"}"/>'
|
||||
)
|
||||
|
||||
elif isinstance(component, Record):
|
||||
try:
|
||||
record_base64 = await component.convert_to_base64()
|
||||
if record_base64:
|
||||
content_parts.append(
|
||||
f'<audio src="data:audio/wav;base64,{record_base64}"/>'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"语音转换为base64失败: {e}")
|
||||
|
||||
elif isinstance(component, Reply):
|
||||
content_parts.append(f'<reply id="{component.id}"/>')
|
||||
|
||||
elif isinstance(component, Video):
|
||||
try:
|
||||
video_path_url = await component.convert_to_file_path()
|
||||
if video_path_url:
|
||||
content_parts.append(f'<video src="{video_path_url}"/>')
|
||||
except Exception as e:
|
||||
logger.error(f"视频文件转换失败: {e}")
|
||||
elif isinstance(component, Nodes):
|
||||
# 合并转发消息
|
||||
node_content = await self._convert_nodes_to_satori(component)
|
||||
if node_content:
|
||||
content_parts.append(node_content)
|
||||
|
||||
content = "".join(content_parts)
|
||||
channel_id = self.session_id
|
||||
@@ -250,3 +197,227 @@ class SatoriPlatformEvent(AstrMessageEvent):
|
||||
logger.error(f"Satori 流式消息发送异常: {e}")
|
||||
|
||||
return await super().send_streaming(generator, use_fallback)
|
||||
|
||||
async def _convert_component_to_satori(self, component) -> str:
|
||||
"""将单个消息组件转换为 Satori 格式"""
|
||||
try:
|
||||
if isinstance(component, Plain):
|
||||
text = (
|
||||
component.text.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
)
|
||||
return text
|
||||
|
||||
elif isinstance(component, At):
|
||||
if component.qq:
|
||||
return f'<at id="{component.qq}"/>'
|
||||
elif component.name:
|
||||
return f'<at name="{component.name}"/>'
|
||||
|
||||
elif isinstance(component, Image):
|
||||
try:
|
||||
image_base64 = await component.convert_to_base64()
|
||||
if image_base64:
|
||||
return f'<img src="data:image/jpeg;base64,{image_base64}"/>'
|
||||
except Exception as e:
|
||||
logger.error(f"图片转换为base64失败: {e}")
|
||||
|
||||
elif isinstance(component, File):
|
||||
return (
|
||||
f'<file src="{component.file}" name="{component.name or "文件"}"/>'
|
||||
)
|
||||
|
||||
elif isinstance(component, Record):
|
||||
try:
|
||||
record_base64 = await component.convert_to_base64()
|
||||
if record_base64:
|
||||
return f'<audio src="data:audio/wav;base64,{record_base64}"/>'
|
||||
except Exception as e:
|
||||
logger.error(f"语音转换为base64失败: {e}")
|
||||
|
||||
elif isinstance(component, Reply):
|
||||
return f'<reply id="{component.id}"/>'
|
||||
|
||||
elif isinstance(component, Video):
|
||||
try:
|
||||
video_path_url = await component.convert_to_file_path()
|
||||
if video_path_url:
|
||||
return f'<video src="{video_path_url}"/>'
|
||||
except Exception as e:
|
||||
logger.error(f"视频文件转换失败: {e}")
|
||||
|
||||
elif isinstance(component, Forward):
|
||||
return f'<message id="{component.id}" forward/>'
|
||||
|
||||
# 对于其他未处理的组件类型,返回空字符串
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"转换消息组件失败: {e}")
|
||||
return ""
|
||||
|
||||
async def _convert_node_to_satori(self, node: Node) -> str:
|
||||
"""将单个转发节点转换为 Satori 格式"""
|
||||
try:
|
||||
content_parts = []
|
||||
if node.content:
|
||||
for content_component in node.content:
|
||||
component_content = await self._convert_component_to_satori(
|
||||
content_component
|
||||
)
|
||||
if component_content:
|
||||
content_parts.append(component_content)
|
||||
|
||||
content = "".join(content_parts)
|
||||
|
||||
# 如果内容为空,添加默认内容
|
||||
if not content.strip():
|
||||
content = "[转发消息]"
|
||||
|
||||
# 构建 Satori 格式的转发节点
|
||||
author_attrs = []
|
||||
if node.uin:
|
||||
author_attrs.append(f'id="{node.uin}"')
|
||||
if node.name:
|
||||
author_attrs.append(f'name="{node.name}"')
|
||||
|
||||
author_attr_str = " ".join(author_attrs)
|
||||
|
||||
return f"<message><author {author_attr_str}/>{content}</message>"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"转换转发节点失败: {e}")
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
async def _convert_component_to_satori_static(cls, component) -> str:
|
||||
"""将单个消息组件转换为 Satori 格式"""
|
||||
try:
|
||||
if isinstance(component, Plain):
|
||||
text = (
|
||||
component.text.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
)
|
||||
return text
|
||||
|
||||
elif isinstance(component, At):
|
||||
if component.qq:
|
||||
return f'<at id="{component.qq}"/>'
|
||||
elif component.name:
|
||||
return f'<at name="{component.name}"/>'
|
||||
|
||||
elif isinstance(component, Image):
|
||||
try:
|
||||
image_base64 = await component.convert_to_base64()
|
||||
if image_base64:
|
||||
return f'<img src="data:image/jpeg;base64,{image_base64}"/>'
|
||||
except Exception as e:
|
||||
logger.error(f"图片转换为base64失败: {e}")
|
||||
|
||||
elif isinstance(component, File):
|
||||
return (
|
||||
f'<file src="{component.file}" name="{component.name or "文件"}"/>'
|
||||
)
|
||||
|
||||
elif isinstance(component, Record):
|
||||
try:
|
||||
record_base64 = await component.convert_to_base64()
|
||||
if record_base64:
|
||||
return f'<audio src="data:audio/wav;base64,{record_base64}"/>'
|
||||
except Exception as e:
|
||||
logger.error(f"语音转换为base64失败: {e}")
|
||||
|
||||
elif isinstance(component, Reply):
|
||||
return f'<reply id="{component.id}"/>'
|
||||
|
||||
elif isinstance(component, Video):
|
||||
try:
|
||||
video_path_url = await component.convert_to_file_path()
|
||||
if video_path_url:
|
||||
return f'<video src="{video_path_url}"/>'
|
||||
except Exception as e:
|
||||
logger.error(f"视频文件转换失败: {e}")
|
||||
|
||||
elif isinstance(component, Forward):
|
||||
return f'<message id="{component.id}" forward/>'
|
||||
|
||||
# 对于其他未处理的组件类型,返回空字符串
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"转换消息组件失败: {e}")
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
async def _convert_node_to_satori_static(cls, node: Node) -> str:
|
||||
"""将单个转发节点转换为 Satori 格式"""
|
||||
try:
|
||||
content_parts = []
|
||||
if node.content:
|
||||
for content_component in node.content:
|
||||
component_content = await cls._convert_component_to_satori_static(
|
||||
content_component
|
||||
)
|
||||
if component_content:
|
||||
content_parts.append(component_content)
|
||||
|
||||
content = "".join(content_parts)
|
||||
|
||||
# 如果内容为空,添加默认内容
|
||||
if not content.strip():
|
||||
content = "[转发消息]"
|
||||
|
||||
author_attrs = []
|
||||
if node.uin:
|
||||
author_attrs.append(f'id="{node.uin}"')
|
||||
if node.name:
|
||||
author_attrs.append(f'name="{node.name}"')
|
||||
|
||||
author_attr_str = " ".join(author_attrs)
|
||||
|
||||
return f"<message><author {author_attr_str}/>{content}</message>"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"转换转发节点失败: {e}")
|
||||
return ""
|
||||
|
||||
async def _convert_nodes_to_satori(self, nodes: Nodes) -> str:
|
||||
"""将多个转发节点转换为 Satori 格式的合并转发"""
|
||||
try:
|
||||
node_parts = []
|
||||
|
||||
for node in nodes.nodes:
|
||||
node_content = await self._convert_node_to_satori(node)
|
||||
if node_content:
|
||||
node_parts.append(node_content)
|
||||
|
||||
if node_parts:
|
||||
return f"<message forward>{''.join(node_parts)}</message>"
|
||||
else:
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"转换合并转发消息失败: {e}")
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
async def _convert_nodes_to_satori_static(cls, nodes: Nodes) -> str:
|
||||
"""将多个转发节点转换为 Satori 格式的合并转发"""
|
||||
try:
|
||||
node_parts = []
|
||||
|
||||
for node in nodes.nodes:
|
||||
node_content = await cls._convert_node_to_satori_static(node)
|
||||
if node_content:
|
||||
node_parts.append(node_content)
|
||||
|
||||
if node_parts:
|
||||
return f"<message forward>{''.join(node_parts)}</message>"
|
||||
else:
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"转换合并转发消息失败: {e}")
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user