feat: migrate aiocqhttp adapter to eba

This commit is contained in:
Junyan Qin
2026-05-10 17:41:06 +08:00
parent 57f2e85388
commit c55db54fd2
14 changed files with 2260 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
from __future__ import annotations
from langbot.pkg.platform.adapters.aiocqhttp.adapter import AiocqhttpAdapter
__all__ = ['AiocqhttpAdapter']

View File

@@ -0,0 +1,172 @@
from __future__ import annotations
import asyncio
import traceback
import typing
import aiocqhttp
import pydantic
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger
from langbot.pkg.platform.adapters.aiocqhttp.api_impl import AiocqhttpAPIMixin
from langbot.pkg.platform.adapters.aiocqhttp.event_converter import AiocqhttpEventConverter
from langbot.pkg.platform.adapters.aiocqhttp.message_converter import AiocqhttpMessageConverter
from langbot.pkg.platform.adapters.aiocqhttp.platform_api import PLATFORM_API_MAP
from langbot_plugin.api.entities.builtin.platform import events as platform_events
from langbot_plugin.api.entities.builtin.platform.errors import NotSupportedError
class AiocqhttpAdapter(AiocqhttpAPIMixin, abstract_platform_adapter.AbstractPlatformAdapter):
bot: aiocqhttp.CQHttp = pydantic.Field(exclude=True)
message_converter: AiocqhttpMessageConverter = AiocqhttpMessageConverter()
event_converter: AiocqhttpEventConverter = AiocqhttpEventConverter()
config: dict
listeners: dict[
typing.Type[platform_events.Event],
typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None],
] = {}
class Config:
arbitrary_types_allowed = True
def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger):
run_config = dict(config)
async def shutdown_trigger_placeholder():
while True:
await asyncio.sleep(1)
run_config['shutdown_trigger'] = shutdown_trigger_placeholder
access_token = run_config.pop('access-token', '') or None
bot = aiocqhttp.CQHttp(access_token=access_token)
super().__init__(
config=run_config,
logger=logger,
bot=bot,
bot_account_id='',
listeners={},
)
self._register_native_handlers()
def get_supported_events(self) -> list[str]:
return [
'message.received',
'message.deleted',
'group.member_joined',
'group.member_left',
'group.member_banned',
'friend.request_received',
'friend.added',
'bot.invited_to_group',
'bot.removed_from_group',
'bot.muted',
'bot.unmuted',
'platform.specific',
]
def get_supported_apis(self) -> list[str]:
return [
'send_message',
'reply_message',
'delete_message',
'forward_message',
'get_message',
'get_group_info',
'get_group_list',
'get_group_member_list',
'get_group_member_info',
'set_group_name',
'get_user_info',
'get_friend_list',
'approve_friend_request',
'approve_group_invite',
'mute_member',
'unmute_member',
'kick_member',
'leave_group',
'call_platform_api',
]
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
handler = PLATFORM_API_MAP.get(action)
if handler is None:
raise NotSupportedError(f'call_platform_api:{action}')
return await handler(self.bot, params)
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
],
):
self.listeners[event_type] = callback
def unregister_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
],
):
registered = self.listeners.get(event_type)
if registered is callback:
self.listeners.pop(event_type, None)
async def run_async(self):
await self.bot._server_app.run_task(**self.config)
async def kill(self) -> bool:
return False
def _register_native_handlers(self):
@self.bot.on_message()
async def on_message(event: aiocqhttp.Event):
await self._handle_native_event(event)
@self.bot.on_notice()
async def on_notice(event: aiocqhttp.Event):
await self._handle_native_event(event)
@self.bot.on_request()
async def on_request(event: aiocqhttp.Event):
await self._handle_native_event(event)
@self.bot.on_websocket_connection
async def on_websocket_connection(event: aiocqhttp.Event):
self.bot_account_id = str(getattr(event, 'self_id', '') or self.bot_account_id)
await self.logger.info(f'WebSocket connection established, bot id: {self.bot_account_id}')
await self._dispatch_native_event(event)
async def _handle_native_event(self, event: aiocqhttp.Event):
self.bot_account_id = str(getattr(event, 'self_id', '') or self.bot_account_id)
if getattr(event, 'type', None) == 'message' and str(getattr(event, 'user_id', '')) == self.bot_account_id:
return
try:
if getattr(event, 'type', None) == 'message' and (
platform_events.FriendMessage in self.listeners or platform_events.GroupMessage in self.listeners
):
legacy_event = await self.event_converter.target2legacy(event, self.bot)
if legacy_event:
callback = self.listeners.get(type(legacy_event))
if callback:
await callback(legacy_event, self)
await self._dispatch_native_event(event)
except Exception:
await self.logger.error(f'Error in aiocqhttp native event: {traceback.format_exc()}')
async def _dispatch_native_event(self, event: aiocqhttp.Event):
eba_event = await self.event_converter.target2yiri(event, self.bot, self.bot_account_id)
if eba_event:
await self._dispatch_eba_event(eba_event)
async def _dispatch_eba_event(self, event: platform_events.EBAEvent):
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
callback = self.listeners.get(event_type)
if callback:
await callback(event, self)
return

