From 131359c3e37b8a77f3510a4d3070929de31c3cbd Mon Sep 17 00:00:00 2001 From: shangxue <1919892171@qq.com> Date: Tue, 16 Sep 2025 20:19:42 +0800 Subject: [PATCH 1/2] Update satori_adapter.py --- .../platform/sources/satori/satori_adapter.py | 287 ++++++++++++++++-- 1 file changed, 261 insertions(+), 26 deletions(-) diff --git a/astrbot/core/platform/sources/satori/satori_adapter.py b/astrbot/core/platform/sources/satori/satori_adapter.py index 7fdeffbf4..dc6a4ab05 100644 --- a/astrbot/core/platform/sources/satori/satori_adapter.py +++ b/astrbot/core/platform/sources/satori/satori_adapter.py @@ -3,7 +3,7 @@ import time import websockets from websockets.asyncio.client import connect -from typing import Optional +from typing import Optional, List from aiohttp import ClientSession, ClientTimeout from websockets.asyncio.client import ClientConnection from astrbot.api import logger @@ -17,7 +17,7 @@ register_platform_adapter, ) from astrbot.core.platform.astr_message_event import MessageSession -from astrbot.api.message_components import Plain, Image, At, File, Record +from astrbot.api.message_components import Plain, Image, At, File, Record, BaseMessageComponent, Reply from xml.etree import ElementTree as ET @@ -38,12 +38,18 @@ def __init__( ) self.token = self.config.get("satori_token", "") self.endpoint = self.config.get( - "satori_endpoint", "ws://127.0.0.1:5140/satori/v1/events" + "satori_endpoint", "ws://localhost:5140/satori/v1/events" ) self.auto_reconnect = self.config.get("satori_auto_reconnect", True) self.heartbeat_interval = self.config.get("satori_heartbeat_interval", 10) self.reconnect_delay = self.config.get("satori_reconnect_delay", 5) + self.metadata = PlatformMetadata( + name="satori", + description="Satori 通用协议适配器", + id=self.config.get("id"), + ) + self.ws: Optional[ClientConnection] = None self.session: Optional[ClientSession] = None self.sequence = 0 @@ -63,7 +69,7 @@ async def send_by_session( await super().send_by_session(session, message_chain) def meta(self) -> PlatformMetadata: - return PlatformMetadata(name="satori", description="Satori 通用协议适配器") + return self.metadata def _is_websocket_closed(self, ws) -> bool: """检查WebSocket连接是否已关闭""" @@ -312,12 +318,52 @@ async def convert_satori_message( abm.self_id = login.get("user", {}).get("id", "") + # 消息链 + abm.message = [] + content = message.get("content", "") - abm.message = await self.parse_satori_elements(content) + + quote = message.get("quote") + content_for_parsing = content # 副本 + + # 提取标签 + if "标签时发生错误: {e}, 错误内容: {content}") + + if quote: + # 引用消息 + quote_abm = await self._convert_quote_message(quote) + if quote_abm: + sender_id = quote_abm.sender.user_id + if isinstance(sender_id, str) and sender_id.isdigit(): + sender_id = int(sender_id) + elif not isinstance(sender_id, int): + sender_id = 0 # 默认值 + + reply_component = Reply( + id=quote_abm.message_id, + chain=quote_abm.message, + sender_id=quote_abm.sender.user_id, + sender_nickname=quote_abm.sender.nickname, + time=quote_abm.timestamp, + message_str=quote_abm.message_str, + text=quote_abm.message_str, + qq=sender_id, + ) + abm.message.append(reply_component) + + # 解析消息内容 + content_elements = await self.parse_satori_elements(content_for_parsing) + abm.message.extend(content_elements) - # parse message_str abm.message_str = "" - for comp in abm.message: + for comp in content_elements: if isinstance(comp, Plain): abm.message_str += comp.text @@ -333,6 +379,155 @@ async def convert_satori_message( logger.error(f"转换 Satori 消息失败: {e}") return None + def _extract_namespace_prefixes(self, content: str) -> set: + """提取XML内容中的命名空间前缀""" + prefixes = set() + + # 查找所有标签 + i = 0 + while i < len(content): + # 查找开始标签 + if content[i] == '<' and i + 1 < len(content) and content[i + 1] != '/': + # 找到标签结束位置 + tag_end = content.find('>', i) + if tag_end != -1: + # 提取标签内容 + tag_content = content[i + 1:tag_end] + # 检查是否有命名空间前缀 + if ':' in tag_content and 'xmlns:' not in tag_content: + # 分割标签名 + parts = tag_content.split() + if parts: + tag_name = parts[0] + if ':' in tag_name: + prefix = tag_name.split(':')[0] + # 确保是有效的命名空间前缀 + if prefix.isalnum() or prefix.replace('_', '').isalnum(): + prefixes.add(prefix) + i = tag_end + 1 + else: + i += 1 + # 查找结束标签 + elif content[i] == '<' and i + 1 < len(content) and content[i + 1] == '/': + # 找到标签结束位置 + tag_end = content.find('>', i) + if tag_end != -1: + # 提取标签内容 + tag_content = content[i + 2:tag_end] + # 检查是否有命名空间前缀 + if ':' in tag_content: + prefix = tag_content.split(':')[0] + # 确保是有效的命名空间前缀 + if prefix.isalnum() or prefix.replace('_', '').isalnum(): + prefixes.add(prefix) + i = tag_end + 1 + else: + i += 1 + else: + i += 1 + + return prefixes + + async def _extract_quote_element(self, content: str) -> Optional[dict]: + """提取标签信息""" + try: + # 处理命名空间前缀问题 + processed_content = content + if ':' in content and not content.startswith('{content}" + elif not content.startswith('{content}" + else: + processed_content = content + + root = ET.fromstring(processed_content) + + # 查找标签 + quote_element = None + for elem in root.iter(): + tag_name = elem.tag + if '}' in tag_name: + tag_name = tag_name.split('}')[1] + if tag_name.lower() == "quote": + quote_element = elem + break + + if quote_element is not None: + # 提取quote标签的属性 + quote_id = quote_element.get("id", "") + + # 提取标签内部的内容 + inner_content = "" + if quote_element.text: + inner_content += quote_element.text + for child in quote_element: + inner_content += ET.tostring(child, encoding='unicode', method='xml') + if child.tail: + inner_content += child.tail + + # 构造移除了标签的内容 + content_without_quote = content.replace( + ET.tostring(quote_element, encoding='unicode', method='xml'), "") + + return { + "quote": { + "id": quote_id, + "content": inner_content + }, + "content_without_quote": content_without_quote + } + + return None + except Exception as e: + logger.error(f"提取标签时发生错误: {e}") + return None + + async def _convert_quote_message(self, quote: dict) -> Optional[AstrBotMessage]: + """转换引用消息""" + try: + quote_abm = AstrBotMessage() + quote_abm.message_id = quote.get("id", "") + + # 解析引用消息的发送者 + quote_author = quote.get("author", {}) + if quote_author: + quote_abm.sender = MessageMember( + user_id=quote_author.get("id", ""), + nickname=quote_author.get("nick", quote_author.get("name", "")), + ) + else: + # 如果没有作者信息,使用默认值 + quote_abm.sender = MessageMember( + user_id=quote.get("user_id", ""), + nickname="内容", + ) + + # 解析引用消息内容 + quote_content = quote.get("content", "") + quote_abm.message = await self.parse_satori_elements(quote_content) + + quote_abm.message_str = "" + for comp in quote_abm.message: + if isinstance(comp, Plain): + quote_abm.message_str += comp.text + + quote_abm.timestamp = int(quote.get("timestamp", time.time())) + + # 如果没有任何内容,使用默认文本 + if not quote_abm.message_str.strip(): + quote_abm.message_str = "[引用消息]" + + return quote_abm + except Exception as e: + logger.error(f"转换引用消息失败: {e}") + return None + async def parse_satori_elements(self, content: str) -> list: """解析 Satori 消息元素""" elements = [] @@ -341,12 +536,30 @@ async def parse_satori_elements(self, content: str) -> list: return elements try: - wrapped_content = f"{content}" - root = ET.fromstring(wrapped_content) + # 处理命名空间前缀问题 + processed_content = content + if ':' in content and not content.startswith('{content}" + elif not content.startswith('{content}" + else: + processed_content = content + + root = ET.fromstring(processed_content) await self._parse_xml_node(root, elements) except ET.ParseError as e: - raise ValueError(f"解析 Satori 元素时发生解析错误: {e}") + logger.error(f"解析 Satori 元素时发生解析错误: {e}, 错误内容: {content}") + # 如果解析失败,将整个内容当作纯文本 + if content.strip(): + elements.append(Plain(text=content)) except Exception as e: + logger.error(f"解析 Satori 元素时发生未知错误: {e}") raise e # 如果没有解析到任何元素,将整个内容当作纯文本 @@ -361,9 +574,14 @@ async def _parse_xml_node(self, node: ET.Element, elements: list) -> None: elements.append(Plain(text=node.text)) for child in node: - tag_name = child.tag.lower() + # 获取标签名,去除命名空间前缀 + tag_name = child.tag + if '}' in tag_name: + tag_name = tag_name.split('}')[1] + tag_name = tag_name.lower() + attrs = child.attrib - + if tag_name == "at": user_id = attrs.get("id") or attrs.get("name", "") elements.append(At(qq=user_id, name=user_id)) @@ -372,31 +590,48 @@ async def _parse_xml_node(self, node: ET.Element, elements: list) -> None: src = attrs.get("src", "") if not src: continue - if src.startswith("data:image/"): - src = src.split(",")[1] - elements.append(Image.fromBase64(src)) - elif src.startswith("http"): - elements.append(Image.fromURL(src)) - else: - logger.error(f"未知的图片 src 格式: {str(src)[:16]}") + elements.append(Image(file=src)) elif tag_name == "file": src = attrs.get("src", "") name = attrs.get("name", "文件") if src: - elements.append(File(file=src, name=name)) + elements.append(File(name=name, file=src)) elif tag_name in ("audio", "record"): src = attrs.get("src", "") if not src: continue - if src.startswith("data:audio/"): - src = src.split(",")[1] - elements.append(Record.fromBase64(src)) - elif src.startswith("http"): - elements.append(Record.fromURL(src)) + elements.append(Record(file=src)) + + elif tag_name == "quote": + # quote标签已经被特殊处理 + pass + + elif tag_name == "face": + face_id = attrs.get("id", "") + face_name = attrs.get("name", "") + face_platform = attrs.get("platform", "") + face_type = attrs.get("type", "") + + if face_name: + elements.append(Plain(text=f"[表情:{face_name}]")) + elif face_id and face_type: + elements.append(Plain(text=f"[表情ID:{face_id},类型:{face_type}]")) + elif face_id: + elements.append(Plain(text=f"[表情ID:{face_id}]")) + else: + elements.append(Plain(text="[表情]")) + + elif tag_name == "ark": + # 作为纯文本添加到消息链中 + data = attrs.get("data", "") + if data: + import html + decoded_data = html.unescape(data) + elements.append(Plain(text=f"[ARK卡片数据: {decoded_data}]")) else: - logger.error(f"未知的音频 src 格式: {str(src)[:16]}") + elements.append(Plain(text="[ARK卡片]")) else: # 未知标签,递归处理其内容 From dd3c96d2a72d6e8fc01eb6e55c5dd7b0f9c5baed Mon Sep 17 00:00:00 2001 From: shangxue <1919892171@qq.com> Date: Tue, 16 Sep 2025 20:48:36 +0800 Subject: [PATCH 2/2] Update satori_adapter.py --- astrbot/core/platform/sources/satori/satori_adapter.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/astrbot/core/platform/sources/satori/satori_adapter.py b/astrbot/core/platform/sources/satori/satori_adapter.py index dc6a4ab05..330264a56 100644 --- a/astrbot/core/platform/sources/satori/satori_adapter.py +++ b/astrbot/core/platform/sources/satori/satori_adapter.py @@ -633,6 +633,16 @@ async def _parse_xml_node(self, node: ET.Element, elements: list) -> None: else: elements.append(Plain(text="[ARK卡片]")) + elif tag_name == "json": + # JSON标签 视为ARK卡片消息 + data = attrs.get("data", "") + if data: + import html + decoded_data = html.unescape(data) + elements.append(Plain(text=f"[ARK卡片数据: {decoded_data}]")) + else: + elements.append(Plain(text="[JSON卡片]")) + else: # 未知标签,递归处理其内容 if child.text and child.text.strip():