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,233 @@
from __future__ import annotations
import argparse
import asyncio
import base64
import json
import time
from collections import Counter
from pathlib import Path
from langbot.pkg.platform.adapters.aiocqhttp.adapter import AiocqhttpAdapter
from langbot_plugin.api.definition.abstract.platform.event_logger import AbstractEventLogger
from langbot_plugin.api.entities.builtin.platform import events as platform_events
from langbot_plugin.api.entities.builtin.platform import message as platform_message
class ProbeLogger(AbstractEventLogger):
async def info(self, text, images=None, message_session_id=None, no_throw=True):
print(f'[info] {text}')
async def debug(self, text, images=None, message_session_id=None, no_throw=True):
print(f'[debug] {text}')
async def warning(self, text, images=None, message_session_id=None, no_throw=True):
print(f'[warn] {text}')
async def error(self, text, images=None, message_session_id=None, no_throw=True):
print(f'[error] {text}')
def dump_event(event: platform_events.Event) -> dict:
data = event.model_dump(exclude={'source_platform_object'})
data['event_class'] = type(event).__name__
return data
async def main():
parser = argparse.ArgumentParser(description='Live OneBot v11 / aiocqhttp EBA probe for Matcha or a real endpoint.')
parser.add_argument('--host', default='127.0.0.1')
parser.add_argument('--port', type=int, default=2280)
parser.add_argument('--access-token', default='')
parser.add_argument('--timeout', type=int, default=120)
parser.add_argument('--target-type', choices=['private', 'group'], default=None)
parser.add_argument('--target-id', default=None)
parser.add_argument(
'--component-sweep', action='store_true', help='Send text, mention, image, file, face, and forward samples.'
)
parser.add_argument('--destructive', action='store_true', help='Enable delete/mute/kick/leave style APIs.')
parser.add_argument('--out', default='data/temp/aiocqhttp_eba_live_probe.jsonl')
args = parser.parse_args()
out_path = Path(args.out)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_fp = out_path.open('a', encoding='utf-8')
adapter = AiocqhttpAdapter(
{'host': args.host, 'port': args.port, 'access-token': args.access_token},
ProbeLogger(),
)
observed: list[platform_events.Event] = []
first_message = asyncio.Event()
async def listener(event, adapter):
observed.append(event)
out_fp.write(json.dumps(dump_event(event), ensure_ascii=False, default=str) + '\n')
out_fp.flush()
print(f'[event] {type(event).__name__} {event.type}')
if isinstance(event, platform_events.MessageReceivedEvent):
first_message.set()
adapter.register_listener(platform_events.EBAEvent, listener)
async def call_api(name: str, awaitable, timeout: int = 8):
try:
return await asyncio.wait_for(awaitable, timeout=timeout)
except Exception as exc:
api_results[name] = f'skip:{type(exc).__name__}:{exc}'
return None
task = asyncio.create_task(adapter.run_async())
print(f'Listening on ws://{args.host}:{args.port}/ws/ . Trigger events from Matcha now.')
api_results: dict[str, str] = {}
try:
try:
await asyncio.wait_for(first_message.wait(), timeout=args.timeout)
first = next(event for event in observed if isinstance(event, platform_events.MessageReceivedEvent))
target_type = args.target_type or ('group' if first.chat_type.value == 'group' else 'private')
target_id = args.target_id or str(first.chat_id)
reply = await call_api(
'reply_message',
adapter.reply_message(
first,
platform_message.MessageChain([platform_message.Plain(text='aiocqhttp EBA reply probe')]),
quote_origin=True,
),
)
if reply:
api_results['reply_message'] = f'ok:{reply.message_id}'
sent = await call_api(
'send_message',
adapter.send_message(
target_type,
target_id,
platform_message.MessageChain([platform_message.Plain(text='aiocqhttp EBA send probe')]),
),
)
if sent:
api_results['send_message'] = f'ok:{sent.message_id}'
if args.component_sweep:
png_base64 = base64.b64encode(
base64.b64decode(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFeAJ5mZtH5QAAAABJRU5ErkJggg=='
)
).decode()
component_cases = {
'component:text_at_face': platform_message.MessageChain(
[
platform_message.Plain(text='component sweep '),
platform_message.At(target=str(first.sender.id)),
platform_message.Plain(text=' '),
platform_message.AtAll(),
platform_message.Plain(text=' '),
platform_message.Face(face_id=14, face_name='微笑'),
]
),
'component:image_base64': platform_message.MessageChain(
[
platform_message.Plain(text='image component '),
platform_message.Image(base64=f'data:image/png;base64,{png_base64}'),
]
),
'component:file': platform_message.MessageChain(
[
platform_message.Plain(text='file component '),
platform_message.File(name='probe.txt', url='https://example.com/probe.txt'),
]
),
}
if target_type == 'group':
component_cases['component:forward'] = platform_message.MessageChain(
[
platform_message.Forward(
node_list=[
platform_message.ForwardMessageNode(
sender_id=adapter.bot_account_id or '960164003',
sender_name='LangBot',
message_chain=platform_message.MessageChain(
[platform_message.Plain(text='forward node 1')]
),
),
platform_message.ForwardMessageNode(
sender_id=str(first.sender.id),
sender_name=first.sender.nickname or 'Matcha',
message_chain=platform_message.MessageChain(
[platform_message.Plain(text='forward node 2')]
),
),
]
)
]
)
for name, chain in component_cases.items():
result = await call_api(name, adapter.send_message(target_type, target_id, chain))
if result:
api_results[name] = f'ok:{result.message_id}'
if sent and sent.message_id:
fetched = await call_api('get_message', adapter.get_message(target_type, target_id, sent.message_id))
if fetched:
api_results['get_message'] = f'ok:{fetched.message_id}'
if args.destructive:
deleted = await call_api(
'delete_message',
adapter.delete_message(target_type, target_id, sent.message_id),
)
if deleted is not None:
api_results['delete_message'] = 'ok'
if target_type == 'group':
group = await call_api('get_group_info', adapter.get_group_info(target_id))
if group:
api_results['get_group_info'] = f'ok:{group.id}'
members = await call_api('get_group_member_list', adapter.get_group_member_list(target_id))
if members is not None:
api_results['get_group_member_list'] = f'ok:{len(members)}'
if members:
member = await call_api(
'get_group_member_info',
adapter.get_group_member_info(target_id, members[0].user.id),
)
if member:
api_results['get_group_member_info'] = f'ok:{member.user.id}'
for action in ('get_login_info', 'get_status', 'get_version_info', 'can_send_image', 'can_send_record'):
result = await call_api(
f'call_platform_api:{action}',
adapter.call_platform_api(action, {}),
)
if result is not None:
api_results[f'call_platform_api:{action}'] = 'ok'
except asyncio.TimeoutError:
api_results['first_message'] = 'timeout'
finally:
task.cancel()
try:
await asyncio.wait_for(task, timeout=3)
except (asyncio.CancelledError, asyncio.TimeoutError):
pass
out_fp.close()
counts = Counter(event.type for event in observed)
print(
json.dumps(
{
'output': str(out_path),
'observed_events': counts,
'api_results': api_results,
'duration_seconds': round(time.monotonic(), 3),
},
ensure_ascii=False,
default=str,
indent=2,
)
)
if __name__ == '__main__':
asyncio.run(main())