View File

@@ -0,0 +1,238 @@
from __future__ import annotations
import typing
import aiocqhttp
from langbot.pkg.platform.adapters.aiocqhttp.event_converter import AiocqhttpEventConverter
from langbot.pkg.platform.adapters.aiocqhttp.message_converter import AiocqhttpMessageConverter
from langbot_plugin.api.entities.builtin.platform import entities as platform_entities
from langbot_plugin.api.entities.builtin.platform import events as platform_events
from langbot_plugin.api.entities.builtin.platform import message as platform_message
from langbot_plugin.api.entities.builtin.platform.errors import NotSupportedError
class AiocqhttpAPIMixin:
bot: aiocqhttp.CQHttp
async def send_message(
self,
target_type: str,
target_id: str,
message: platform_message.MessageChain,
) -> platform_events.MessageResult:
forward = message.get_first(platform_message.Forward)
if forward and target_type == 'group':
raw = await self._send_forward_message(int(target_id), typing.cast(platform_message.Forward, forward))
return platform_events.MessageResult(message_id=raw.get('message_id'), raw=raw)
aiocq_msg, _, _ = await AiocqhttpMessageConverter.yiri2target(message)
if target_type == 'group':
raw = await self.bot.send_group_msg(group_id=int(target_id), message=aiocq_msg)
elif target_type in ('person', 'private'):
raw = await self.bot.send_private_msg(user_id=int(target_id), message=aiocq_msg)
else:
raise ValueError(f'Unsupported aiocqhttp target_type: {target_type}')
return platform_events.MessageResult(message_id=(raw or {}).get('message_id'), raw=raw or {})
async def reply_message(
self,
message_source: platform_events.MessageEvent,
message: platform_message.MessageChain,
quote_origin: bool = False,
) -> platform_events.MessageResult:
assert isinstance(message_source.source_platform_object, aiocqhttp.Event)
aiocq_msg, _, _ = await AiocqhttpMessageConverter.yiri2target(message)
if quote_origin:
source_id = getattr(message_source, 'message_id', None) or message_source.message_chain.message_id
aiocq_msg = aiocqhttp.MessageSegment.reply(source_id) + aiocq_msg
raw = await self.bot.send(message_source.source_platform_object, aiocq_msg)
return platform_events.MessageResult(message_id=(raw or {}).get('message_id'), raw=raw or {})
async def delete_message(
self,
chat_type: str,
chat_id: typing.Union[int, str],
message_id: typing.Union[int, str],
) -> None:
await self.bot.delete_msg(message_id=int(message_id))
async def forward_message(
self,
from_chat_type: str,
from_chat_id: typing.Union[int, str],
message_id: typing.Union[int, str],
to_chat_type: str,
to_chat_id: typing.Union[int, str],
) -> platform_events.MessageResult:
raw_message = await self.bot.get_msg(message_id=int(message_id))
target_message = aiocqhttp.Message(raw_message.get('message', []))
if to_chat_type == 'group':
raw = await self.bot.send_group_msg(group_id=int(to_chat_id), message=target_message)
elif to_chat_type in ('person', 'private'):
raw = await self.bot.send_private_msg(user_id=int(to_chat_id), message=target_message)
else:
raise ValueError(f'Unsupported aiocqhttp to_chat_type: {to_chat_type}')
return platform_events.MessageResult(message_id=(raw or {}).get('message_id'), raw=raw or {})
async def get_message(
self,
chat_type: str,
chat_id: typing.Union[int, str],
message_id: typing.Union[int, str],
) -> platform_events.MessageReceivedEvent:
raw = await self.bot.get_msg(message_id=int(message_id))
message_type = raw.get('message_type') or chat_type
event = aiocqhttp.Event.from_payload(
{
'post_type': 'message',
'message_type': 'group' if message_type == 'group' else 'private',
'sub_type': raw.get('sub_type', 'normal'),
'time': raw.get('time', 0),
'self_id': self.bot_account_id or 0,
'message_id': raw.get('message_id', message_id),
'user_id': raw.get('sender', {}).get('user_id') or raw.get('user_id') or chat_id,
'group_id': raw.get('group_id') or (chat_id if message_type == 'group' else None),
'message': raw.get('message', []),
'raw_message': raw.get('raw_message', ''),
'sender': raw.get('sender', {}),
}
)
return await AiocqhttpEventConverter.message_to_eba(event, self.bot)
async def get_group_info(self, group_id: typing.Union[int, str]) -> platform_entities.UserGroup:
raw = await self.bot.get_group_info(group_id=int(group_id))
return platform_entities.UserGroup(
id=raw.get('group_id', group_id),
name=raw.get('group_name', ''),
member_count=raw.get('member_count'),
)
async def get_group_list(self) -> list[platform_entities.UserGroup]:
raw_list = await self.bot.get_group_list()
return [
platform_entities.UserGroup(
id=item.get('group_id', ''),
name=item.get('group_name', ''),
member_count=item.get('member_count'),
)
for item in raw_list
]
async def get_group_member_list(
self,
group_id: typing.Union[int, str],
) -> list[platform_entities.UserGroupMember]:
raw_list = await self.bot.get_group_member_list(group_id=int(group_id))
return [self._member_to_entity(item, group_id) for item in raw_list]
async def get_group_member_info(
self,
group_id: typing.Union[int, str],
user_id: typing.Union[int, str],
) -> platform_entities.UserGroupMember:
raw = await self.bot.get_group_member_info(group_id=int(group_id), user_id=int(user_id), no_cache=True)
return self._member_to_entity(raw, group_id)
async def set_group_name(self, group_id: typing.Union[int, str], name: str) -> None:
await self.bot.set_group_name(group_id=int(group_id), group_name=name)
async def mute_member(
self,
group_id: typing.Union[int, str],
user_id: typing.Union[int, str],
duration: int = 0,
) -> None:
await self.bot.set_group_ban(group_id=int(group_id), user_id=int(user_id), duration=int(duration))
async def unmute_member(self, group_id: typing.Union[int, str], user_id: typing.Union[int, str]) -> None:
await self.bot.set_group_ban(group_id=int(group_id), user_id=int(user_id), duration=0)
async def kick_member(self, group_id: typing.Union[int, str], user_id: typing.Union[int, str]) -> None:
await self.bot.set_group_kick(group_id=int(group_id), user_id=int(user_id), reject_add_request=False)
async def leave_group(self, group_id: typing.Union[int, str]) -> None:
await self.bot.set_group_leave(group_id=int(group_id), is_dismiss=False)
async def get_user_info(self, user_id: typing.Union[int, str]) -> platform_entities.User:
raw = await self.bot.get_stranger_info(user_id=int(user_id), no_cache=True)
return platform_entities.User(
id=raw.get('user_id', user_id),
nickname=raw.get('nickname', ''),
avatar_url=raw.get('avatar_url'),
)
async def get_friend_list(self) -> list[platform_entities.User]:
raw_list = await self.bot.get_friend_list()
return [
platform_entities.User(
id=item.get('user_id', ''),
nickname=item.get('nickname', ''),
remark=item.get('remark'),
)
for item in raw_list
]
async def approve_friend_request(
self,
request_id: typing.Union[int, str],
approve: bool = True,
remark: typing.Optional[str] = None,
) -> None:
await self.bot.set_friend_add_request(flag=str(request_id), approve=approve, remark=remark or '')
async def approve_group_invite(self, request_id: typing.Union[int, str], approve: bool = True) -> None:
await self.bot.set_group_add_request(flag=str(request_id), sub_type='invite', approve=approve, reason='')
async def upload_file(self, file_data: bytes, filename: str) -> str:
raise NotSupportedError('upload_file')
async def get_file_url(self, file_id: str) -> str:
raise NotSupportedError('get_file_url')
@staticmethod
def _member_to_entity(raw: dict, group_id: typing.Union[int, str]) -> platform_entities.UserGroupMember:
role = platform_entities.MemberRole.MEMBER
if raw.get('role') == 'owner':
role = platform_entities.MemberRole.OWNER
elif raw.get('role') == 'admin':
role = platform_entities.MemberRole.ADMIN
return platform_entities.UserGroupMember(
user=platform_entities.User(
id=raw.get('user_id', ''),
nickname=raw.get('nickname', ''),
remark=raw.get('card') or raw.get('remark'),
),
group_id=group_id,
role=role,
display_name=raw.get('card') or raw.get('nickname'),
joined_at=float(raw['join_time']) if raw.get('join_time') else None,
title=raw.get('title'),
)
async def _send_forward_message(self, group_id: int, forward: platform_message.Forward) -> dict:
messages = []
for node in forward.node_list:
if not node.message_chain:
continue
content, _, _ = await AiocqhttpMessageConverter.yiri2target(node.message_chain)
if not content:
continue
messages.append(
{
'type': 'node',
'data': {
'user_id': str(node.sender_id or self.bot_account_id or '10000'),
'nickname': node.sender_name or 'LangBot',
'content': list(content),
},
}
)
if not messages:
return {}
try:
return await self.bot.call_action(
'send_forward_msg', group_id=group_id, user_id=str(self.bot_account_id), messages=messages
)
except Exception:
return await self.bot.call_action('send_group_forward_msg', group_id=group_id, messages=messages)

