diff --git a/README.md b/README.md index 0592319c..11b7a2f4 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ docker compose up -d | Lark | ✅ | | | DingTalk | ✅ | | | KOOK | ✅ | | +| Satori | ✅ | | --- diff --git a/README_ES.md b/README_ES.md index 62c11b47..5c9289f0 100644 --- a/README_ES.md +++ b/README_ES.md @@ -89,6 +89,7 @@ docker compose up -d | Lark | ✅ | | | DingTalk | ✅ | | | KOOK | ✅ | | +| Satori | ✅ | | --- diff --git a/README_FR.md b/README_FR.md index e923df26..84c1c544 100644 --- a/README_FR.md +++ b/README_FR.md @@ -89,6 +89,7 @@ docker compose up -d | Lark | ✅ | | | DingTalk | ✅ | | | KOOK | ✅ | | +| Satori | ✅ | | --- diff --git a/README_JP.md b/README_JP.md index 91a6c4e4..55b1a04a 100644 --- a/README_JP.md +++ b/README_JP.md @@ -89,6 +89,7 @@ docker compose up -d | Lark | ✅ | | | DingTalk | ✅ | | | KOOK | ✅ | | +| Satori | ✅ | | --- diff --git a/README_KO.md b/README_KO.md index b8cf9795..13ecaee3 100644 --- a/README_KO.md +++ b/README_KO.md @@ -89,6 +89,7 @@ docker compose up -d | Lark | ✅ | | | DingTalk | ✅ | | | KOOK | ✅ | | +| Satori | ✅ | | --- diff --git a/README_RU.md b/README_RU.md index 3215357d..1203a8a3 100644 --- a/README_RU.md +++ b/README_RU.md @@ -89,6 +89,7 @@ docker compose up -d | Lark | ✅ | | | DingTalk | ✅ | | | KOOK | ✅ | | +| Satori | ✅ | | --- diff --git a/README_TW.md b/README_TW.md index e82928f5..01b2cc96 100644 --- a/README_TW.md +++ b/README_TW.md @@ -91,6 +91,7 @@ docker compose up -d | Slack | ✅ | | | LINE | ✅ | | | KOOK | ✅ | | +| Satori | ✅ | | --- diff --git a/README_VI.md b/README_VI.md index 1e568731..4bbd6cbb 100644 --- a/README_VI.md +++ b/README_VI.md @@ -89,6 +89,7 @@ docker compose up -d | Lark | ✅ | | | DingTalk | ✅ | | | KOOK | ✅ | | +| Satori | ✅ | | --- diff --git a/src/langbot/pkg/platform/sources/satori.png b/src/langbot/pkg/platform/sources/satori.png new file mode 100644 index 00000000..8a12cc6d Binary files /dev/null and b/src/langbot/pkg/platform/sources/satori.png differ diff --git a/src/langbot/pkg/platform/sources/satori.py b/src/langbot/pkg/platform/sources/satori.py new file mode 100644 index 00000000..6bfa342e --- /dev/null +++ b/src/langbot/pkg/platform/sources/satori.py @@ -0,0 +1,1090 @@ +from __future__ import annotations + +import typing +import time +import datetime +import json +import asyncio +import traceback +import re +import base64 + +import aiohttp +import pydantic +import websockets + +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger + + +class SatoriMessageConverter(abstract_platform_adapter.AbstractMessageConverter): + """Convert between LangBot MessageChain and Satori message format""" + + @staticmethod + async def yiri2target(message_chain: platform_message.MessageChain, adapter: 'SatoriAdapter') -> str: + """Convert LangBot MessageChain to Satori message format""" + content_parts = [] + + for component in message_chain: + if isinstance(component, platform_message.Plain): + text = component.text.replace('&', '&').replace('<', '<').replace('>', '>') + content_parts.append(text) + elif isinstance(component, platform_message.Image): + # Prefer URL over base64 to avoid buffer overflow issues with large images + if component.url: + content_parts.append(f'') + elif hasattr(component, 'base64') and component.base64: + # Process base64 data + base64_data = component.base64 + # Remove whitespace that might corrupt the data + base64_data = base64_data.replace('\n', '').replace('\r', '').replace(' ', '') + + # Check size - if too large, try to upload + MAX_INLINE_SIZE = 32 * 1024 # 32KB limit for inline base64 + + # Extract raw base64 and mime type + raw_b64 = base64_data + mime_type = 'image/png' + if base64_data.startswith('data:'): + try: + header, raw_b64 = base64_data.split(',', 1) + if ';' in header: + mime_type = header.split(':')[1].split(';')[0] + except (ValueError, IndexError): + pass + + if len(raw_b64) > MAX_INLINE_SIZE: + # Try to upload large image + try: + # Fix base64 padding if needed + padding = 4 - len(raw_b64) % 4 + if padding != 4: + raw_b64 += '=' * padding + image_bytes = base64.b64decode(raw_b64) + uploaded_url = await adapter.upload_image(image_bytes, mime_type) + if uploaded_url: + await adapter.logger.info(f'Satori 图片上传成功: {len(image_bytes)} 字节') + content_parts.append(f'') + else: + # Upload failed, use inline (may fail) + await adapter.logger.warning('Satori 图片上传失败,使用内联模式') + content_parts.append(f'') + except Exception as e: + await adapter.logger.error(f'Satori 图片处理失败: {e}') + content_parts.append(f'') + else: + # Small image, use inline + content_parts.append(f'') + elif isinstance(component, platform_message.At): + if component.target: + content_parts.append(f'') + elif isinstance(component, platform_message.AtAll): + content_parts.append('') + elif isinstance(component, platform_message.Reply): + content_parts.append(f'') + elif isinstance(component, platform_message.Quote): + content_parts.append(f'') + elif isinstance(component, platform_message.Face): + # Satori中的表情可以使用emoticon元素 + face_id = getattr(component, 'face_id', 'unknown') + content_parts.append(f'') + elif isinstance(component, platform_message.Voice): + if hasattr(component, 'url') and component.url: + content_parts.append(f'