From 17aee086a321e9c8a38e96a97e7476d52538da87 Mon Sep 17 00:00:00 2001
From: shangxue <1919892171@qq.com>
Date: Sat, 6 Sep 2025 23:52:00 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Satori=20=E5=8D=8F?=
=?UTF-8?q?=E8=AE=AE=E9=80=82=E9=85=8D=E5=99=A8=E6=94=AF=E6=8C=81=20(#2633?=
=?UTF-8?q?)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Create satori_adapter.py
* Add files via upload
* Update default.py
* Update manager.py
* Update platform_adapter_type.py
* Update PlatformPage.vue
* Add files via upload
* Update default.py
* Update manager.py
* Update platform_adapter_type.py
* Update PlatformPage.vue
* Add files via upload
* Update default.py
* chore: format code
* feat: 修复 Image, Audio 的解析,修复 message_str 的解析
* perf: 增强鲁棒性
* feat: 添加 Satori 配置项描述,移除适配器默认配置
---------
Co-authored-by: Soulter <905617992@qq.com>
---
astrbot/core/config/default.py | 41 ++
astrbot/core/message/components.py | 4 +
astrbot/core/platform/manager.py | 2 +
.../platform/sources/satori/satori_adapter.py | 482 ++++++++++++++++++
.../platform/sources/satori/satori_event.py | 221 ++++++++
.../core/star/filter/platform_adapter_type.py | 3 +
.../assets/images/platform_logos/satori.png | Bin 0 -> 21508 bytes
dashboard/src/views/PlatformPage.vue | 5 +-
8 files changed, 757 insertions(+), 1 deletion(-)
create mode 100644 astrbot/core/platform/sources/satori/satori_adapter.py
create mode 100644 astrbot/core/platform/sources/satori/satori_event.py
create mode 100644 dashboard/src/assets/images/platform_logos/satori.png
diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py
index 0f0b26018..7d3d2f7d4 100644
--- a/astrbot/core/config/default.py
+++ b/astrbot/core/config/default.py
@@ -246,8 +246,49 @@ CONFIG_METADATA_2 = {
"slack_webhook_port": 6197,
"slack_webhook_path": "/astrbot-slack-webhook/callback",
},
+ "Satori": {
+ "id": "satori",
+ "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_token": "",
+ "satori_auto_reconnect": True,
+ "satori_heartbeat_interval": 10,
+ "satori_reconnect_delay": 5,
+ },
},
"items": {
+ "satori_api_base_url": {
+ "description": "Satori API Base URL",
+ "type": "string",
+ "hint": "The base URL for the Satori API.",
+ },
+ "satori_endpoint": {
+ "description": "Satori WebSocket Endpoint",
+ "type": "string",
+ "hint": "The WebSocket endpoint for Satori events.",
+ },
+ "satori_token": {
+ "description": "Satori Token",
+ "type": "string",
+ "hint": "The token used for authenticating with the Satori API.",
+ },
+ "satori_auto_reconnect": {
+ "description": "Enable Auto Reconnect",
+ "type": "bool",
+ "hint": "Whether to automatically reconnect the WebSocket on disconnection.",
+ },
+ "satori_heartbeat_interval": {
+ "description": "Satori Heartbeat Interval",
+ "type": "int",
+ "hint": "The interval (in seconds) for sending heartbeat messages.",
+ },
+ "satori_reconnect_delay": {
+ "description": "Satori Reconnect Delay",
+ "type": "int",
+ "hint": "The delay (in seconds) before attempting to reconnect.",
+ },
"slack_connection_mode": {
"description": "Slack Connection Mode",
"type": "string",
diff --git a/astrbot/core/message/components.py b/astrbot/core/message/components.py
index f02d492d0..d9ec4b41b 100644
--- a/astrbot/core/message/components.py
+++ b/astrbot/core/message/components.py
@@ -165,6 +165,10 @@ class Record(BaseMessageComponent):
return Record(file=url, **_)
raise Exception("not a valid url")
+ @staticmethod
+ def fromBase64(bs64_data: str, **_):
+ return Record(file=f"base64://{bs64_data}", **_)
+
async def convert_to_file_path(self) -> str:
"""将这个语音统一转换为本地文件路径。这个方法避免了手动判断语音数据类型,直接返回语音数据的本地路径(如果是网络 URL, 则会自动进行下载)。
diff --git a/astrbot/core/platform/manager.py b/astrbot/core/platform/manager.py
index 62328e881..6ac990e0e 100644
--- a/astrbot/core/platform/manager.py
+++ b/astrbot/core/platform/manager.py
@@ -85,6 +85,8 @@ class PlatformManager:
)
case "slack":
from .sources.slack.slack_adapter import SlackAdapter # noqa: F401
+ case "satori":
+ from .sources.satori.satori_adapter import SatoriPlatformAdapter # noqa: F401
except (ImportError, ModuleNotFoundError) as e:
logger.error(
f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->控制台->安装Pip库 中安装依赖库。"
diff --git a/astrbot/core/platform/sources/satori/satori_adapter.py b/astrbot/core/platform/sources/satori/satori_adapter.py
new file mode 100644
index 000000000..7fdeffbf4
--- /dev/null
+++ b/astrbot/core/platform/sources/satori/satori_adapter.py
@@ -0,0 +1,482 @@
+import asyncio
+import json
+import time
+import websockets
+from websockets.asyncio.client import connect
+from typing import Optional
+from aiohttp import ClientSession, ClientTimeout
+from websockets.asyncio.client import ClientConnection
+from astrbot.api import logger
+from astrbot.api.event import MessageChain
+from astrbot.api.platform import (
+ AstrBotMessage,
+ MessageMember,
+ MessageType,
+ Platform,
+ PlatformMetadata,
+ register_platform_adapter,
+)
+from astrbot.core.platform.astr_message_event import MessageSession
+from astrbot.api.message_components import Plain, Image, At, File, Record
+from xml.etree import ElementTree as ET
+
+
+@register_platform_adapter(
+ "satori",
+ "Satori 协议适配器",
+)
+class SatoriPlatformAdapter(Platform):
+ def __init__(
+ self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue
+ ) -> None:
+ super().__init__(event_queue)
+ self.config = platform_config
+ self.settings = platform_settings
+
+ self.api_base_url = self.config.get(
+ "satori_api_base_url", "http://localhost:5140/satori/v1"
+ )
+ self.token = self.config.get("satori_token", "")
+ self.endpoint = self.config.get(
+ "satori_endpoint", "ws://127.0.0.1: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.ws: Optional[ClientConnection] = None
+ self.session: Optional[ClientSession] = None
+ self.sequence = 0
+ self.logins = []
+ self.running = False
+ self.heartbeat_task: Optional[asyncio.Task] = None
+ self.ready_received = False
+
+ async def send_by_session(
+ self, session: MessageSession, message_chain: MessageChain
+ ):
+ from .satori_event import SatoriPlatformEvent
+
+ await SatoriPlatformEvent.send_with_adapter(
+ self, message_chain, session.session_id
+ )
+ await super().send_by_session(session, message_chain)
+
+ def meta(self) -> PlatformMetadata:
+ return PlatformMetadata(name="satori", description="Satori 通用协议适配器")
+
+ def _is_websocket_closed(self, ws) -> bool:
+ """检查WebSocket连接是否已关闭"""
+ if not ws:
+ return True
+ try:
+ if hasattr(ws, "closed"):
+ return ws.closed
+ elif hasattr(ws, "close_code"):
+ return ws.close_code is not None
+ else:
+ return False
+ except AttributeError:
+ return False
+
+ async def run(self):
+ self.running = True
+ self.session = ClientSession(timeout=ClientTimeout(total=30))
+
+ retry_count = 0
+ max_retries = 10
+
+ while self.running:
+ try:
+ await self.connect_websocket()
+ retry_count = 0
+ except websockets.exceptions.ConnectionClosed as e:
+ logger.warning(f"Satori WebSocket 连接关闭: {e}")
+ retry_count += 1
+ except Exception as e:
+ logger.error(f"Satori WebSocket 连接失败: {e}")
+ retry_count += 1
+
+ if not self.running:
+ break
+
+ if retry_count >= max_retries:
+ logger.error(f"达到最大重试次数 ({max_retries}),停止重试")
+ break
+
+ if not self.auto_reconnect:
+ break
+
+ delay = min(self.reconnect_delay * (2 ** (retry_count - 1)), 60)
+ await asyncio.sleep(delay)
+
+ if self.session:
+ await self.session.close()
+
+ async def connect_websocket(self):
+ logger.info(f"Satori 适配器正在连接到 WebSocket: {self.endpoint}")
+ logger.info(f"Satori 适配器 HTTP API 地址: {self.api_base_url}")
+
+ if not self.endpoint.startswith(("ws://", "wss://")):
+ logger.error(f"无效的WebSocket URL: {self.endpoint}")
+ raise ValueError(f"WebSocket URL必须以ws://或wss://开头: {self.endpoint}")
+
+ try:
+ websocket = await connect(self.endpoint, additional_headers={})
+ self.ws = websocket
+
+ await asyncio.sleep(0.1)
+
+ await self.send_identify()
+
+ self.heartbeat_task = asyncio.create_task(self.heartbeat_loop())
+
+ async for message in websocket:
+ try:
+ await self.handle_message(message) # type: ignore
+ except Exception as e:
+ logger.error(f"Satori 处理消息异常: {e}")
+
+ except websockets.exceptions.ConnectionClosed as e:
+ logger.warning(f"Satori WebSocket 连接关闭: {e}")
+ raise
+ except Exception as e:
+ logger.error(f"Satori WebSocket 连接异常: {e}")
+ raise
+ finally:
+ if self.heartbeat_task:
+ self.heartbeat_task.cancel()
+ try:
+ await self.heartbeat_task
+ except asyncio.CancelledError:
+ pass
+ if self.ws:
+ try:
+ await self.ws.close()
+ except Exception as e:
+ logger.error(f"Satori WebSocket 关闭异常: {e}")
+
+ async def send_identify(self):
+ if not self.ws:
+ raise Exception("WebSocket连接未建立")
+
+ if self._is_websocket_closed(self.ws):
+ raise Exception("WebSocket连接已关闭")
+
+ identify_payload = {
+ "op": 3, # IDENTIFY
+ "body": {
+ "token": str(self.token) if self.token else "", # 字符串
+ },
+ }
+
+ # 只有在有序列号时才添加sn字段
+ if self.sequence > 0:
+ identify_payload["body"]["sn"] = self.sequence
+
+ try:
+ message_str = json.dumps(identify_payload, ensure_ascii=False)
+ await self.ws.send(message_str)
+ except websockets.exceptions.ConnectionClosed as e:
+ logger.error(f"发送 IDENTIFY 信令时连接关闭: {e}")
+ raise
+ except Exception as e:
+ logger.error(f"发送 IDENTIFY 信令失败: {e}")
+ raise
+
+ async def heartbeat_loop(self):
+ try:
+ while self.running and self.ws:
+ await asyncio.sleep(self.heartbeat_interval)
+
+ 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))
+ except websockets.exceptions.ConnectionClosed as e:
+ logger.error(f"Satori WebSocket 连接关闭: {e}")
+ break
+ except Exception as e:
+ logger.error(f"Satori WebSocket 发送心跳失败: {e}")
+ break
+ else:
+ break
+ except asyncio.CancelledError:
+ pass
+ except Exception as e:
+ logger.error(f"心跳任务异常: {e}")
+
+ async def handle_message(self, message: str):
+ try:
+ data = json.loads(message)
+ op = data.get("op")
+ body = data.get("body", {})
+
+ if op == 4: # READY
+ self.logins = body.get("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
+ pass
+
+ elif op == 0: # 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"]
+
+ except json.JSONDecodeError as e:
+ logger.error(f"解析 WebSocket 消息失败: {e}, 消息内容: {message}")
+ except Exception as e:
+ logger.error(f"处理 WebSocket 消息异常: {e}")
+
+ async def handle_event(self, event_data: dict):
+ 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"):
+ return
+
+ abm = await self.convert_satori_message(
+ message, user, channel, guild, login, timestamp
+ )
+ if abm:
+ 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: Optional[dict],
+ login: dict,
+ timestamp: Optional[int] = None,
+ ) -> Optional[AstrBotMessage]:
+ try:
+ abm = AstrBotMessage()
+ abm.message_id = message.get("id", "")
+ abm.raw_message = {
+ "message": message,
+ "user": user,
+ "channel": channel,
+ "guild": guild,
+ "login": login,
+ }
+
+ if guild and guild.get("id"):
+ abm.type = MessageType.GROUP_MESSAGE
+ abm.group_id = guild.get("id", "")
+ abm.session_id = channel.get("id", "")
+ else:
+ abm.type = MessageType.FRIEND_MESSAGE
+ abm.session_id = channel.get("id", "")
+
+ abm.sender = MessageMember(
+ user_id=user.get("id", ""),
+ nickname=user.get("nick", user.get("name", "")),
+ )
+
+ abm.self_id = login.get("user", {}).get("id", "")
+
+ content = message.get("content", "")
+ abm.message = await self.parse_satori_elements(content)
+
+ # parse message_str
+ abm.message_str = ""
+ for comp in abm.message:
+ 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
+
+ async def parse_satori_elements(self, content: str) -> list:
+ """解析 Satori 消息元素"""
+ elements = []
+
+ if not content:
+ return elements
+
+ try:
+ wrapped_content = f"{content}"
+ root = ET.fromstring(wrapped_content)
+ await self._parse_xml_node(root, elements)
+ except ET.ParseError as e:
+ raise ValueError(f"解析 Satori 元素时发生解析错误: {e}")
+ except Exception as 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.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
+ 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]}")
+
+ elif tag_name == "file":
+ src = attrs.get("src", "")
+ name = attrs.get("name", "文件")
+ if src:
+ elements.append(File(file=src, name=name))
+
+ 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))
+ else:
+ logger.error(f"未知的音频 src 格式: {str(src)[:16]}")
+
+ 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))
+
+ async def handle_msg(self, message: AstrBotMessage):
+ from .satori_event import SatoriPlatformEvent
+
+ message_event = SatoriPlatformEvent(
+ message_str=message.message_str,
+ message_obj=message,
+ platform_meta=self.meta(),
+ session_id=message.session_id,
+ adapter=self,
+ )
+ self.commit_event(message_event)
+
+ async def send_http_request(
+ self,
+ method: str,
+ path: str,
+ data: dict | None = None,
+ platform: str | None = None,
+ user_id: str | None = None,
+ ) -> dict:
+ if not self.session:
+ raise Exception("HTTP session 未初始化")
+
+ headers = {
+ "Content-Type": "application/json",
+ }
+
+ if self.token:
+ headers["Authorization"] = f"Bearer {self.token}"
+
+ if platform and 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 ""
+
+ if not path.startswith("/"):
+ path = "/" + path
+
+ # 使用新的API地址配置
+ url = f"{self.api_base_url.rstrip('/')}{path}"
+
+ try:
+ async with self.session.request(
+ method, url, json=data, headers=headers
+ ) as response:
+ if response.status == 200:
+ result = await response.json()
+ return result
+ else:
+ return {}
+ except Exception as e:
+ logger.error(f"Satori HTTP 请求异常: {e}")
+ return {}
+
+ async def terminate(self):
+ self.running = False
+
+ if self.heartbeat_task:
+ self.heartbeat_task.cancel()
+
+ if self.ws:
+ try:
+ await self.ws.close()
+ except Exception as e:
+ logger.error(f"Satori WebSocket 关闭异常: {e}")
+
+ if self.session:
+ await self.session.close()
diff --git a/astrbot/core/platform/sources/satori/satori_event.py b/astrbot/core/platform/sources/satori/satori_event.py
new file mode 100644
index 000000000..f760f5716
--- /dev/null
+++ b/astrbot/core/platform/sources/satori/satori_event.py
@@ -0,0 +1,221 @@
+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
+
+if TYPE_CHECKING:
+ from .satori_adapter import SatoriPlatformAdapter
+
+
+class SatoriPlatformEvent(AstrMessageEvent):
+ def __init__(
+ self,
+ message_str: str,
+ message_obj: AstrBotMessage,
+ platform_meta: PlatformMetadata,
+ session_id: str,
+ adapter: "SatoriPlatformAdapter",
+ ):
+ 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)
+ ):
+ 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
+
+ @classmethod
+ async def send_with_adapter(
+ cls, adapter: "SatoriPlatformAdapter", message: MessageChain, session_id: str
+ ):
+ try:
+ content_parts = []
+
+ for component in message.chain:
+ if isinstance(component, Plain):
+ text = (
+ component.text.replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ )
+ content_parts.append(text)
+
+ elif isinstance(component, At):
+ if component.qq:
+ content_parts.append(f'')
+ elif component.name:
+ content_parts.append(f'')
+
+ elif isinstance(component, Image):
+ try:
+ image_base64 = await component.convert_to_base64()
+ if image_base64:
+ content_parts.append(
+ f'
'
+ )
+ except Exception as e:
+ logger.error(f"图片转换为base64失败: {e}")
+
+ elif isinstance(component, File):
+ content_parts.append(
+ f''
+ )
+
+ elif isinstance(component, Record):
+ try:
+ record_base64 = await component.convert_to_base64()
+ if record_base64:
+ content_parts.append(
+ f''
+ )
+ except Exception as e:
+ logger.error(f"语音转换为base64失败: {e}")
+
+ content = "".join(content_parts)
+ channel_id = session_id
+ data = {"channel_id": channel_id, "content": content}
+
+ platform = None
+ user_id = None
+
+ if hasattr(adapter, "logins") and 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 ""
+
+ result = await adapter.send_http_request(
+ "POST", "/message.create", data, platform, user_id
+ )
+ if result:
+ return result
+ else:
+ return None
+
+ except Exception as e:
+ logger.error(f"Satori 消息发送异常: {e}")
+ return None
+
+ async def send(self, message: MessageChain):
+ platform = getattr(self, "platform", None)
+ user_id = getattr(self, "user_id", None)
+
+ if not platform or not user_id:
+ if hasattr(self.adapter, "logins") and 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 ""
+
+ try:
+ content_parts = []
+
+ for component in message.chain:
+ if isinstance(component, Plain):
+ text = (
+ component.text.replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ )
+ content_parts.append(text)
+
+ elif isinstance(component, At):
+ if component.qq:
+ content_parts.append(f'')
+ elif component.name:
+ content_parts.append(f'')
+
+ elif isinstance(component, Image):
+ try:
+ image_base64 = await component.convert_to_base64()
+ if image_base64:
+ content_parts.append(
+ f'
'
+ )
+ except Exception as e:
+ logger.error(f"图片转换为base64失败: {e}")
+
+ elif isinstance(component, File):
+ content_parts.append(
+ f''
+ )
+
+ elif isinstance(component, Record):
+ try:
+ record_base64 = await component.convert_to_base64()
+ if record_base64:
+ content_parts.append(
+ f''
+ )
+ except Exception as e:
+ logger.error(f"语音转换为base64失败: {e}")
+
+ content = "".join(content_parts)
+ channel_id = self.session_id
+ data = {"channel_id": channel_id, "content": content}
+
+ result = await self.adapter.send_http_request(
+ "POST", "/message.create", data, platform, user_id
+ )
+ if not result:
+ logger.error("Satori 消息发送失败")
+ except Exception as e:
+ logger.error(f"Satori 消息发送异常: {e}")
+
+ await super().send(message)
+
+ async def send_streaming(self, generator, use_fallback: bool = False):
+ try:
+ content_parts = []
+
+ async for chain in generator:
+ if isinstance(chain, MessageChain):
+ if chain.type == "break":
+ if content_parts:
+ content = "".join(content_parts)
+ temp_chain = MessageChain([Plain(text=content)])
+ await self.send(temp_chain)
+ content_parts = []
+ continue
+
+ for component in chain.chain:
+ if isinstance(component, Plain):
+ content_parts.append(component.text)
+ elif isinstance(component, Image):
+ if content_parts:
+ content = "".join(content_parts)
+ temp_chain = MessageChain([Plain(text=content)])
+ await self.send(temp_chain)
+ content_parts = []
+ try:
+ image_base64 = await component.convert_to_base64()
+ if image_base64:
+ img_chain = MessageChain(
+ [
+ Plain(
+ text=f'
'
+ )
+ ]
+ )
+ await self.send(img_chain)
+ except Exception as e:
+ logger.error(f"图片转换为base64失败: {e}")
+ else:
+ content_parts.append(str(component))
+
+ if content_parts:
+ content = "".join(content_parts)
+ temp_chain = MessageChain([Plain(text=content)])
+ await self.send(temp_chain)
+
+ except Exception as e:
+ logger.error(f"Satori 流式消息发送异常: {e}")
+
+ return await super().send_streaming(generator, use_fallback)
diff --git a/astrbot/core/star/filter/platform_adapter_type.py b/astrbot/core/star/filter/platform_adapter_type.py
index 6c2d38572..1634001f3 100644
--- a/astrbot/core/star/filter/platform_adapter_type.py
+++ b/astrbot/core/star/filter/platform_adapter_type.py
@@ -18,6 +18,7 @@ class PlatformAdapterType(enum.Flag):
KOOK = enum.auto()
VOCECHAT = enum.auto()
WEIXIN_OFFICIAL_ACCOUNT = enum.auto()
+ SATORI = enum.auto()
ALL = (
AIOCQHTTP
| QQOFFICIAL
@@ -31,6 +32,7 @@ class PlatformAdapterType(enum.Flag):
| KOOK
| VOCECHAT
| WEIXIN_OFFICIAL_ACCOUNT
+ | SATORI
)
@@ -47,6 +49,7 @@ ADAPTER_NAME_2_TYPE = {
"wechatpadpro": PlatformAdapterType.WECHATPADPRO,
"vocechat": PlatformAdapterType.VOCECHAT,
"weixin_official_account": PlatformAdapterType.WEIXIN_OFFICIAL_ACCOUNT,
+ "satori": PlatformAdapterType.SATORI,
}
diff --git a/dashboard/src/assets/images/platform_logos/satori.png b/dashboard/src/assets/images/platform_logos/satori.png
new file mode 100644
index 0000000000000000000000000000000000000000..8e94f6097b155cac38040f446184362d675ce31d
GIT binary patch
literal 21508
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
literal 0
HcmV?d00001
diff --git a/dashboard/src/views/PlatformPage.vue b/dashboard/src/views/PlatformPage.vue
index b87538aa1..c29b0a3ee 100644
--- a/dashboard/src/views/PlatformPage.vue
+++ b/dashboard/src/views/PlatformPage.vue
@@ -279,6 +279,8 @@ export default {
return new URL('@/assets/images/platform_logos/kook.png', import.meta.url).href
} else if (name === 'vocechat') {
return new URL('@/assets/images/platform_logos/vocechat.png', import.meta.url).href
+ } else if (name === 'satori' || name === 'Satori') {
+ return new URL('@/assets/images/platform_logos/satori.png', import.meta.url).href
}
},
@@ -297,6 +299,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
}
return tutorial_map[platform_type] || "https://docs.astrbot.app";
},
@@ -532,4 +535,4 @@ export default {
font-weight: bold;
opacity: 0.3;
}
-
\ No newline at end of file
+