View File

@@ -0,0 +1,244 @@
from __future__ import annotations
import typing
import aiocqhttp
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
from langbot.pkg.platform.adapters.aiocqhttp.message_converter import AiocqhttpMessageConverter
from langbot_plugin.api.entities.builtin.platform import entities as platform_entities
from langbot_plugin.api.entities.builtin.platform import events as platform_events
class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
@staticmethod
async def yiri2target(event: platform_events.Event, bot_account_id: int | str | None = None):
return getattr(event, 'source_platform_object', None)
@staticmethod
async def target2yiri(
event: aiocqhttp.Event,
bot: aiocqhttp.CQHttp | None = None,
bot_user_id: int | str | None = None,
) -> platform_events.Event | None:
event_type = getattr(event, 'type', None)
if event_type == 'message':
return await AiocqhttpEventConverter.message_to_eba(event, bot)
if event_type == 'notice':
return AiocqhttpEventConverter.notice_to_eba(event, bot_user_id)
if event_type == 'request':
return AiocqhttpEventConverter.request_to_eba(event)
if event_type == 'meta_event':
return AiocqhttpEventConverter.platform_specific(event, f'meta.{getattr(event, "detail_type", "")}')
return None
@staticmethod
async def target2legacy(
event: aiocqhttp.Event,
bot: aiocqhttp.CQHttp | None = None,
) -> platform_events.FriendMessage | platform_events.GroupMessage | None:
eba_event = await AiocqhttpEventConverter.message_to_eba(event, bot)
if eba_event:
return eba_event.to_legacy_event()
return None
@staticmethod
async def message_to_eba(
event: aiocqhttp.Event,
bot: aiocqhttp.CQHttp | None = None,
) -> platform_events.MessageReceivedEvent:
message_chain = await AiocqhttpMessageConverter.target2yiri(
getattr(event, 'message', []),
getattr(event, 'message_id', -1),
getattr(event, 'time', None),
bot,
)
message_type = getattr(event, 'message_type', getattr(event, 'detail_type', 'private'))
group = None
chat_type = platform_entities.ChatType.PRIVATE
chat_id = getattr(event, 'user_id', '')
if message_type == 'group':
chat_type = platform_entities.ChatType.GROUP
chat_id = getattr(event, 'group_id', '')
group = AiocqhttpEventConverter.group_from_event(event)
return platform_events.MessageReceivedEvent(
type='message.received',
adapter_name='aiocqhttp',
message_id=getattr(event, 'message_id', ''),
message_chain=message_chain,
sender=AiocqhttpEventConverter.user_from_sender(event),
chat_type=chat_type,
chat_id=chat_id,
group=group,
timestamp=float(getattr(event, 'time', 0) or 0),
source_platform_object=event,
)
@staticmethod
def notice_to_eba(
event: aiocqhttp.Event,
bot_user_id: int | str | None = None,
) -> platform_events.EBAEvent:
notice_type = getattr(event, 'notice_type', getattr(event, 'detail_type', ''))
if notice_type in ('group_recall', 'friend_recall'):
return platform_events.MessageDeletedEvent(
type='message.deleted',
adapter_name='aiocqhttp',
message_id=getattr(event, 'message_id', ''),
operator=AiocqhttpEventConverter.user(getattr(event, 'operator_id', None)),
chat_type=platform_entities.ChatType.GROUP
if notice_type == 'group_recall'
else platform_entities.ChatType.PRIVATE,
chat_id=getattr(event, 'group_id', getattr(event, 'user_id', '')),
group=AiocqhttpEventConverter.group_from_event(event) if notice_type == 'group_recall' else None,
timestamp=float(getattr(event, 'time', 0) or 0),
source_platform_object=event,
)
if notice_type == 'group_increase':
group = AiocqhttpEventConverter.group_from_event(event)
user = AiocqhttpEventConverter.user(getattr(event, 'user_id', ''))
inviter_id = getattr(event, 'operator_id', None)
if AiocqhttpEventConverter._is_bot_user(getattr(event, 'user_id', None), bot_user_id, event):
return platform_events.BotInvitedToGroupEvent(
type='bot.invited_to_group',
adapter_name='aiocqhttp',
group=group,
inviter=AiocqhttpEventConverter.user(inviter_id) if inviter_id else None,
timestamp=float(getattr(event, 'time', 0) or 0),
source_platform_object=event,
)
return platform_events.MemberJoinedEvent(
type='group.member_joined',
adapter_name='aiocqhttp',
group=group,
member=user,
inviter=AiocqhttpEventConverter.user(inviter_id) if inviter_id else None,
join_type=getattr(event, 'sub_type', None) or 'direct',
timestamp=float(getattr(event, 'time', 0) or 0),
source_platform_object=event,
)
if notice_type == 'group_decrease':
group = AiocqhttpEventConverter.group_from_event(event)
operator = AiocqhttpEventConverter.user(getattr(event, 'operator_id', None))
if AiocqhttpEventConverter._is_bot_user(getattr(event, 'user_id', None), bot_user_id, event):
return platform_events.BotRemovedFromGroupEvent(
type='bot.removed_from_group',
adapter_name='aiocqhttp',
group=group,
operator=operator,
timestamp=float(getattr(event, 'time', 0) or 0),
source_platform_object=event,
)
return platform_events.MemberLeftEvent(
type='group.member_left',
adapter_name='aiocqhttp',
group=group,
member=AiocqhttpEventConverter.user(getattr(event, 'user_id', '')),
is_kicked=getattr(event, 'sub_type', '') in ('kick', 'kick_me'),
operator=operator,
timestamp=float(getattr(event, 'time', 0) or 0),
source_platform_object=event,
)
if notice_type == 'group_ban':
group = AiocqhttpEventConverter.group_from_event(event)
duration = int(getattr(event, 'duration', 0) or 0)
operator = AiocqhttpEventConverter.user(getattr(event, 'operator_id', None))
if AiocqhttpEventConverter._is_bot_user(getattr(event, 'user_id', None), bot_user_id, event):
event_cls = platform_events.BotMutedEvent if duration > 0 else platform_events.BotUnmutedEvent
kwargs: dict[str, typing.Any] = {
'type': 'bot.muted' if duration > 0 else 'bot.unmuted',
'adapter_name': 'aiocqhttp',
'group': group,
'operator': operator,
'timestamp': float(getattr(event, 'time', 0) or 0),
'source_platform_object': event,
}
if duration > 0:
kwargs['duration'] = duration
return event_cls(**kwargs)
if duration > 0:
return platform_events.MemberBannedEvent(
type='group.member_banned',
adapter_name='aiocqhttp',
group=group,
member=AiocqhttpEventConverter.user(getattr(event, 'user_id', '')),
operator=operator,
duration=duration,
timestamp=float(getattr(event, 'time', 0) or 0),
source_platform_object=event,
)
if notice_type == 'friend_add':
return platform_events.FriendAddedEvent(
type='friend.added',
adapter_name='aiocqhttp',
user=AiocqhttpEventConverter.user(getattr(event, 'user_id', '')),
timestamp=float(getattr(event, 'time', 0) or 0),
source_platform_object=event,
)
return AiocqhttpEventConverter.platform_specific(event, f'notice.{notice_type}')
@staticmethod
def request_to_eba(event: aiocqhttp.Event) -> platform_events.EBAEvent:
request_type = getattr(event, 'request_type', getattr(event, 'detail_type', ''))
if request_type == 'friend':
return platform_events.FriendRequestReceivedEvent(
type='friend.request_received',
adapter_name='aiocqhttp',
request_id=getattr(event, 'flag', ''),
user=AiocqhttpEventConverter.user(getattr(event, 'user_id', '')),
message=getattr(event, 'comment', None),
timestamp=float(getattr(event, 'time', 0) or 0),
source_platform_object=event,
)
if request_type == 'group' and getattr(event, 'sub_type', '') == 'invite':
return platform_events.BotInvitedToGroupEvent(
type='bot.invited_to_group',
adapter_name='aiocqhttp',
group=AiocqhttpEventConverter.group_from_event(event),
inviter=AiocqhttpEventConverter.user(getattr(event, 'user_id', '')),
request_id=getattr(event, 'flag', ''),
timestamp=float(getattr(event, 'time', 0) or 0),
source_platform_object=event,
)
return AiocqhttpEventConverter.platform_specific(event, f'request.{request_type}')
@staticmethod
def user_from_sender(event: aiocqhttp.Event) -> platform_entities.User:
sender = getattr(event, 'sender', {}) or {}
nickname = sender.get('card') or sender.get('nickname') or ''
return platform_entities.User(
id=sender.get('user_id', getattr(event, 'user_id', '')),
nickname=nickname,
remark=sender.get('remark'),
)
@staticmethod
def user(user_id: typing.Union[int, str, None], nickname: str = '') -> platform_entities.User | None:
if user_id is None or user_id == '':
return None
return platform_entities.User(id=user_id, nickname=nickname)
@staticmethod
def group_from_event(event: aiocqhttp.Event) -> platform_entities.UserGroup:
return platform_entities.UserGroup(
id=getattr(event, 'group_id', ''),
name=getattr(event, 'group_name', '') or '',
member_count=getattr(event, 'member_count', None),
)
@staticmethod
def platform_specific(event: aiocqhttp.Event, action: str) -> platform_events.PlatformSpecificEvent:
return platform_events.PlatformSpecificEvent(
type='platform.specific',
adapter_name='aiocqhttp',
action=action,
data={key: value for key, value in dict(event).items() if key not in {'message'}},
timestamp=float(getattr(event, 'time', 0) or 0),
source_platform_object=event,
)
@staticmethod
def _is_bot_user(user_id: typing.Any, bot_user_id: typing.Any, event: aiocqhttp.Event) -> bool:
candidate = bot_user_id or getattr(event, 'self_id', None)
return candidate is not None and user_id is not None and str(user_id) == str(candidate)

