From cda3a2f30559aa67b7d68fa8fd9513020fd15561 Mon Sep 17 00:00:00 2001 From: shangxue <1919892171@qq.com> Date: Mon, 8 Sep 2025 17:09:15 +0800 Subject: [PATCH 1/9] Update PlatformPage.vue --- dashboard/src/views/PlatformPage.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/src/views/PlatformPage.vue b/dashboard/src/views/PlatformPage.vue index 0dd71be20..c5078672f 100644 --- a/dashboard/src/views/PlatformPage.vue +++ b/dashboard/src/views/PlatformPage.vue @@ -331,7 +331,7 @@ export default { "slack": "https://astrbot.app/deploy/platform/slack.html", "kook": "https://astrbot.app/deploy/platform/kook.html", "vocechat": "https://astrbot.app/deploy/platform/vocechat.html", - "satori": "https://astrbot.app/deploy/platform/satori.html", // TODO + "satori": "https://astrbot.app/deploy/platform/satori/llonebot.html", } return tutorial_map[platform_type] || "https://docs.astrbot.app"; }, From c2be6536f873d88f61c7df4c1d439cee1db7d2cd Mon Sep 17 00:00:00 2001 From: shangxue <1919892171@qq.com> Date: Mon, 8 Sep 2025 17:12:31 +0800 Subject: [PATCH 2/9] Update PlatformPage.vue --- dashboard/src/views/PlatformPage.vue | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/dashboard/src/views/PlatformPage.vue b/dashboard/src/views/PlatformPage.vue index c5078672f..a1cf69068 100644 --- a/dashboard/src/views/PlatformPage.vue +++ b/dashboard/src/views/PlatformPage.vue @@ -318,20 +318,20 @@ export default { getTutorialLink(platform_type) { let tutorial_map = { - "qq_official_webhook": "https://astrbot.app/deploy/platform/qqofficial/webhook.html", - "qq_official": "https://astrbot.app/deploy/platform/qqofficial/websockets.html", - "aiocqhttp": "https://astrbot.app/deploy/platform/aiocqhttp/napcat.html", - "wecom": "https://astrbot.app/deploy/platform/wecom.html", - "lark": "https://astrbot.app/deploy/platform/lark.html", - "telegram": "https://astrbot.app/deploy/platform/telegram.html", - "dingtalk": "https://astrbot.app/deploy/platform/dingtalk.html", - "wechatpadpro": "https://astrbot.app/deploy/platform/wechat/wechatpadpro.html", - "weixin_official_account": "https://astrbot.app/deploy/platform/weixin-official-account.html", - "discord": "https://astrbot.app/deploy/platform/discord.html", - "slack": "https://astrbot.app/deploy/platform/slack.html", - "kook": "https://astrbot.app/deploy/platform/kook.html", - "vocechat": "https://astrbot.app/deploy/platform/vocechat.html", - "satori": "https://astrbot.app/deploy/platform/satori/llonebot.html", + "qq_official_webhook": "https://docs.astrbot.app/deploy/platform/qqofficial/webhook.html", + "qq_official": "https://docs.astrbot.app/deploy/platform/qqofficial/websockets.html", + "aiocqhttp": "https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html", + "wecom": "https://docs.astrbot.app/deploy/platform/wecom.html", + "lark": "https://docs.astrbot.app/deploy/platform/lark.html", + "telegram": "https://docs.astrbot.app/deploy/platform/telegram.html", + "dingtalk": "https://docs.astrbot.app/deploy/platform/dingtalk.html", + "wechatpadpro": "https://docs.astrbot.app/deploy/platform/wechat/wechatpadpro.html", + "weixin_official_account": "https://docs.astrbot.app/deploy/platform/weixin-official-account.html", + "discord": "https://docs.astrbot.app/deploy/platform/discord.html", + "slack": "https://docs.astrbot.app/deploy/platform/slack.html", + "kook": "https://docs.astrbot.app/deploy/platform/kook.html", + "vocechat": "https://docs.astrbot.app/deploy/platform/vocechat.html", + "satori": "https://docs.astrbot.app/deploy/platform/satori/llonebot.html", } return tutorial_map[platform_type] || "https://docs.astrbot.app"; }, From 3bdd25884fc5bd5e1f19c19acc1d11e4317a95c0 Mon Sep 17 00:00:00 2001 From: shangxue <1919892171@qq.com> Date: Tue, 9 Sep 2025 19:19:37 +0800 Subject: [PATCH 3/9] Update PlatformPage.vue --- dashboard/src/views/PlatformPage.vue | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/dashboard/src/views/PlatformPage.vue b/dashboard/src/views/PlatformPage.vue index a1cf69068..0dd71be20 100644 --- a/dashboard/src/views/PlatformPage.vue +++ b/dashboard/src/views/PlatformPage.vue @@ -318,20 +318,20 @@ export default { getTutorialLink(platform_type) { let tutorial_map = { - "qq_official_webhook": "https://docs.astrbot.app/deploy/platform/qqofficial/webhook.html", - "qq_official": "https://docs.astrbot.app/deploy/platform/qqofficial/websockets.html", - "aiocqhttp": "https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html", - "wecom": "https://docs.astrbot.app/deploy/platform/wecom.html", - "lark": "https://docs.astrbot.app/deploy/platform/lark.html", - "telegram": "https://docs.astrbot.app/deploy/platform/telegram.html", - "dingtalk": "https://docs.astrbot.app/deploy/platform/dingtalk.html", - "wechatpadpro": "https://docs.astrbot.app/deploy/platform/wechat/wechatpadpro.html", - "weixin_official_account": "https://docs.astrbot.app/deploy/platform/weixin-official-account.html", - "discord": "https://docs.astrbot.app/deploy/platform/discord.html", - "slack": "https://docs.astrbot.app/deploy/platform/slack.html", - "kook": "https://docs.astrbot.app/deploy/platform/kook.html", - "vocechat": "https://docs.astrbot.app/deploy/platform/vocechat.html", - "satori": "https://docs.astrbot.app/deploy/platform/satori/llonebot.html", + "qq_official_webhook": "https://astrbot.app/deploy/platform/qqofficial/webhook.html", + "qq_official": "https://astrbot.app/deploy/platform/qqofficial/websockets.html", + "aiocqhttp": "https://astrbot.app/deploy/platform/aiocqhttp/napcat.html", + "wecom": "https://astrbot.app/deploy/platform/wecom.html", + "lark": "https://astrbot.app/deploy/platform/lark.html", + "telegram": "https://astrbot.app/deploy/platform/telegram.html", + "dingtalk": "https://astrbot.app/deploy/platform/dingtalk.html", + "wechatpadpro": "https://astrbot.app/deploy/platform/wechat/wechatpadpro.html", + "weixin_official_account": "https://astrbot.app/deploy/platform/weixin-official-account.html", + "discord": "https://astrbot.app/deploy/platform/discord.html", + "slack": "https://astrbot.app/deploy/platform/slack.html", + "kook": "https://astrbot.app/deploy/platform/kook.html", + "vocechat": "https://astrbot.app/deploy/platform/vocechat.html", + "satori": "https://astrbot.app/deploy/platform/satori.html", // TODO } return tutorial_map[platform_type] || "https://docs.astrbot.app"; }, From e0335b4bd01cc3f0504b71c5948de14e4d6f8c8a Mon Sep 17 00:00:00 2001 From: shangxue <1919892171@qq.com> Date: Tue, 9 Sep 2025 19:20:48 +0800 Subject: [PATCH 4/9] Update satori_adapter.py --- .../platform/sources/satori/satori_adapter.py | 203 +++++++++++++++--- 1 file changed, 174 insertions(+), 29 deletions(-) diff --git a/astrbot/core/platform/sources/satori/satori_adapter.py b/astrbot/core/platform/sources/satori/satori_adapter.py index 7fdeffbf4..e3b92c157 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,14 +318,58 @@ async def convert_satori_message( abm.self_id = login.get("user", {}).get("id", "") - content = message.get("content", "") - abm.message = await self.parse_satori_elements(content) + # 消息链 + abm.message = [] - # parse message_str - abm.message_str = "" + content = message.get("content", "") + + quote = message.get("quote") + content_for_parsing = content # 副本 + + # 提取标签 + if "]*>(.*?)', content, re.DOTALL) + if quote_match: + quote_id = quote_match.group(1) + quote_content = quote_match.group(2) + quote = { + "id": quote_id, + "content": quote_content + } + # 移除标签部分 + content_for_parsing = re.sub(r']*>.*?', '', content, flags=re.DOTALL) + + 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) + + abm.message_str = self._build_message_str(abm.message) for comp in abm.message: - if isinstance(comp, Plain): - abm.message_str += comp.text + if isinstance(comp, Reply): + abm.message_str += f"[引用消息(内容: {comp.message_str})] " # 优先使用Satori事件中的时间戳 if timestamp is not None: @@ -333,6 +383,59 @@ async def convert_satori_message( logger.error(f"转换 Satori 消息失败: {e}") return None + def _build_message_str(self, components: List[BaseMessageComponent]) -> str: + """构建消息文本表示""" + message_str = "" + for comp in components: + if isinstance(comp, Plain): + message_str += comp.text + elif isinstance(comp, Image): + message_str += "[图片]" + elif isinstance(comp, Record): + message_str += "[语音]" + elif isinstance(comp, File): + message_str += "[文件]" + elif isinstance(comp, At): + message_str += f"@{comp.name}" + return message_str + + 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 = self._build_message_str(quote_abm.message) + + 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 +444,32 @@ 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 +484,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 +500,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 bfdb03d3f85dc50a2a1a8b576b1d77f38e11313f Mon Sep 17 00:00:00 2001 From: shangxue <1919892171@qq.com> Date: Tue, 9 Sep 2025 19:20:57 +0800 Subject: [PATCH 5/9] Update satori_event.py --- astrbot/core/platform/sources/satori/satori_event.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/astrbot/core/platform/sources/satori/satori_event.py b/astrbot/core/platform/sources/satori/satori_event.py index f760f5716..90b031bcc 100644 --- a/astrbot/core/platform/sources/satori/satori_event.py +++ b/astrbot/core/platform/sources/satori/satori_event.py @@ -17,6 +17,15 @@ def __init__( session_id: str, adapter: "SatoriPlatformAdapter", ): + # 更新平台元数据 + if adapter and hasattr(adapter, 'logins') and adapter.logins: + current_login = adapter.logins[0] + platform_name = current_login.get("platform", "satori") + user = current_login.get("user", {}) + user_id = user.get("id", "") if user else "" + if not platform_meta.id and user_id: + platform_meta.id = f"{platform_name}({user_id})" + super().__init__(message_str, message_obj, platform_meta, session_id) self.adapter = adapter self.platform = None From 4c258760cc7ba7d6f85f05cb4311a4221773ef38 Mon Sep 17 00:00:00 2001 From: shangxue <1919892171@qq.com> Date: Tue, 9 Sep 2025 19:23:12 +0800 Subject: [PATCH 6/9] Update default.py --- astrbot/core/config/default.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 60a2dae78..9dbcbfa6a 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -252,7 +252,7 @@ "type": "satori", "enable": False, "satori_api_base_url": "http://localhost:5140/satori/v1", - "satori_endpoint": "ws://127.0.0.1:5140/satori/v1/events", + "satori_endpoint": "ws://localhost:5140/satori/v1/events", "satori_token": "", "satori_auto_reconnect": True, "satori_heartbeat_interval": 10, @@ -261,34 +261,34 @@ }, "items": { "satori_api_base_url": { - "description": "Satori API Base URL", + "description": "Satori API 终结点", "type": "string", - "hint": "The base URL for the Satori API.", + "hint": "Satori API 的基础地址。", }, "satori_endpoint": { - "description": "Satori WebSocket Endpoint", + "description": "Satori WebSocket 终结点", "type": "string", - "hint": "The WebSocket endpoint for Satori events.", + "hint": "Satori 事件的 WebSocket 端点。", }, "satori_token": { - "description": "Satori Token", + "description": "Satori 令牌", "type": "string", - "hint": "The token used for authenticating with the Satori API.", + "hint": "用于 Satori API 身份验证的令牌。", }, "satori_auto_reconnect": { - "description": "Enable Auto Reconnect", + "description": "启用自动重连", "type": "bool", - "hint": "Whether to automatically reconnect the WebSocket on disconnection.", + "hint": "断开连接时是否自动重新连接 WebSocket。", }, "satori_heartbeat_interval": { - "description": "Satori Heartbeat Interval", + "description": "Satori 心跳间隔", "type": "int", - "hint": "The interval (in seconds) for sending heartbeat messages.", + "hint": "发送心跳消息的间隔(秒)。", }, "satori_reconnect_delay": { - "description": "Satori Reconnect Delay", + "description": "Satori 重连延迟", "type": "int", - "hint": "The delay (in seconds) before attempting to reconnect.", + "hint": "尝试重新连接前的延迟时间(秒)。", }, "slack_connection_mode": { "description": "Slack Connection Mode", From ee7a1a6d776631429df3a5daf270739958d76dab Mon Sep 17 00:00:00 2001 From: shangxue <1919892171@qq.com> Date: Fri, 12 Sep 2025 13:28:01 +0800 Subject: [PATCH 7/9] Update satori_adapter.py --- .../platform/sources/satori/satori_adapter.py | 29 +++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/astrbot/core/platform/sources/satori/satori_adapter.py b/astrbot/core/platform/sources/satori/satori_adapter.py index e3b92c157..c99631c4d 100644 --- a/astrbot/core/platform/sources/satori/satori_adapter.py +++ b/astrbot/core/platform/sources/satori/satori_adapter.py @@ -366,10 +366,10 @@ async def convert_satori_message( content_elements = await self.parse_satori_elements(content_for_parsing) abm.message.extend(content_elements) - abm.message_str = self._build_message_str(abm.message) - for comp in abm.message: - if isinstance(comp, Reply): - abm.message_str += f"[引用消息(内容: {comp.message_str})] " + abm.message_str = "" + for comp in content_elements: + if isinstance(comp, Plain): + abm.message_str += comp.text # 优先使用Satori事件中的时间戳 if timestamp is not None: @@ -383,22 +383,6 @@ async def convert_satori_message( logger.error(f"转换 Satori 消息失败: {e}") return None - def _build_message_str(self, components: List[BaseMessageComponent]) -> str: - """构建消息文本表示""" - message_str = "" - for comp in components: - if isinstance(comp, Plain): - message_str += comp.text - elif isinstance(comp, Image): - message_str += "[图片]" - elif isinstance(comp, Record): - message_str += "[语音]" - elif isinstance(comp, File): - message_str += "[文件]" - elif isinstance(comp, At): - message_str += f"@{comp.name}" - return message_str - async def _convert_quote_message(self, quote: dict) -> Optional[AstrBotMessage]: """转换引用消息""" try: @@ -423,7 +407,10 @@ async def _convert_quote_message(self, quote: dict) -> Optional[AstrBotMessage]: quote_content = quote.get("content", "") quote_abm.message = await self.parse_satori_elements(quote_content) - quote_abm.message_str = self._build_message_str(quote_abm.message) + 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())) From da5ffb1d350cd59c3d19b6e69efac36ada685223 Mon Sep 17 00:00:00 2001 From: shangxue <1919892171@qq.com> Date: Tue, 16 Sep 2025 20:57:47 +0800 Subject: [PATCH 8/9] Update satori_adapter.py --- .../platform/sources/satori/satori_adapter.py | 141 ++++++++++++++++-- 1 file changed, 127 insertions(+), 14 deletions(-) diff --git a/astrbot/core/platform/sources/satori/satori_adapter.py b/astrbot/core/platform/sources/satori/satori_adapter.py index c99631c4d..330264a56 100644 --- a/astrbot/core/platform/sources/satori/satori_adapter.py +++ b/astrbot/core/platform/sources/satori/satori_adapter.py @@ -328,17 +328,13 @@ async def convert_satori_message( # 提取标签 if "]*>(.*?)', content, re.DOTALL) - if quote_match: - quote_id = quote_match.group(1) - quote_content = quote_match.group(2) - quote = { - "id": quote_id, - "content": quote_content - } - # 移除标签部分 - content_for_parsing = re.sub(r']*>.*?', '', content, flags=re.DOTALL) + try: + quote_info = await self._extract_quote_element(content) + if quote_info: + quote = quote_info["quote"] + content_for_parsing = quote_info["content_without_quote"] + except Exception as e: + logger.error(f"解析标签时发生错误: {e}, 错误内容: {content}") if quote: # 引用消息 @@ -383,6 +379,115 @@ 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: @@ -434,9 +539,7 @@ async def parse_satori_elements(self, content: str) -> list: # 处理命名空间前缀问题 processed_content = content if ':' in content and not content.startswith(' 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(): From 106ddde0e98f29ccaebeb0e3bcb1eacda80b3436 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sun, 21 Sep 2025 21:44:57 +0800 Subject: [PATCH 9/9] style: format code --- .../platform/sources/satori/satori_adapter.py | 143 ++++++++++-------- .../platform/sources/satori/satori_event.py | 4 +- 2 files changed, 84 insertions(+), 63 deletions(-) diff --git a/astrbot/core/platform/sources/satori/satori_adapter.py b/astrbot/core/platform/sources/satori/satori_adapter.py index 330264a56..efb756680 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, List +from typing import Optional from aiohttp import ClientSession, ClientTimeout from websockets.asyncio.client import ClientConnection from astrbot.api import logger @@ -17,7 +17,14 @@ register_platform_adapter, ) from astrbot.core.platform.astr_message_event import MessageSession -from astrbot.api.message_components import Plain, Image, At, File, Record, BaseMessageComponent, Reply +from astrbot.api.message_components import ( + Plain, + Image, + At, + File, + Record, + Reply, +) from xml.etree import ElementTree as ET @@ -47,7 +54,7 @@ def __init__( self.metadata = PlatformMetadata( name="satori", description="Satori 通用协议适配器", - id=self.config.get("id"), + id=self.config["id"], ) self.ws: Optional[ClientConnection] = None @@ -322,10 +329,10 @@ async def convert_satori_message( abm.message = [] content = message.get("content", "") - + quote = message.get("quote") content_for_parsing = content # 副本 - + # 提取标签 if " set: """提取XML内容中的命名空间前缀""" prefixes = set() - + # 查找所有标签 i = 0 while i < len(content): # 查找开始标签 - if content[i] == '<' and i + 1 < len(content) and content[i + 1] != '/': + if content[i] == "<" and i + 1 < len(content) and content[i + 1] != "/": # 找到标签结束位置 - tag_end = content.find('>', i) + tag_end = content.find(">", i) if tag_end != -1: # 提取标签内容 - tag_content = content[i + 1:tag_end] + tag_content = content[i + 1 : tag_end] # 检查是否有命名空间前缀 - if ':' in tag_content and 'xmlns:' not in tag_content: + 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 ":" in tag_name: + prefix = tag_name.split(":")[0] # 确保是有效的命名空间前缀 - if prefix.isalnum() or prefix.replace('_', '').isalnum(): + 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] == '/': + elif content[i] == "<" and i + 1 < len(content) and content[i + 1] == "/": # 找到标签结束位置 - tag_end = content.find('>', i) + tag_end = content.find(">", i) if tag_end != -1: # 提取标签内容 - tag_content = content[i + 2:tag_end] + tag_content = content[i + 2 : tag_end] # 检查是否有命名空间前缀 - if ':' in tag_content: - prefix = tag_content.split(':')[0] + if ":" in tag_content: + prefix = tag_content.split(":")[0] # 确保是有效的命名空间前缀 - if prefix.isalnum() or prefix.replace('_', '').isalnum(): + 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]: @@ -433,56 +443,61 @@ 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 "}" 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') + 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'), "") - + ET.tostring(quote_element, encoding="unicode", method="xml"), "" + ) + return { - "quote": { - "id": quote_id, - "content": inner_content - }, - "content_without_quote": content_without_quote + "quote": {"id": quote_id, "content": inner_content}, + "content_without_quote": content_without_quote, } - + return None except Exception as e: logger.error(f"提取标签时发生错误: {e}") @@ -493,7 +508,7 @@ 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: @@ -507,22 +522,22 @@ async def _convert_quote_message(self, quote: dict) -> Optional[AstrBotMessage]: 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}") @@ -538,19 +553,24 @@ async def parse_satori_elements(self, content: str) -> list: 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) await self._parse_xml_node(root, elements) except ET.ParseError as e: @@ -576,12 +596,12 @@ async def _parse_xml_node(self, node: ET.Element, elements: list) -> None: for child in node: # 获取标签名,去除命名空间前缀 tag_name = child.tag - if '}' in tag_name: - tag_name = tag_name.split('}')[1] + 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)) @@ -611,9 +631,8 @@ async def _parse_xml_node(self, node: ET.Element, elements: list) -> None: 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: @@ -628,6 +647,7 @@ async def _parse_xml_node(self, node: ET.Element, elements: list) -> None: data = attrs.get("data", "") if data: import html + decoded_data = html.unescape(data) elements.append(Plain(text=f"[ARK卡片数据: {decoded_data}]")) else: @@ -638,6 +658,7 @@ async def _parse_xml_node(self, node: ET.Element, elements: list) -> None: data = attrs.get("data", "") if data: import html + decoded_data = html.unescape(data) elements.append(Plain(text=f"[ARK卡片数据: {decoded_data}]")) else: diff --git a/astrbot/core/platform/sources/satori/satori_event.py b/astrbot/core/platform/sources/satori/satori_event.py index 90b031bcc..c90fa3af6 100644 --- a/astrbot/core/platform/sources/satori/satori_event.py +++ b/astrbot/core/platform/sources/satori/satori_event.py @@ -18,14 +18,14 @@ def __init__( adapter: "SatoriPlatformAdapter", ): # 更新平台元数据 - if adapter and hasattr(adapter, 'logins') and adapter.logins: + if adapter and hasattr(adapter, "logins") and adapter.logins: current_login = adapter.logins[0] platform_name = current_login.get("platform", "satori") user = current_login.get("user", {}) user_id = user.get("id", "") if user else "" if not platform_meta.id and user_id: platform_meta.id = f"{platform_name}({user_id})" - + super().__init__(message_str, message_obj, platform_meta, session_id) self.adapter = adapter self.platform = None