From 3c5820e51e403fa780b361e2d19ae3212b37cf95 Mon Sep 17 00:00:00 2001 From: RF-Tar-Railt Date: Thu, 12 Mar 2026 11:11:21 +0800 Subject: [PATCH 1/4] feat: import `satori-python-core` --- .../platform/sources/satori/satori_adapter.py | 587 ++++++------------ pyproject.toml | 1 + requirements.txt | 1 + 3 files changed, 185 insertions(+), 404 deletions(-) diff --git a/astrbot/core/platform/sources/satori/satori_adapter.py b/astrbot/core/platform/sources/satori/satori_adapter.py index 5c2f7a37f3..6a49ec4b44 100644 --- a/astrbot/core/platform/sources/satori/satori_adapter.py +++ b/astrbot/core/platform/sources/satori/satori_adapter.py @@ -1,7 +1,9 @@ import asyncio import json +import re import time -from xml.etree import ElementTree as ET +from collections.abc import Sequence +from typing import cast import websockets from aiohttp import ClientSession, ClientTimeout @@ -11,14 +13,18 @@ from astrbot.api.event import MessageChain from astrbot.api.message_components import ( At, + AtAll, + Face, File, Image, Plain, Record, + Video, Reply, ) from astrbot.api.platform import ( AstrBotMessage, + Group, MessageMember, MessageType, Platform, @@ -26,7 +32,14 @@ register_platform_adapter, ) from astrbot.core.platform.astr_message_event import MessageSession +from satori import element +from satori.const import EventType +from satori.event import MessageEvent +from satori.model import Event, Opcode, Identify, Ready, Login +from satori.parser import parse +from satori.utils import decode, encode +b64_cap = re.compile(r"^data:([\w/.+-]+);base64,") @register_platform_adapter( "satori", "Satori 协议适配器", support_streaming_message=False @@ -64,7 +77,7 @@ def __init__( self.ws: ClientConnection | None = None self.session: ClientSession | None = None self.sequence = 0 - self.logins = [] + self.logins: Sequence[Login] = [] self.running = False self.heartbeat_task: asyncio.Task | None = None self.ready_received = False @@ -188,19 +201,13 @@ async def send_identify(self) -> None: if self._is_websocket_closed(self.ws): raise Exception("WebSocket连接已关闭") - identify_payload = { - "op": 3, # IDENTIFY - "body": { - "token": str(self.token) if self.token else "", # 字符串 - }, - } - + identify_payload = Identify(token=self.token) # 只有在有序列号时才添加sn字段 if self.sequence > 0: - identify_payload["body"]["sn"] = self.sequence + identify_payload.sn = self.sequence try: - message_str = json.dumps(identify_payload, ensure_ascii=False) + message_str = encode({"op": Opcode.IDENTIFY, "body": identify_payload.dump()}) await self.ws.send(message_str) except websockets.exceptions.ConnectionClosed as e: logger.error(f"发送 IDENTIFY 信令时连接关闭: {e}") @@ -216,11 +223,8 @@ async def heartbeat_loop(self) -> None: if self.ws and not self._is_websocket_closed(self.ws): try: - ping_payload = { - "op": 1, # PING - "body": {}, - } - await self.ws.send(json.dumps(ping_payload, ensure_ascii=False)) + ping_payload = {"op": Opcode.PING} + await self.ws.send(encode(ping_payload)) except websockets.exceptions.ConnectionClosed as e: logger.error(f"Satori WebSocket 连接关闭: {e}") break @@ -236,39 +240,33 @@ async def heartbeat_loop(self) -> None: async def handle_message(self, message: str) -> None: try: - data = json.loads(message) + data = decode(message) + op = data.get("op") body = data.get("body", {}) - if op == 4: # READY - self.logins = body.get("logins", []) + if op == Opcode.READY: + resp = Ready.parse(body) + self.logins = resp.logins self.ready_received = True # 输出连接成功的bot信息 - if self.logins: - for i, login in enumerate(self.logins): - platform = login.get("platform", "") - user = login.get("user", {}) - user_id = user.get("id", "") - user_name = user.get("name", "") - logger.info( - f"Satori 连接成功 - Bot {i + 1}: platform={platform}, user_id={user_id}, user_name={user_name}", - ) - - if "sn" in body: - self.sequence = body["sn"] - - elif op == 2: # PONG + for i, login in enumerate(self.logins): + logger.info( + f"Satori 连接成功 - Bot {i + 1}: " + f"platform={login.platform}, " + f"user_id={login.user.id if login.user else ''}, " + f"user_name={login.user.name if login.user else ''}", + ) + elif op == Opcode.PONG: pass - elif op == 0: # EVENT + elif op == Opcode.EVENT: # EVENT await self.handle_event(body) - if "sn" in body: - self.sequence = body["sn"] - elif op == 5: # META - if "sn" in body: - self.sequence = body["sn"] + elif op == Opcode.META: + # TODO: META 消息会携带 satori-server 支持的 proxy_urls, 用于资源链接的下载 + pass except json.JSONDecodeError as e: logger.error(f"解析 WebSocket 消息失败: {e}, 消息内容: {message}") @@ -277,93 +275,70 @@ async def handle_message(self, message: str) -> None: async def handle_event(self, event_data: dict) -> None: try: - event_type = event_data.get("type") - sn = event_data.get("sn") - if sn: - self.sequence = sn - - if event_type == "message-created": - message = event_data.get("message", {}) - user = event_data.get("user", {}) - channel = event_data.get("channel", {}) - guild = event_data.get("guild") - login = event_data.get("login", {}) - timestamp = event_data.get("timestamp") - - if user.get("id") == login.get("user", {}).get("id"): + event = Event.parse(event_data) + except Exception as e: + if ( + "self_id" in event_data + or ("login" in event_data and "self_id" in event_data["login"]) + or ("login" in event_data and "user" in event_data["login"] and "self_id" in event_data["login"]["user"]) + ): + logger.error(f"解析事件失败: {e}") + else: + logger.debug(f"解析事件失败: {e}") + else: + self.sequence = event.sn + if event.type == EventType.MESSAGE_CREATED: + if event.user and event.user.id == event.login.user.id: return - - abm = await self.convert_satori_message( - message, - user, - channel, - guild, - login, - timestamp, - ) - if abm: + if abm := await self.convert_satori_message(cast(MessageEvent, event)): await self.handle_msg(abm) - except Exception as e: - logger.error(f"处理事件失败: {e}") - - async def convert_satori_message( - self, - message: dict, - user: dict, - channel: dict, - guild: dict | None, - login: dict, - timestamp: int | None = None, - ) -> AstrBotMessage | None: + async def convert_satori_message(self, event: MessageEvent) -> AstrBotMessage | None: try: abm = AstrBotMessage() - abm.message_id = message.get("id", "") + abm.message_id = event.message.id + abm.timestamp = int(event.timestamp.timestamp()) abm.raw_message = { - "message": message, - "user": user, - "channel": channel, - "guild": guild, - "login": login, + "message": event.message.dump(), + "user": event.user.dump(), + "channel": event.channel.dump(), + "guild": event.guild.dump() if event.guild else None, + "login": event.login.dump(), } - - if guild and guild.get("id"): - abm.type = MessageType.GROUP_MESSAGE - abm.group_id = guild.get("id", "") - abm.session_id = channel.get("id", "") - else: + channel_id = event.channel.id + if channel_id.startswith("private:"): abm.type = MessageType.FRIEND_MESSAGE - abm.session_id = channel.get("id", "") + abm.session_id = channel_id + else: + abm.type = MessageType.GROUP_MESSAGE + abm.group = Group( + group_id=channel_id, + group_name=event.channel.name, + group_avatar=event.guild.avatar if event.guild else None, + ) + if event.guild and event.guild.id != channel_id: # 二级频道 + abm.session_id = f"{event.guild.id}:{channel_id}" + else: # 一级群组 + abm.session_id = channel_id abm.sender = MessageMember( - user_id=user.get("id", ""), - nickname=user.get("nick", user.get("name", "")), + user_id=event.user.id, + nickname=event.user.nick or event.user.name or "", ) - - abm.self_id = login.get("user", {}).get("id", "") - + abm.self_id = event.login.user.id # 消息链 abm.message = [] - content = message.get("content", "") - - quote = message.get("quote") - content_for_parsing = content # 副本 - - # 提取标签 - if "标签时发生错误: {e}, 错误内容: {content}") - + elements = event.message.message + if raw_quote := event.message._raw_data.get("quote"): + quote: element.Quote | None = element.transform([raw_quote])[0] # type: ignore + elif quotes := element.select(elements, element.Quote): + quote = quotes[0] + else: + quote = None if quote: - # 引用消息 - quote_abm = await self._convert_quote_message(quote) - if quote_abm: + elements = [e for e in elements if not isinstance(e, element.Quote)] + if quote_abm := self._convert_quote_message(quote, abm.self_id): sender_id = quote_abm.sender.user_id if isinstance(sender_id, str) and sender_id.isdigit(): sender_id = int(sender_id) @@ -381,204 +356,52 @@ async def convert_satori_message( qq=sender_id, ) abm.message.append(reply_component) - + # 解析消息内容 - content_elements = await self.parse_satori_elements(content_for_parsing) + content_elements = self.parse_satori_elements(elements) abm.message.extend(content_elements) abm.message_str = "" for comp in content_elements: if isinstance(comp, Plain): abm.message_str += comp.text - - # 优先使用Satori事件中的时间戳 - if timestamp is not None: - abm.timestamp = timestamp - else: - abm.timestamp = int(time.time()) - return abm except Exception as e: 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) -> dict | None: - """提取标签信息""" - 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 ET.ParseError as e: - logger.warning(f"XML解析失败,使用正则提取: {e}") - return await self._extract_quote_with_regex(content) - except Exception as e: - logger.error(f"提取标签时发生错误: {e}") - return None - - async def _extract_quote_with_regex(self, content: str) -> dict | None: - """使用正则表达式提取quote标签信息""" - import re - - quote_pattern = r"]*)>(.*?)" - 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) -> AstrBotMessage | None: + def _convert_quote_message(self, quote: element.Quote, self_id: str) -> AstrBotMessage | None: """转换引用消息""" try: quote_abm = AstrBotMessage() - quote_abm.message_id = quote.get("id", "") + quote_abm.message_id = quote.id or "" # 解析引用消息的发送者 - quote_author = quote.get("author", {}) - if quote_author: + quote_authors = element.select(quote, element.Author) + if quote_authors: + quote_author = quote_authors[0] quote_abm.sender = MessageMember( - user_id=quote_author.get("id", ""), - nickname=quote_author.get("nick", quote_author.get("name", "")), + user_id=quote_author.id, + nickname=quote_author.name or "", ) else: # 如果没有作者信息,使用默认值 quote_abm.sender = MessageMember( - user_id=quote.get("user_id", ""), + user_id=self_id, nickname="内容", ) # 解析引用消息内容 - quote_content = quote.get("content", "") - quote_abm.message = await self.parse_satori_elements(quote_content) + quote_abm.message = self.parse_satori_elements(quote.children) 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())) + quote_abm.timestamp = int(time.time()) # 如果没有任何内容,使用默认文本 if not quote_abm.message_str.strip(): @@ -589,136 +412,93 @@ async def _convert_quote_message(self, quote: dict) -> AstrBotMessage | None: logger.error(f"转换引用消息失败: {e}") return None - async def parse_satori_elements(self, content: str) -> list: + def parse_satori_elements(self, elements: list[element.Element]) -> list: """解析 Satori 消息元素""" - elements = [] - - if not content: - return elements - - 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: - logger.warning(f"解析 Satori 元素时发生解析错误: {e}, 错误内容: {content}") - # 如果解析失败,将整个内容当作纯文本 - if content.strip(): - elements.append(Plain(text=content)) - except Exception as e: - logger.error(f"解析 Satori 元素时发生未知错误: {e}") - raise e - - # 如果没有解析到任何元素,将整个内容当作纯文本 - if not elements and content.strip(): - elements.append(Plain(text=content)) - - return elements - - async def _parse_xml_node(self, node: ET.Element, elements: list) -> None: - """递归解析 XML 节点""" - if node.text and node.text.strip(): - elements.append(Plain(text=node.text)) - - for child in node: - # 获取标签名,去除命名空间前缀 - 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)) - - elif tag_name in ("img", "image"): - src = attrs.get("src", "") - if not src: - continue - elements.append(Image(file=src)) - - elif tag_name == "file": - src = attrs.get("src", "") - name = attrs.get("name", "文件") - if src: - elements.append(File(name=name, file=src)) - - elif tag_name in ("audio", "record"): - src = attrs.get("src", "") - if not src: - continue - 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_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}]")) + if item.href: + parsed_elements.append(Plain(text=f" ({item.href})")) + elif isinstance(item, element.Br): + parsed_elements.append(Plain(text="\n")) + elif isinstance(item, element.Paragraph): + prev = parsed_elements[-1] if parsed_elements else None + if prev and isinstance(prev, Plain): + if not prev.text.endswith("\n"): + prev.text += "\n" 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}]")) + parsed_elements.append(Plain(text="\n")) + parsed_elements.extend( + self.parse_satori_elements(item.children) + ) + parsed_elements.append(Plain(text="\n")) + elif isinstance(item, element.At): + if item.type: + parsed_elements.append(AtAll()) 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}]")) + user_id = item.id or "" + parsed_elements.append(At(qq=user_id, name=item.name or user_id)) + elif isinstance(item, element.Image): + file = item.src + if mat := b64_cap.match(item.src): + file = f"base64://{item.src[len(mat[0]):]}" + parsed_elements.append(Image(file=file)) + elif isinstance(item, element.File): + file = item.src + if mat := b64_cap.match(item.src): + file = f"base64://{item.src[len(mat[0]):]}" + parsed_elements.append(File(name=item.title or "文件", file=file)) + elif isinstance(item, element.Audio): + file = item.src + if mat := b64_cap.match(item.src): + file = f"base64://{item.src[len(mat[0]):]}" + parsed_elements.append(Record(file=file)) + elif isinstance(item, element.Video): + file = item.src + if mat := b64_cap.match(item.src): + file = f"base64://{item.src[len(mat[0]):]}" + parsed_elements.append(Video(file=file)) + elif isinstance(item, element.Emoji): + if item.name: + parsed_elements.append(Plain(text=f"[表情:{item.name}]")) else: - elements.append(Plain(text="[JSON卡片]")) - + parsed_elements.append(Face(id=item.id)) + elif isinstance(item, element.Custom): + if item.tag == "ark": + data = item._attrs.get("data", "") + if data: + import html + + decoded_data = html.unescape(data) + parsed_elements.append(Plain(text=f"[ARK卡片数据: {decoded_data}]")) + else: + parsed_elements.append(Plain(text="[ARK卡片]")) + elif item.tag == "json": + data = item._attrs.get("data", "") + if data: + import html + + decoded_data = html.unescape(data) + parsed_elements.append(Plain(text=f"[JSON卡片数据: {decoded_data}]")) + else: + parsed_elements.append(Plain(text="[JSON卡片]")) + else: + parsed_elements.extend( + self.parse_satori_elements(item.children) + ) else: - # 未知标签,递归处理其内容 - if child.text and child.text.strip(): - elements.append(Plain(text=child.text)) - await self._parse_xml_node(child, elements) - - # 处理标签后的文本 - if child.tail and child.tail.strip(): - elements.append(Plain(text=child.tail)) + parsed_elements.extend( + self.parse_satori_elements(item.children) + ) + return parsed_elements async def handle_msg(self, message: AstrBotMessage) -> None: from .satori_event import SatoriPlatformEvent @@ -751,13 +531,12 @@ async def send_http_request( headers["Authorization"] = f"Bearer {self.token}" if platform and user_id: - headers["satori-platform"] = platform - headers["satori-user-id"] = user_id + headers["Satori-Platform"] = platform + headers["Satori-User-Id"] = user_id elif self.logins: current_login = self.logins[0] - headers["satori-platform"] = current_login.get("platform", "") - user = current_login.get("user", {}) - headers["satori-user-id"] = user.get("id", "") if user else "" + headers["Satori-Platform"] = current_login.platform + headers["Satori-User-Id"] = current_login.user.id if current_login.user else "" if not path.startswith("/"): path = "/" + path diff --git a/pyproject.toml b/pyproject.toml index 787af9ee76..34552c310c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ dependencies = [ "qq-botpy>=1.2.1", "quart>=0.20.0", "readability-lxml>=0.8.4.1", + "satori-python-core>=1.3.0", "silk-python>=0.2.6", "slack-sdk>=3.35.0", "sqlalchemy[asyncio]>=2.0.41", diff --git a/requirements.txt b/requirements.txt index 49e0707e09..3d32c88cfa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,6 +36,7 @@ python-telegram-bot>=22.6 qq-botpy>=1.2.1 quart>=0.20.0 readability-lxml>=0.8.4.1 +satori-python-core>=1.3.0 silk-python>=0.2.6 slack-sdk>=3.35.0 sqlalchemy[asyncio]>=2.0.41 From b9b8eebdb14360b137407a98de495f5f975339ec Mon Sep 17 00:00:00 2001 From: RF-Tar-Railt Date: Thu, 12 Mar 2026 11:38:55 +0800 Subject: [PATCH 2/4] feat: complete adapt satori_event.py --- .../platform/sources/satori/satori_adapter.py | 5 + .../platform/sources/satori/satori_event.py | 282 +++++------------- 2 files changed, 78 insertions(+), 209 deletions(-) diff --git a/astrbot/core/platform/sources/satori/satori_adapter.py b/astrbot/core/platform/sources/satori/satori_adapter.py index 6a49ec4b44..8a834a8710 100644 --- a/astrbot/core/platform/sources/satori/satori_adapter.py +++ b/astrbot/core/platform/sources/satori/satori_adapter.py @@ -86,6 +86,7 @@ async def send_by_session( self, session: MessageSession, message_chain: MessageChain, + referrer: dict | None = None, ) -> None: from .satori_event import SatoriPlatformEvent @@ -93,6 +94,7 @@ async def send_by_session( self, message_chain, session.session_id, + referrer=referrer, ) await super().send_by_session(session, message_chain) @@ -299,11 +301,14 @@ async def convert_satori_message(self, event: MessageEvent) -> AstrBotMessage | abm.message_id = event.message.id abm.timestamp = int(event.timestamp.timestamp()) abm.raw_message = { + "type": event._type, + "data": event._data, "message": event.message.dump(), "user": event.user.dump(), "channel": event.channel.dump(), "guild": event.guild.dump() if event.guild else None, "login": event.login.dump(), + "referrer": event.referrer, } channel_id = event.channel.id if channel_id.startswith("private:"): diff --git a/astrbot/core/platform/sources/satori/satori_event.py b/astrbot/core/platform/sources/satori/satori_event.py index 0214222837..ffcf63eb2c 100644 --- a/astrbot/core/platform/sources/satori/satori_event.py +++ b/astrbot/core/platform/sources/satori/satori_event.py @@ -15,6 +15,10 @@ Video, ) from astrbot.api.platform import AstrBotMessage, PlatformMetadata +from satori import E, Element +from satori.const import Api + +from astrbot.core.message.components import AtAll, Face if TYPE_CHECKING: from .satori_adapter import SatoriPlatformAdapter @@ -30,27 +34,23 @@ def __init__( adapter: "SatoriPlatformAdapter", ) -> None: # 更新平台元数据 - 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})" + current_login = adapter.logins[0] + platform_name = current_login.platform or "satori" + user_id = current_login.user.id if current_login.user else None + 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 self.user_id = None - if ( - hasattr(message_obj, "raw_message") - and message_obj.raw_message - and isinstance(message_obj.raw_message, dict) - ): + self.referrer = None + if isinstance(message_obj.raw_message, dict): login = message_obj.raw_message.get("login", {}) self.platform = login.get("platform") user = login.get("user", {}) self.user_id = user.get("id") if user else None + self.referrer = message_obj.raw_message.get("referrer") @classmethod async def send_with_adapter( @@ -58,46 +58,43 @@ async def send_with_adapter( adapter: "SatoriPlatformAdapter", message: MessageChain, session_id: str, + referrer: dict | None = None, ): try: content_parts = [] for component in message.chain: - component_content = await cls._convert_component_to_satori_static( + component_content = await cls._convert_component_to_satori( component, ) - if component_content: - content_parts.append(component_content) + content_parts.append(component_content) # 特殊处理 Node 和 Nodes 组件 if isinstance(component, Node): # 单个转发节点 - node_content = await cls._convert_node_to_satori_static(component) - if node_content: - content_parts.append(node_content) + node_content = await cls._convert_node_to_satori(component) + content_parts.append(node_content) elif isinstance(component, Nodes): # 合并转发消息 - node_content = await cls._convert_nodes_to_satori_static(component) - if node_content: - content_parts.append(node_content) + node_content = await cls._convert_nodes_to_satori(component) + content_parts.append(node_content) - content = "".join(content_parts) + content = "".join(str(i) for i in content_parts) channel_id = session_id - data = {"channel_id": channel_id, "content": content} + data = {"channel_id": channel_id, "content": content, "referrer": referrer} platform = None user_id = None - if hasattr(adapter, "logins") and adapter.logins: + if adapter.logins: current_login = adapter.logins[0] - platform = current_login.get("platform", "") - user = current_login.get("user", {}) - user_id = user.get("id", "") if user else "" + platform = current_login.platform or "satori" + user_id = current_login.user.id if current_login.user else None result = await adapter.send_http_request( "POST", - "/message.create", + Api.MESSAGE_CREATE, data, platform, user_id, @@ -115,40 +112,36 @@ async def send(self, message: MessageChain) -> None: user_id = getattr(self, "user_id", None) if not platform or not user_id: - if hasattr(self.adapter, "logins") and self.adapter.logins: + if self.adapter.logins: current_login = self.adapter.logins[0] - platform = current_login.get("platform", "") - user = current_login.get("user", {}) - user_id = user.get("id", "") if user else "" + platform = current_login.platform or "satori" + user_id = current_login.user.id if current_login.user else None try: content_parts = [] for component in message.chain: component_content = await self._convert_component_to_satori(component) - if component_content: - content_parts.append(component_content) + content_parts.append(component_content) # 特殊处理 Node 和 Nodes 组件 if isinstance(component, Node): # 单个转发节点 node_content = await self._convert_node_to_satori(component) - if node_content: - content_parts.append(node_content) + content_parts.append(node_content) elif isinstance(component, Nodes): # 合并转发消息 node_content = await self._convert_nodes_to_satori(component) - if node_content: - content_parts.append(node_content) + content_parts.append(node_content) - content = "".join(content_parts) + content = "".join(str(i) for i in content_parts) channel_id = self.session_id - data = {"channel_id": channel_id, "content": content} + data = {"channel_id": channel_id, "content": content, "referrer": self.referrer} result = await self.adapter.send_http_request( "POST", - "/message.create", + Api.MESSAGE_CREATE, data, platform, user_id, @@ -209,224 +202,95 @@ async def send_streaming(self, generator, use_fallback: bool = False): 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 - - if isinstance(component, At): - if component.qq: - return f'' - if component.name: - return f'' - - elif isinstance(component, Image): - try: - image_base64 = await component.convert_to_base64() - if image_base64: - return f'' - except Exception as e: - logger.error(f"图片转换为base64失败: {e}") - - elif isinstance(component, File): - return ( - f'' - ) - - elif isinstance(component, Record): - try: - record_base64 = await component.convert_to_base64() - if record_base64: - return f'