View File

@@ -0,0 +1,131 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: aiocqhttp-eba
label:
en_US: OneBot v11 (EBA)
zh_Hans: OneBot v11 (EBA)
zh_Hant: OneBot v11 (EBA)
description:
en_US: OneBot v11 adapter for QQ-compatible protocol endpoints (EBA architecture)
zh_Hans: OneBot v11 适配器,用于接入 QQ 兼容协议端EBA 架构版本)
zh_Hant: OneBot v11 適配器,用於接入 QQ 相容協定端EBA 架構版本)
icon: onebot.svg
spec:
categories:
- protocol
help_links:
zh: https://link.langbot.app/zh/platforms/aiocqhttp
en: https://link.langbot.app/en/platforms/aiocqhttp
ja: https://link.langbot.app/ja/platforms/aiocqhttp
config:
- name: host
label:
en_US: Host
zh_Hans: 主机
zh_Hant: 主機
description:
en_US: The host that OneBot v11 listens on for reverse WebSocket connections. Unless you know what you're doing, use 0.0.0.0
zh_Hans: OneBot v11 反向 WebSocket 监听主机,除非你知道自己在做什么,否则请写 0.0.0.0
zh_Hant: OneBot v11 反向 WebSocket 監聽主機,除非你知道自己在做什麼,否則請填 0.0.0.0
type: string
required: true
default: 0.0.0.0
- name: port
label:
en_US: Port
zh_Hans: 端口
zh_Hant: 連接埠
description:
en_US: Reverse WebSocket listen port
zh_Hans: 反向 WebSocket 监听端口
zh_Hant: 反向 WebSocket 監聽連接埠
type: integer
required: true
default: 2280
- name: access-token
label:
en_US: Access Token
zh_Hans: 访问令牌
zh_Hant: 存取令牌
description:
en_US: Custom connection token for the protocol endpoint. Leave empty if the endpoint has no token configured
zh_Hans: 自定义的协议端连接令牌;若协议端未设置,则不填
zh_Hant: 自訂的協定端連線令牌;若協定端未設定,則不填
type: string
required: false
default: ""
supported_events:
- message.received
- message.deleted
- group.member_joined
- group.member_left
- group.member_banned
- friend.request_received
- friend.added
- bot.invited_to_group
- bot.removed_from_group
- bot.muted
- bot.unmuted
- platform.specific
supported_apis:
required:
- send_message
- reply_message
optional:
- delete_message
- forward_message
- get_message
- get_group_info
- get_group_list
- get_group_member_list
- get_group_member_info
- set_group_name
- get_user_info
- get_friend_list
- approve_friend_request
- approve_group_invite
- mute_member
- unmute_member
- kick_member
- leave_group
- call_platform_api
platform_specific_apis:
- action: get_login_info
description: { en_US: "Get current bot account information", zh_Hans: "获取当前机器人账号信息" }
- action: get_status
description: { en_US: "Get endpoint status", zh_Hans: "获取协议端状态" }
- action: get_version_info
description: { en_US: "Get endpoint version information", zh_Hans: "获取协议端版本信息" }
- action: get_group_honor_info
description: { en_US: "Get group honor information", zh_Hans: "获取群荣誉信息" }
- action: set_group_card
description: { en_US: "Set a member group card", zh_Hans: "设置群名片" }
- action: set_group_special_title
description: { en_US: "Set a member special title", zh_Hans: "设置群专属头衔" }
- action: set_group_admin
description: { en_US: "Set group administrator status", zh_Hans: "设置群管理员" }
- action: set_group_whole_ban
description: { en_US: "Enable or disable whole-group mute", zh_Hans: "设置全员禁言" }
- action: send_group_forward_msg
description: { en_US: "Send a merged forward message", zh_Hans: "发送合并转发消息" }
- action: get_forward_msg
description: { en_US: "Get merged forward message content", zh_Hans: "获取合并转发消息内容" }
- action: get_record
description: { en_US: "Get voice file", zh_Hans: "获取语音文件" }
- action: get_image
description: { en_US: "Get image file", zh_Hans: "获取图片文件" }
- action: can_send_image
description: { en_US: "Check whether images can be sent", zh_Hans: "检查是否可以发送图片" }
- action: can_send_record
description: { en_US: "Check whether voice messages can be sent", zh_Hans: "检查是否可以发送语音" }
execution:
python:
path: ./adapter.py
attr: AiocqhttpAdapter

View File

@@ -0,0 +1,251 @@
from __future__ import annotations
import datetime
import typing
import aiocqhttp
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
from langbot_plugin.api.entities.builtin.platform import message as platform_message
FACE_NAMES = {
'14': '微笑',
'21': '可爱',
'23': '傲慢',
'24': '饥饿',
'25': '',
'26': '惊恐',
'27': '流汗',
'28': '憨笑',
'29': '悠闲',
'30': '奋斗',
'32': '疑问',
'33': '',
'34': '',
'38': '敲打',
'39': '再见',
'42': '爱情',
'43': '跳跳',
'49': '拥抱',
'53': '蛋糕',
'63': '玫瑰',
'66': '爱心',
'74': '太阳',
'75': '月亮',
'76': '',
'78': '握手',
'79': '胜利',
'85': '飞吻',
'89': '西瓜',
'96': '冷汗',
'97': '擦汗',
'98': '抠鼻',
'99': '鼓掌',
'100': '糗大了',
'101': '坏笑',
'102': '左哼哼',
'103': '右哼哼',
'104': '哈欠',
'106': '委屈',
'111': '可怜',
'120': '拳头',
'122': '爱你',
'123': 'NO',
'124': 'OK',
'129': '挥手',
'144': '喝彩',
'147': '棒棒糖',
'171': '',
'173': '泪奔',
'174': '无奈',
'175': '卖萌',
'179': 'doge',
'180': '惊喜',
'182': '笑哭',
'201': '点赞',
'203': '托脸',
'212': '托腮',
'264': '捂脸',
'271': '吃瓜',
'285': '摸鱼',
}
class AiocqhttpMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
@staticmethod
async def yiri2target(
message_chain: platform_message.MessageChain,
) -> tuple[aiocqhttp.Message, typing.Union[int, str, None], datetime.datetime | None]:
target = aiocqhttp.Message()
source_id: typing.Union[int, str, None] = None
source_time: datetime.datetime | None = None
for component in message_chain:
if isinstance(component, platform_message.Source):
source_id = component.id
source_time = component.time
elif isinstance(component, platform_message.Plain):
target.append(aiocqhttp.MessageSegment.text(component.text))
elif isinstance(component, platform_message.At):
target.append(aiocqhttp.MessageSegment.at(component.target))
elif isinstance(component, platform_message.AtAll):
target.append(aiocqhttp.MessageSegment.at('all'))
elif isinstance(component, platform_message.Image):
file_arg = AiocqhttpMessageConverter._file_arg(component)
if file_arg:
target.append(aiocqhttp.MessageSegment.image(file_arg))
elif isinstance(component, platform_message.Voice):
file_arg = AiocqhttpMessageConverter._file_arg(component)
if file_arg:
target.append(aiocqhttp.MessageSegment.record(file_arg))
elif isinstance(component, platform_message.File):
file_arg = component.url or component.path or component.base64 or component.id
target.append({'type': 'file', 'data': {'file': file_arg, 'name': component.name or 'file'}})
elif isinstance(component, platform_message.Face):
if component.face_type == 'rps':
target.append(aiocqhttp.MessageSegment.rps())
elif component.face_type == 'dice':
target.append(aiocqhttp.MessageSegment.dice())
else:
target.append(aiocqhttp.MessageSegment.face(component.face_id))
elif isinstance(component, platform_message.Forward):
for node in component.node_list:
if node.message_chain:
node_message, _, _ = await AiocqhttpMessageConverter.yiri2target(node.message_chain)
target.extend(node_message)
elif isinstance(component, platform_message.Quote) and component.id is not None:
target.append(aiocqhttp.MessageSegment.reply(component.id))
else:
target.append(aiocqhttp.MessageSegment.text(str(component)))
return target, source_id, source_time
@staticmethod
async def target2yiri(
message: typing.Any,
message_id: typing.Union[int, str] = -1,
timestamp: float | None = None,
bot: aiocqhttp.CQHttp | None = None,
) -> platform_message.MessageChain:
target = aiocqhttp.Message(message)
message_time = datetime.datetime.fromtimestamp(timestamp) if timestamp else datetime.datetime.now()
components: list[platform_message.MessageComponent] = [
platform_message.Source(id=message_id, time=message_time),
]
for segment in target:
if segment.type == 'text':
components.append(platform_message.Plain(text=segment.data.get('text', '')))
elif segment.type == 'at':
qq = str(segment.data.get('qq', ''))
components.append(platform_message.AtAll() if qq == 'all' else platform_message.At(target=qq))
elif segment.type == 'image':
if segment.data.get('emoji_package_id'):
components.append(
platform_message.Face(
face_id=int(segment.data.get('emoji_package_id') or 0),
face_name=segment.data.get('summary', ''),
)
)
else:
components.append(
platform_message.Image(
image_id=str(segment.data.get('file', '')),
url=segment.data.get('url') or segment.data.get('file') or '',
)
)
elif segment.type == 'record':
components.append(
platform_message.Voice(
voice_id=str(segment.data.get('file', '')),
url=segment.data.get('url') or segment.data.get('file') or '',
)
)
elif segment.type == 'file':
components.append(
platform_message.File(
id=str(segment.data.get('file_id') or segment.data.get('file') or ''),
name=segment.data.get('name') or segment.data.get('file') or '',
size=int(segment.data.get('size') or segment.data.get('file_size') or 0),
url=segment.data.get('url') or segment.data.get('file_url') or '',
)
)
elif segment.type == 'reply':
quote = await AiocqhttpMessageConverter._quote_from_reply_segment(segment, bot)
components.append(quote)
elif segment.type == 'face':
face_id = str(segment.data.get('id', 0))
face_name = ''
raw = segment.data.get('raw')
if isinstance(raw, dict):
face_name = str(raw.get('faceText') or '')
components.append(
platform_message.Face(
face_id=int(face_id or 0),
face_name=face_name.replace('/', '') or FACE_NAMES.get(face_id, ''),
)
)
elif segment.type == 'rps':
components.append(
platform_message.Face(
face_type='rps',
face_id=int(segment.data.get('result') or 0),
face_name='猜拳',
)
)
elif segment.type == 'dice':
components.append(
platform_message.Face(
face_type='dice',
face_id=int(segment.data.get('result') or 0),
face_name='骰子',
)
)
else:
components.append(platform_message.Unknown(text=f'{segment.type}:{segment.data}'))
return platform_message.MessageChain(components)
@staticmethod
def _file_arg(component: platform_message.Image | platform_message.Voice) -> str:
if component.base64:
_, _, payload = component.base64.partition(',')
return f'base64://{payload or component.base64}'
if component.url:
return component.url
if component.path:
return str(component.path)
return ''
@staticmethod
async def _quote_from_reply_segment(
segment: aiocqhttp.MessageSegment,
bot: aiocqhttp.CQHttp | None,
) -> platform_message.Quote:
reply_id = segment.data.get('id')
origin = platform_message.MessageChain([])
sender_id = None
group_id = None
target_id = None
if bot is not None and reply_id is not None:
try:
message_data = await bot.get_msg(message_id=int(reply_id))
sender_id = message_data.get('sender', {}).get('user_id') or message_data.get('user_id')
group_id = message_data.get('group_id')
target_id = group_id or sender_id
origin = await AiocqhttpMessageConverter.target2yiri(
message_data.get('message', []),
message_data.get('message_id', reply_id),
message_data.get('time'),
bot=None,
)
except Exception:
origin = platform_message.MessageChain([])
return platform_message.Quote(
id=reply_id,
group_id=group_id,
sender_id=sender_id,
target_id=target_id,
origin=origin,
)

View File

@@ -0,0 +1,7 @@
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="96" height="96" rx="20" fill="#16A34A"/>
<path d="M24 33C24 25.268 30.268 19 38 19H58C65.732 19 72 25.268 72 33V51C72 58.732 65.732 65 58 65H41.5L29 77V64.059C26.024 61.514 24 57.729 24 51V33Z" fill="white"/>
<circle cx="39" cy="42" r="5" fill="#16A34A"/>
<circle cx="57" cy="42" r="5" fill="#16A34A"/>
<path d="M39 53C44.5 57 51.5 57 57 53" stroke="#16A34A" stroke-width="5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 527 B

View File

@@ -0,0 +1,84 @@
from __future__ import annotations
import typing
import aiocqhttp
async def _call(bot: aiocqhttp.CQHttp, action: str, params: dict[str, typing.Any]) -> dict:
result = await bot.call_action(action, **params)
return result or {}
async def get_login_info(bot: aiocqhttp.CQHttp, params: dict) -> dict:
return await _call(bot, 'get_login_info', params)
async def get_status(bot: aiocqhttp.CQHttp, params: dict) -> dict:
return await _call(bot, 'get_status', params)
async def get_version_info(bot: aiocqhttp.CQHttp, params: dict) -> dict:
return await _call(bot, 'get_version_info', params)
async def get_group_honor_info(bot: aiocqhttp.CQHttp, params: dict) -> dict:
return await _call(bot, 'get_group_honor_info', params)
async def set_group_card(bot: aiocqhttp.CQHttp, params: dict) -> dict:
return await _call(bot, 'set_group_card', params)
async def set_group_special_title(bot: aiocqhttp.CQHttp, params: dict) -> dict:
return await _call(bot, 'set_group_special_title', params)
async def set_group_admin(bot: aiocqhttp.CQHttp, params: dict) -> dict:
return await _call(bot, 'set_group_admin', params)
async def set_group_whole_ban(bot: aiocqhttp.CQHttp, params: dict) -> dict:
return await _call(bot, 'set_group_whole_ban', params)
async def send_group_forward_msg(bot: aiocqhttp.CQHttp, params: dict) -> dict:
return await _call(bot, 'send_group_forward_msg', params)
async def get_forward_msg(bot: aiocqhttp.CQHttp, params: dict) -> dict:
return await _call(bot, 'get_forward_msg', params)
async def get_record(bot: aiocqhttp.CQHttp, params: dict) -> dict:
return await _call(bot, 'get_record', params)
async def get_image(bot: aiocqhttp.CQHttp, params: dict) -> dict:
return await _call(bot, 'get_image', params)
async def can_send_image(bot: aiocqhttp.CQHttp, params: dict) -> dict:
return await _call(bot, 'can_send_image', params)
async def can_send_record(bot: aiocqhttp.CQHttp, params: dict) -> dict:
return await _call(bot, 'can_send_record', params)
PLATFORM_API_MAP = {
'get_login_info': get_login_info,
'get_status': get_status,
'get_version_info': get_version_info,
'get_group_honor_info': get_group_honor_info,
'set_group_card': set_group_card,
'set_group_special_title': set_group_special_title,
'set_group_admin': set_group_admin,
'set_group_whole_ban': set_group_whole_ban,
'send_group_forward_msg': send_group_forward_msg,
'get_forward_msg': get_forward_msg,
'get_record': get_record,
'get_image': get_image,
'can_send_image': can_send_image,
'can_send_record': can_send_record,
}

View File

@@ -0,0 +1,9 @@
from __future__ import annotations
import typing
import aiocqhttp
TargetMessage = typing.Union[str, list, dict, aiocqhttp.Message]
OneBotResponse = dict[str, typing.Any] | None