mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-24 06:24:20 +00:00
Merge remote-tracking branch 'origin/refactor/eba' into dev_4_11
# Conflicts: # pyproject.toml # src/langbot/pkg/pipeline/preproc/preproc.py # uv.lock
This commit is contained in:
@@ -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())
|
||||
@@ -0,0 +1,420 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from langbot.pkg.platform.adapters.discord.adapter import DiscordAdapter
|
||||
from langbot_plugin.api.definition.abstract.platform.event_logger import AbstractEventLogger
|
||||
from langbot_plugin.api.entities.builtin.platform import entities as platform_entities
|
||||
from langbot_plugin.api.entities.builtin.platform import errors as platform_errors
|
||||
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'[warning] {text}')
|
||||
|
||||
async def error(self, text, images=None, message_session_id=None, no_throw=True):
|
||||
print(f'[error] {text}')
|
||||
|
||||
|
||||
PNG_1X1 = base64.b64decode(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII='
|
||||
)
|
||||
|
||||
|
||||
def summarize_event(event: platform_events.EBAEvent) -> dict[str, Any]:
|
||||
data: dict[str, Any] = {
|
||||
'type': event.type,
|
||||
'adapter_name': event.adapter_name,
|
||||
'timestamp': event.timestamp,
|
||||
}
|
||||
for field in ('message_id', 'chat_id', 'chat_type', 'reaction', 'is_add', 'action', 'data'):
|
||||
if hasattr(event, field):
|
||||
value = getattr(event, field)
|
||||
data[field] = value.value if hasattr(value, 'value') else value
|
||||
for field in ('sender', 'user', 'member', 'group', 'operator', 'inviter'):
|
||||
if hasattr(event, field):
|
||||
value = getattr(event, field)
|
||||
if value is not None and hasattr(value, 'model_dump'):
|
||||
data[field] = value.model_dump()
|
||||
return data
|
||||
|
||||
|
||||
def chat_type_value(chat_type: platform_entities.ChatType | str) -> str:
|
||||
return chat_type.value if hasattr(chat_type, 'value') else str(chat_type)
|
||||
|
||||
|
||||
def record_api(results: list[dict[str, Any]], name: str, ok: bool, result: Any = None, error: Exception | None = None):
|
||||
entry: dict[str, Any] = {'name': name, 'ok': ok}
|
||||
if result is not None:
|
||||
entry['result'] = result
|
||||
if error is not None:
|
||||
entry['error'] = repr(error)
|
||||
results.append(entry)
|
||||
print('DISCORD_EBA_API', json.dumps(entry, ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
async def run_api(results: list[dict[str, Any]], name: str, func):
|
||||
try:
|
||||
result = await func()
|
||||
record_api(results, name, True, result)
|
||||
return result
|
||||
except Exception as exc:
|
||||
record_api(results, name, False, error=exc)
|
||||
return None
|
||||
|
||||
|
||||
async def run_expected_error(results: list[dict[str, Any]], name: str, func, error_type: type[Exception]):
|
||||
try:
|
||||
await func()
|
||||
except Exception as exc:
|
||||
if isinstance(exc, error_type):
|
||||
record_api(results, name, True, {'expected_error': type(exc).__name__})
|
||||
return
|
||||
record_api(results, name, False, error=exc)
|
||||
return
|
||||
record_api(results, name, False, error=RuntimeError(f'expected {error_type.__name__}'))
|
||||
|
||||
|
||||
async def wait_for_event(events: list[platform_events.EBAEvent], predicate, timeout: int) -> platform_events.EBAEvent:
|
||||
deadline = asyncio.get_running_loop().time() + timeout
|
||||
seen = 0
|
||||
while asyncio.get_running_loop().time() < deadline:
|
||||
for event in events[seen:]:
|
||||
if predicate(event):
|
||||
return event
|
||||
seen = len(events)
|
||||
await asyncio.sleep(0.2)
|
||||
raise TimeoutError('event was not observed before timeout')
|
||||
|
||||
|
||||
async def run_probe(
|
||||
token: str,
|
||||
client_id: str,
|
||||
channel_id: str,
|
||||
log_path: Path,
|
||||
timeout: int,
|
||||
guild_id: str | None,
|
||||
moderation_user_id: str | None,
|
||||
kick_user_id: str | None,
|
||||
allow_invite: bool,
|
||||
allow_leave_group: bool,
|
||||
):
|
||||
adapter = DiscordAdapter({'client_id': client_id, 'token': token}, ProbeLogger())
|
||||
events: list[platform_events.EBAEvent] = []
|
||||
api_results: list[dict[str, Any]] = []
|
||||
first_message = asyncio.Event()
|
||||
|
||||
async def listener(event, adapter):
|
||||
events.append(event)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log_path.open('a', encoding='utf-8') as fp:
|
||||
fp.write(json.dumps(summarize_event(event), ensure_ascii=False, default=str) + '\n')
|
||||
print('DISCORD_EBA_EVENT', json.dumps(summarize_event(event), ensure_ascii=False, default=str))
|
||||
if isinstance(event, platform_events.MessageReceivedEvent):
|
||||
first_message.set()
|
||||
|
||||
adapter.register_listener(platform_events.EBAEvent, listener)
|
||||
run_task = asyncio.create_task(adapter.run_async())
|
||||
|
||||
try:
|
||||
print('READY: send a message in the Discord test channel now.')
|
||||
await asyncio.wait_for(first_message.wait(), timeout=timeout)
|
||||
source = next(event for event in events if isinstance(event, platform_events.MessageReceivedEvent))
|
||||
source_chat_type = chat_type_value(source.chat_type)
|
||||
target_chat_id = str(source.chat_id)
|
||||
guild_id = guild_id or (str(source.group.id) if source.group else None)
|
||||
actor_user_id = str(source.sender.id)
|
||||
|
||||
await run_api(
|
||||
api_results,
|
||||
'reply_message:text_image_file',
|
||||
lambda: adapter.reply_message(
|
||||
source,
|
||||
platform_message.MessageChain(
|
||||
[
|
||||
platform_message.Plain(text='Discord EBA live reply: text'),
|
||||
platform_message.Image(base64=base64.b64encode(PNG_1X1).decode()),
|
||||
platform_message.File(
|
||||
name='discord-eba-live.txt',
|
||||
size=16,
|
||||
base64='data:text/plain;base64,' + base64.b64encode(b'discord-eba-live').decode(),
|
||||
),
|
||||
]
|
||||
),
|
||||
quote_origin=True,
|
||||
),
|
||||
)
|
||||
sent = await run_api(
|
||||
api_results,
|
||||
'send_message:text_image_file',
|
||||
lambda: adapter.send_message(
|
||||
source_chat_type,
|
||||
target_chat_id,
|
||||
platform_message.MessageChain(
|
||||
[
|
||||
platform_message.Plain(text='Discord EBA live send_message OK'),
|
||||
platform_message.Image(base64=base64.b64encode(PNG_1X1).decode()),
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
edit_probe = await run_api(
|
||||
api_results,
|
||||
'send_message:edit_delete_probe',
|
||||
lambda: adapter.send_message(
|
||||
source_chat_type,
|
||||
target_chat_id,
|
||||
platform_message.MessageChain([platform_message.Plain(text='Discord EBA edit/delete probe')]),
|
||||
),
|
||||
)
|
||||
if edit_probe:
|
||||
await run_api(
|
||||
api_results,
|
||||
'edit_message',
|
||||
lambda: adapter.edit_message(
|
||||
source_chat_type,
|
||||
target_chat_id,
|
||||
edit_probe.message_id,
|
||||
platform_message.MessageChain([platform_message.Plain(text='Discord EBA edit probe edited')]),
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'delete_message',
|
||||
lambda: adapter.delete_message(source_chat_type, target_chat_id, edit_probe.message_id),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'event_observed:message.edited',
|
||||
lambda: wait_for_event(
|
||||
events,
|
||||
lambda event: isinstance(event, platform_events.MessageEditedEvent)
|
||||
and str(event.message_id) == str(edit_probe.message_id),
|
||||
timeout=max(10, timeout // 3),
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'event_observed:message.deleted',
|
||||
lambda: wait_for_event(
|
||||
events,
|
||||
lambda event: isinstance(event, platform_events.MessageDeletedEvent)
|
||||
and str(event.message_id) == str(edit_probe.message_id),
|
||||
timeout=max(10, timeout // 3),
|
||||
),
|
||||
)
|
||||
|
||||
if sent:
|
||||
await run_api(
|
||||
api_results,
|
||||
'forward_message',
|
||||
lambda: adapter.forward_message(
|
||||
source_chat_type,
|
||||
target_chat_id,
|
||||
sent.message_id,
|
||||
source_chat_type,
|
||||
target_chat_id,
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:add_reaction',
|
||||
lambda: adapter.call_platform_api(
|
||||
'add_reaction',
|
||||
{'channel_id': target_chat_id, 'message_id': sent.message_id, 'emoji': '👍'},
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:remove_reaction',
|
||||
lambda: adapter.call_platform_api(
|
||||
'remove_reaction',
|
||||
{'channel_id': target_chat_id, 'message_id': sent.message_id, 'emoji': '👍'},
|
||||
),
|
||||
)
|
||||
|
||||
await run_api(api_results, 'get_user_info', lambda: adapter.get_user_info(actor_user_id))
|
||||
await run_expected_error(
|
||||
api_results,
|
||||
'upload_file:not_supported',
|
||||
lambda: adapter.upload_file(b'discord-eba-upload', 'discord-eba-upload.txt'),
|
||||
platform_errors.NotSupportedError,
|
||||
)
|
||||
await run_api(api_results, 'get_file_url', lambda: adapter.get_file_url('https://cdn.discordapp.com/file.txt'))
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:get_channel',
|
||||
lambda: adapter.call_platform_api('get_channel', {'channel_id': target_chat_id}),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:typing',
|
||||
lambda: adapter.call_platform_api('typing', {'channel_id': target_chat_id}),
|
||||
)
|
||||
|
||||
pin_probe = await run_api(
|
||||
api_results,
|
||||
'send_message:pin_probe',
|
||||
lambda: adapter.send_message(
|
||||
source_chat_type,
|
||||
target_chat_id,
|
||||
platform_message.MessageChain([platform_message.Plain(text='Discord EBA pin probe')]),
|
||||
),
|
||||
)
|
||||
if pin_probe:
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:pin_message',
|
||||
lambda: adapter.call_platform_api(
|
||||
'pin_message',
|
||||
{'channel_id': target_chat_id, 'message_id': pin_probe.message_id},
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:unpin_message',
|
||||
lambda: adapter.call_platform_api(
|
||||
'unpin_message',
|
||||
{'channel_id': target_chat_id, 'message_id': pin_probe.message_id},
|
||||
),
|
||||
)
|
||||
|
||||
if guild_id:
|
||||
await run_api(api_results, 'get_group_info', lambda: adapter.get_group_info(guild_id))
|
||||
await run_api(api_results, 'get_group_member_list', lambda: adapter.get_group_member_list(guild_id))
|
||||
await run_api(
|
||||
api_results,
|
||||
'get_group_member_info',
|
||||
lambda: adapter.get_group_member_info(guild_id, actor_user_id),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:get_guild',
|
||||
lambda: adapter.call_platform_api('get_guild', {'guild_id': guild_id}),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:get_guild_channels',
|
||||
lambda: adapter.call_platform_api('get_guild_channels', {'guild_id': guild_id}),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:get_guild_roles',
|
||||
lambda: adapter.call_platform_api('get_guild_roles', {'guild_id': guild_id}),
|
||||
)
|
||||
if allow_invite:
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:create_invite',
|
||||
lambda: adapter.call_platform_api('create_invite', {'channel_id': target_chat_id, 'max_age': 300}),
|
||||
)
|
||||
else:
|
||||
record_api(api_results, 'call_platform_api:create_invite', False, error=RuntimeError('skipped'))
|
||||
|
||||
if moderation_user_id:
|
||||
await run_api(
|
||||
api_results,
|
||||
'mute_member',
|
||||
lambda: adapter.mute_member(guild_id, moderation_user_id, duration=30),
|
||||
)
|
||||
await run_api(api_results, 'unmute_member', lambda: adapter.unmute_member(guild_id, moderation_user_id))
|
||||
else:
|
||||
record_api(api_results, 'mute_member', False, error=RuntimeError('skipped'))
|
||||
record_api(api_results, 'unmute_member', False, error=RuntimeError('skipped'))
|
||||
|
||||
if kick_user_id:
|
||||
await run_api(api_results, 'kick_member', lambda: adapter.kick_member(guild_id, kick_user_id))
|
||||
else:
|
||||
record_api(api_results, 'kick_member', False, error=RuntimeError('skipped'))
|
||||
|
||||
if allow_leave_group:
|
||||
await run_api(api_results, 'leave_group', lambda: adapter.leave_group(guild_id))
|
||||
else:
|
||||
record_api(api_results, 'leave_group', False, error=RuntimeError('skipped'))
|
||||
else:
|
||||
for name in (
|
||||
'get_group_info',
|
||||
'get_group_member_list',
|
||||
'get_group_member_info',
|
||||
'call_platform_api:get_guild',
|
||||
'call_platform_api:get_guild_channels',
|
||||
'call_platform_api:get_guild_roles',
|
||||
'call_platform_api:create_invite',
|
||||
'mute_member',
|
||||
'unmute_member',
|
||||
'kick_member',
|
||||
'leave_group',
|
||||
):
|
||||
record_api(api_results, name, False, error=RuntimeError('skipped: no guild id'))
|
||||
|
||||
await asyncio.sleep(3)
|
||||
finally:
|
||||
await adapter.kill()
|
||||
run_task.cancel()
|
||||
summary = {
|
||||
'events': [summarize_event(event) for event in events],
|
||||
'event_types': [event.type for event in events],
|
||||
'api_results': api_results,
|
||||
'api_passed': [result['name'] for result in api_results if result['ok']],
|
||||
'api_failed': [result for result in api_results if not result['ok']],
|
||||
}
|
||||
print('SUMMARY', json.dumps(summary, ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--token', default=os.getenv('DISCORD_BOT_TOKEN', ''))
|
||||
parser.add_argument('--client-id', default=os.getenv('DISCORD_CLIENT_ID', ''))
|
||||
parser.add_argument('--channel-id', default=os.getenv('DISCORD_EBA_CHANNEL_ID', ''))
|
||||
parser.add_argument('--guild-id', default=os.getenv('DISCORD_EBA_GUILD_ID'))
|
||||
parser.add_argument('--moderation-user-id', default=os.getenv('DISCORD_EBA_MODERATION_USER_ID'))
|
||||
parser.add_argument('--kick-user-id', default=os.getenv('DISCORD_EBA_KICK_USER_ID'))
|
||||
parser.add_argument('--log', default='data/temp/live_discord_eba_probe.jsonl')
|
||||
parser.add_argument('--timeout', type=int, default=90)
|
||||
parser.add_argument('--allow-invite', action='store_true')
|
||||
parser.add_argument('--allow-leave-group', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.token:
|
||||
raise SystemExit('Set DISCORD_BOT_TOKEN or pass --token')
|
||||
if not args.client_id:
|
||||
raise SystemExit('Set DISCORD_CLIENT_ID or pass --client-id')
|
||||
if not args.channel_id:
|
||||
raise SystemExit('Set DISCORD_EBA_CHANNEL_ID or pass --channel-id')
|
||||
|
||||
log_path = Path(args.log)
|
||||
if log_path.exists():
|
||||
log_path.unlink()
|
||||
asyncio.run(
|
||||
run_probe(
|
||||
args.token,
|
||||
args.client_id,
|
||||
args.channel_id,
|
||||
log_path,
|
||||
args.timeout,
|
||||
args.guild_id,
|
||||
args.moderation_user_id,
|
||||
args.kick_user_id,
|
||||
args.allow_invite,
|
||||
args.allow_leave_group,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,158 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from quart import Quart, request
|
||||
|
||||
from langbot.pkg.platform.adapters.officialaccount.adapter import OfficialAccountAdapter
|
||||
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'[warning] {text}')
|
||||
|
||||
async def error(self, text, images=None, message_session_id=None, no_throw=True):
|
||||
print(f'[error] {text}')
|
||||
|
||||
|
||||
def redact(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: '<redacted>' if key.lower() in {'secret', 'token', 'encodingaeskey', 'encrypt', 'appsecret'} else redact(item)
|
||||
for key, item in value.items()
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [redact(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def summarize_event(event: platform_events.EBAEvent) -> dict:
|
||||
data = {
|
||||
'type': event.type,
|
||||
'adapter_name': event.adapter_name,
|
||||
'timestamp': event.timestamp,
|
||||
}
|
||||
for field in ('message_id', 'chat_id', 'chat_type', 'action', 'data'):
|
||||
if hasattr(event, field):
|
||||
value = getattr(event, field)
|
||||
if hasattr(value, 'value'):
|
||||
value = value.value
|
||||
data[field] = redact(value)
|
||||
if hasattr(event, 'sender') and event.sender is not None:
|
||||
data['sender'] = event.sender.model_dump()
|
||||
if hasattr(event, 'message_chain') and event.message_chain is not None:
|
||||
data['message_chain'] = event.message_chain.model_dump()
|
||||
return data
|
||||
|
||||
|
||||
def record_api(results: list[dict[str, Any]], name: str, ok: bool, result: Any = None, error: Exception | None = None):
|
||||
entry = {'name': name, 'ok': ok}
|
||||
if result is not None:
|
||||
entry['result'] = redact(result)
|
||||
if error is not None:
|
||||
entry['error'] = repr(error)
|
||||
results.append(entry)
|
||||
print('OFFICIALACCOUNT_EBA_API', json.dumps(entry, ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
async def run_api(results: list[dict[str, Any]], name: str, func):
|
||||
try:
|
||||
result = await func()
|
||||
record_api(results, name, True, result)
|
||||
return result
|
||||
except Exception as exc:
|
||||
record_api(results, name, False, error=exc)
|
||||
return None
|
||||
|
||||
|
||||
def config_from_env() -> dict:
|
||||
config = {
|
||||
'token': os.getenv('OFFICIALACCOUNT_TOKEN', ''),
|
||||
'EncodingAESKey': os.getenv('OFFICIALACCOUNT_ENCODING_AES_KEY', ''),
|
||||
'AppSecret': os.getenv('OFFICIALACCOUNT_APP_SECRET', ''),
|
||||
'AppID': os.getenv('OFFICIALACCOUNT_APP_ID', ''),
|
||||
'Mode': os.getenv('OFFICIALACCOUNT_MODE', 'drop'),
|
||||
'LoadingMessage': os.getenv('OFFICIALACCOUNT_LOADING_MESSAGE', 'AI正在思考中,请发送任意内容获取回复。'),
|
||||
'api_base_url': os.getenv('OFFICIALACCOUNT_API_BASE_URL', 'https://api.weixin.qq.com'),
|
||||
}
|
||||
missing = [key for key in ('token', 'EncodingAESKey', 'AppSecret', 'AppID', 'Mode') if not config.get(key)]
|
||||
if missing:
|
||||
raise RuntimeError(f'Missing required OfficialAccount env vars for fields: {missing}')
|
||||
return config
|
||||
|
||||
|
||||
async def run_probe(args: argparse.Namespace):
|
||||
adapter = OfficialAccountAdapter(config_from_env(), ProbeLogger())
|
||||
events: list[platform_events.EBAEvent] = []
|
||||
api_results: list[dict[str, Any]] = []
|
||||
first_message = asyncio.Event()
|
||||
log_path = Path(args.log)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async def listener(event, adapter):
|
||||
events.append(event)
|
||||
with log_path.open('a', encoding='utf-8') as fp:
|
||||
fp.write(json.dumps(summarize_event(event), ensure_ascii=False, default=str) + '\n')
|
||||
print('OFFICIALACCOUNT_EBA_EVENT', json.dumps(summarize_event(event), ensure_ascii=False, default=str))
|
||||
if isinstance(event, platform_events.MessageReceivedEvent):
|
||||
first_message.set()
|
||||
|
||||
adapter.register_listener(platform_events.EBAEvent, listener)
|
||||
|
||||
app = Quart(__name__)
|
||||
|
||||
@app.route('/callback', methods=['GET', 'POST'])
|
||||
async def callback():
|
||||
return await adapter.handle_unified_webhook('probe', '', request)
|
||||
|
||||
server_task = asyncio.create_task(app.run_task(host=args.host, port=args.port))
|
||||
try:
|
||||
await asyncio.wait_for(first_message.wait(), timeout=args.timeout)
|
||||
first = next(event for event in events if isinstance(event, platform_events.MessageReceivedEvent))
|
||||
await run_api(api_results, 'reply_message', lambda: adapter.reply_message(first, platform_message.MessageChain([platform_message.Plain(text=args.reply_text)])))
|
||||
await run_api(api_results, 'get_message', lambda: adapter.get_message(first.chat_type.value, first.chat_id, first.message_id))
|
||||
await run_api(api_results, 'get_user_info', lambda: adapter.get_user_info(first.sender.id))
|
||||
await run_api(api_results, 'get_friend_list', adapter.get_friend_list)
|
||||
await run_api(api_results, 'call_platform_api.get_mode', lambda: adapter.call_platform_api('get_mode', {}))
|
||||
finally:
|
||||
server_task.cancel()
|
||||
try:
|
||||
await server_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
summary = {
|
||||
'events': [event.type for event in events],
|
||||
'api_results': api_results,
|
||||
'log': str(log_path),
|
||||
}
|
||||
print('OFFICIALACCOUNT_EBA_SUMMARY', json.dumps(summary, ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Live OfficialAccount EBA adapter probe')
|
||||
parser.add_argument('--host', default='127.0.0.1')
|
||||
parser.add_argument('--port', type=int, default=5311)
|
||||
parser.add_argument('--timeout', type=float, default=300)
|
||||
parser.add_argument('--log', default='data/temp/officialaccount_eba_probe.jsonl')
|
||||
parser.add_argument('--reply-text', default='OfficialAccount EBA probe reply')
|
||||
args = parser.parse_args()
|
||||
asyncio.run(run_probe(args))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,171 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from quart import Quart, request
|
||||
|
||||
from langbot.pkg.platform.adapters.qqofficial.adapter import QQOfficialAdapter
|
||||
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'[warning] {text}')
|
||||
|
||||
async def error(self, text, images=None, message_session_id=None, no_throw=True):
|
||||
print(f'[error] {text}')
|
||||
|
||||
|
||||
def redact(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
secret_keys = {'secret', 'token', 'authorization', 'access_token', 'clientsecret'}
|
||||
return {key: '<redacted>' if key.lower() in secret_keys else redact(item) for key, item in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [redact(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def summarize_event(event: platform_events.EBAEvent) -> dict:
|
||||
data = {
|
||||
'type': event.type,
|
||||
'adapter_name': event.adapter_name,
|
||||
'timestamp': event.timestamp,
|
||||
}
|
||||
for field in ('message_id', 'chat_id', 'chat_type', 'action', 'data'):
|
||||
if hasattr(event, field):
|
||||
value = getattr(event, field)
|
||||
if hasattr(value, 'value'):
|
||||
value = value.value
|
||||
data[field] = redact(value)
|
||||
if hasattr(event, 'sender') and event.sender is not None:
|
||||
data['sender'] = event.sender.model_dump()
|
||||
if hasattr(event, 'group') and event.group is not None:
|
||||
data['group'] = event.group.model_dump()
|
||||
if hasattr(event, 'message_chain') and event.message_chain is not None:
|
||||
data['message_chain'] = event.message_chain.model_dump()
|
||||
return data
|
||||
|
||||
|
||||
def record_api(results: list[dict[str, Any]], name: str, ok: bool, result: Any = None, error: Exception | None = None):
|
||||
entry = {'name': name, 'ok': ok}
|
||||
if result is not None:
|
||||
entry['result'] = redact(result)
|
||||
if error is not None:
|
||||
entry['error'] = repr(error)
|
||||
results.append(entry)
|
||||
print('QQOFFICIAL_EBA_API', json.dumps(entry, ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
async def run_api(results: list[dict[str, Any]], name: str, func):
|
||||
try:
|
||||
result = await func()
|
||||
record_api(results, name, True, result)
|
||||
return result
|
||||
except Exception as exc:
|
||||
record_api(results, name, False, error=exc)
|
||||
return None
|
||||
|
||||
|
||||
def config_from_env(enable_webhook: bool) -> dict:
|
||||
config = {
|
||||
'appid': os.getenv('QQOFFICIAL_APPID', ''),
|
||||
'secret': os.getenv('QQOFFICIAL_SECRET', ''),
|
||||
'token': os.getenv('QQOFFICIAL_TOKEN', ''),
|
||||
'enable-webhook': enable_webhook,
|
||||
'enable-stream-reply': os.getenv('QQOFFICIAL_ENABLE_STREAM_REPLY', '').lower() in {'1', 'true', 'yes'},
|
||||
}
|
||||
missing = [key for key in ('appid', 'secret', 'token') if not config.get(key)]
|
||||
if missing:
|
||||
raise RuntimeError(f'Missing required QQOfficial env vars for fields: {missing}')
|
||||
return config
|
||||
|
||||
|
||||
async def run_probe(args: argparse.Namespace):
|
||||
adapter = QQOfficialAdapter(config_from_env(args.webhook), ProbeLogger())
|
||||
events: list[platform_events.EBAEvent] = []
|
||||
api_results: list[dict[str, Any]] = []
|
||||
first_message = asyncio.Event()
|
||||
log_path = Path(args.log)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async def listener(event, adapter):
|
||||
events.append(event)
|
||||
summary = summarize_event(event)
|
||||
with log_path.open('a', encoding='utf-8') as fp:
|
||||
fp.write(json.dumps(summary, ensure_ascii=False, default=str) + '\n')
|
||||
print('QQOFFICIAL_EBA_EVENT', json.dumps(summary, ensure_ascii=False, default=str))
|
||||
if isinstance(event, platform_events.MessageReceivedEvent):
|
||||
first_message.set()
|
||||
|
||||
adapter.register_listener(platform_events.EBAEvent, listener)
|
||||
|
||||
server_task = None
|
||||
if args.webhook:
|
||||
app = Quart(__name__)
|
||||
|
||||
@app.route('/callback', methods=['GET', 'POST'])
|
||||
async def callback():
|
||||
return await adapter.handle_unified_webhook('probe', '', request)
|
||||
|
||||
server_task = asyncio.create_task(app.run_task(host=args.host, port=args.port))
|
||||
else:
|
||||
server_task = asyncio.create_task(adapter.run_async())
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(first_message.wait(), timeout=args.timeout)
|
||||
first = next(event for event in events if isinstance(event, platform_events.MessageReceivedEvent))
|
||||
await run_api(api_results, 'reply_message', lambda: adapter.reply_message(first, platform_message.MessageChain([platform_message.Plain(text=args.reply_text)])))
|
||||
await run_api(api_results, 'get_message', lambda: adapter.get_message(first.chat_type.value, first.chat_id, first.message_id))
|
||||
await run_api(api_results, 'get_user_info', lambda: adapter.get_user_info(first.sender.id))
|
||||
await run_api(api_results, 'get_friend_list', adapter.get_friend_list)
|
||||
if getattr(first, 'group', None):
|
||||
await run_api(api_results, 'get_group_info', lambda: adapter.get_group_info(first.group.id))
|
||||
await run_api(api_results, 'get_group_member_info', lambda: adapter.get_group_member_info(first.group.id, first.sender.id))
|
||||
await run_api(api_results, 'get_group_member_list', lambda: adapter.get_group_member_list(first.group.id))
|
||||
await run_api(api_results, 'call_platform_api.get_mode', lambda: adapter.call_platform_api('get_mode', {}))
|
||||
await run_api(api_results, 'call_platform_api.check_access_token', lambda: adapter.call_platform_api('check_access_token', {}))
|
||||
finally:
|
||||
if server_task:
|
||||
server_task.cancel()
|
||||
try:
|
||||
await server_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
await adapter.kill()
|
||||
|
||||
summary = {
|
||||
'events': [event.type for event in events],
|
||||
'api_results': api_results,
|
||||
'log': str(log_path),
|
||||
}
|
||||
print('QQOFFICIAL_EBA_SUMMARY', json.dumps(summary, ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Live QQOfficial EBA adapter probe')
|
||||
parser.add_argument('--host', default='127.0.0.1')
|
||||
parser.add_argument('--port', type=int, default=5312)
|
||||
parser.add_argument('--timeout', type=float, default=300)
|
||||
parser.add_argument('--log', default='data/temp/qqofficial_eba_probe.jsonl')
|
||||
parser.add_argument('--reply-text', default='QQOfficial EBA probe reply')
|
||||
parser.add_argument('--webhook', action='store_true', help='Run a local webhook callback server instead of the WebSocket gateway')
|
||||
args = parser.parse_args()
|
||||
asyncio.run(run_probe(args))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from quart import Quart, request
|
||||
|
||||
from langbot.pkg.platform.adapters.slack.adapter import SlackAdapter
|
||||
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'[warning] {text}')
|
||||
|
||||
async def error(self, text, images=None, message_session_id=None, no_throw=True):
|
||||
print(f'[error] {text}')
|
||||
|
||||
|
||||
def redact(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
secret_keys = {'token', 'signing_secret', 'authorization', 'access_token'}
|
||||
return {key: '<redacted>' if key.lower() in secret_keys else redact(item) for key, item in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [redact(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def summarize_event(event: platform_events.EBAEvent) -> dict:
|
||||
data = {
|
||||
'type': event.type,
|
||||
'adapter_name': event.adapter_name,
|
||||
'timestamp': event.timestamp,
|
||||
}
|
||||
for field in ('message_id', 'chat_id', 'chat_type', 'action', 'data'):
|
||||
if hasattr(event, field):
|
||||
value = getattr(event, field)
|
||||
if hasattr(value, 'value'):
|
||||
value = value.value
|
||||
data[field] = redact(value)
|
||||
if hasattr(event, 'sender') and event.sender is not None:
|
||||
data['sender'] = event.sender.model_dump()
|
||||
if hasattr(event, 'group') and event.group is not None:
|
||||
data['group'] = event.group.model_dump()
|
||||
if hasattr(event, 'message_chain') and event.message_chain is not None:
|
||||
data['message_chain'] = event.message_chain.model_dump()
|
||||
return data
|
||||
|
||||
|
||||
def record_api(results: list[dict[str, Any]], name: str, ok: bool, result: Any = None, error: Exception | None = None):
|
||||
entry = {'name': name, 'ok': ok}
|
||||
if result is not None:
|
||||
entry['result'] = redact(result)
|
||||
if error is not None:
|
||||
entry['error'] = repr(error)
|
||||
results.append(entry)
|
||||
print('SLACK_EBA_API', json.dumps(entry, ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
async def run_api(results: list[dict[str, Any]], name: str, func):
|
||||
try:
|
||||
result = await func()
|
||||
record_api(results, name, True, result)
|
||||
return result
|
||||
except Exception as exc:
|
||||
record_api(results, name, False, error=exc)
|
||||
return None
|
||||
|
||||
|
||||
def config_from_env() -> dict:
|
||||
config = {
|
||||
'bot_token': os.getenv('SLACK_BOT_TOKEN', ''),
|
||||
'signing_secret': os.getenv('SLACK_SIGNING_SECRET', ''),
|
||||
'bot_user_id': os.getenv('SLACK_BOT_USER_ID', ''),
|
||||
}
|
||||
missing = [key for key in ('bot_token', 'signing_secret') if not config.get(key)]
|
||||
if missing:
|
||||
raise RuntimeError(f'Missing required Slack env vars for fields: {missing}')
|
||||
return config
|
||||
|
||||
|
||||
async def run_probe(args: argparse.Namespace):
|
||||
adapter = SlackAdapter(config_from_env(), ProbeLogger())
|
||||
events: list[platform_events.EBAEvent] = []
|
||||
api_results: list[dict[str, Any]] = []
|
||||
first_message = asyncio.Event()
|
||||
log_path = Path(args.log)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async def listener(event, adapter):
|
||||
events.append(event)
|
||||
summary = summarize_event(event)
|
||||
with log_path.open('a', encoding='utf-8') as fp:
|
||||
fp.write(json.dumps(summary, ensure_ascii=False, default=str) + '\n')
|
||||
print('SLACK_EBA_EVENT', json.dumps(summary, ensure_ascii=False, default=str))
|
||||
if isinstance(event, platform_events.MessageReceivedEvent):
|
||||
first_message.set()
|
||||
|
||||
adapter.register_listener(platform_events.EBAEvent, listener)
|
||||
|
||||
app = Quart(__name__)
|
||||
|
||||
@app.route('/callback', methods=['GET', 'POST'])
|
||||
async def callback():
|
||||
return await adapter.handle_unified_webhook('probe', '', request)
|
||||
|
||||
server_task = asyncio.create_task(app.run_task(host=args.host, port=args.port))
|
||||
try:
|
||||
await asyncio.wait_for(first_message.wait(), timeout=args.timeout)
|
||||
first = next(event for event in events if isinstance(event, platform_events.MessageReceivedEvent))
|
||||
await run_api(api_results, 'reply_message', lambda: adapter.reply_message(first, platform_message.MessageChain([platform_message.Plain(text=args.reply_text)])))
|
||||
await run_api(api_results, 'get_message', lambda: adapter.get_message(first.chat_type.value, first.chat_id, first.message_id))
|
||||
await run_api(api_results, 'get_user_info', lambda: adapter.get_user_info(first.sender.id))
|
||||
await run_api(api_results, 'get_friend_list', adapter.get_friend_list)
|
||||
if getattr(first, 'group', None):
|
||||
await run_api(api_results, 'get_group_info', lambda: adapter.get_group_info(first.group.id))
|
||||
await run_api(api_results, 'get_group_member_info', lambda: adapter.get_group_member_info(first.group.id, first.sender.id))
|
||||
await run_api(api_results, 'get_group_member_list', lambda: adapter.get_group_member_list(first.group.id))
|
||||
await run_api(api_results, 'call_platform_api.get_mode', lambda: adapter.call_platform_api('get_mode', {}))
|
||||
await run_api(api_results, 'call_platform_api.auth_test', lambda: adapter.call_platform_api('auth_test', {}))
|
||||
finally:
|
||||
server_task.cancel()
|
||||
try:
|
||||
await server_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
await adapter.kill()
|
||||
|
||||
summary = {
|
||||
'events': [event.type for event in events],
|
||||
'api_results': api_results,
|
||||
'log': str(log_path),
|
||||
}
|
||||
print('SLACK_EBA_SUMMARY', json.dumps(summary, ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Live Slack EBA adapter probe')
|
||||
parser.add_argument('--host', default='127.0.0.1')
|
||||
parser.add_argument('--port', type=int, default=5313)
|
||||
parser.add_argument('--timeout', type=float, default=300)
|
||||
parser.add_argument('--log', default='data/temp/slack_eba_probe.jsonl')
|
||||
parser.add_argument('--reply-text', default='Slack EBA probe reply')
|
||||
args = parser.parse_args()
|
||||
asyncio.run(run_probe(args))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,588 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import telegram
|
||||
|
||||
from langbot.pkg.platform.adapters.telegram.adapter import TelegramAdapter
|
||||
from langbot_plugin.api.definition.abstract.platform.event_logger import AbstractEventLogger
|
||||
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
|
||||
|
||||
|
||||
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'[warning] {text}')
|
||||
|
||||
async def error(self, text, images=None, message_session_id=None, no_throw=True):
|
||||
print(f'[error] {text}')
|
||||
|
||||
|
||||
PNG_1X1 = base64.b64decode(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII='
|
||||
)
|
||||
|
||||
|
||||
def summarize_event(event: platform_events.EBAEvent) -> dict:
|
||||
data = {
|
||||
'type': event.type,
|
||||
'adapter_name': event.adapter_name,
|
||||
'timestamp': event.timestamp,
|
||||
}
|
||||
for field in (
|
||||
'message_id',
|
||||
'chat_id',
|
||||
'chat_type',
|
||||
'reaction',
|
||||
'is_add',
|
||||
'action',
|
||||
'data',
|
||||
):
|
||||
if hasattr(event, field):
|
||||
value = getattr(event, field)
|
||||
if hasattr(value, 'value'):
|
||||
value = value.value
|
||||
data[field] = value
|
||||
if hasattr(event, 'member') and event.member is not None:
|
||||
data['member'] = event.member.model_dump()
|
||||
if hasattr(event, 'group') and event.group is not None:
|
||||
data['group'] = event.group.model_dump()
|
||||
if hasattr(event, 'operator') and event.operator is not None:
|
||||
data['operator'] = event.operator.model_dump()
|
||||
return data
|
||||
|
||||
|
||||
def chat_type_value(chat_type: platform_entities.ChatType | str) -> str:
|
||||
return chat_type.value if hasattr(chat_type, 'value') else str(chat_type)
|
||||
|
||||
|
||||
def record_api(results: list[dict[str, Any]], name: str, ok: bool, result: Any = None, error: Exception | None = None):
|
||||
entry = {'name': name, 'ok': ok}
|
||||
if result is not None:
|
||||
entry['result'] = redact_sensitive(result)
|
||||
if error is not None:
|
||||
entry['error'] = repr(error)
|
||||
results.append(entry)
|
||||
print('TELEGRAM_EBA_API', json.dumps(entry, ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
def redact_sensitive(value: Any) -> Any:
|
||||
if isinstance(value, str):
|
||||
return re.sub(r'bot\d+:[A-Za-z0-9_-]+', 'bot<redacted>', value)
|
||||
if isinstance(value, dict):
|
||||
return {key: redact_sensitive(item) for key, item in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [redact_sensitive(item) for item in value]
|
||||
if isinstance(value, int | float | bool) or value is None:
|
||||
return value
|
||||
return redact_sensitive(str(value))
|
||||
|
||||
|
||||
async def run_api(results: list[dict[str, Any]], name: str, func):
|
||||
try:
|
||||
result = await func()
|
||||
record_api(results, name, True, result)
|
||||
return result
|
||||
except Exception as exc:
|
||||
record_api(results, name, False, error=exc)
|
||||
return None
|
||||
|
||||
|
||||
async def run_expected_error(results: list[dict[str, Any]], name: str, func, error_type_name: str):
|
||||
try:
|
||||
await func()
|
||||
except Exception as exc:
|
||||
if type(exc).__name__ == error_type_name:
|
||||
record_api(results, name, True, {'expected_error': error_type_name})
|
||||
return
|
||||
record_api(results, name, False, error=exc)
|
||||
return
|
||||
record_api(results, name, False, error=RuntimeError(f'expected {error_type_name}'))
|
||||
|
||||
|
||||
async def run_probe(
|
||||
token: str,
|
||||
log_path: Path,
|
||||
timeout: int,
|
||||
group_chat_id: str | None,
|
||||
moderation_user_id: str | None,
|
||||
kick_user_id: str | None,
|
||||
allow_group_mutation: bool,
|
||||
allow_unpin_all: bool,
|
||||
allow_leave_group: bool,
|
||||
):
|
||||
adapter = TelegramAdapter(
|
||||
{
|
||||
'token': token,
|
||||
'markdown_card': False,
|
||||
'enable-stream-reply': False,
|
||||
},
|
||||
ProbeLogger(),
|
||||
)
|
||||
events: list[platform_events.EBAEvent] = []
|
||||
api_results: list[dict[str, Any]] = []
|
||||
first_message = asyncio.Event()
|
||||
callback_event = asyncio.Event()
|
||||
callback_query_id: str | None = None
|
||||
callback_probe_message_id: int | None = None
|
||||
awaiting_callback = False
|
||||
|
||||
async def listener(event, adapter):
|
||||
nonlocal callback_query_id
|
||||
events.append(event)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log_path.open('a', encoding='utf-8') as fp:
|
||||
fp.write(json.dumps(summarize_event(event), ensure_ascii=False) + '\n')
|
||||
print('TELEGRAM_EBA_EVENT', json.dumps(summarize_event(event), ensure_ascii=False))
|
||||
if isinstance(event, platform_events.MessageReceivedEvent):
|
||||
first_message.set()
|
||||
if isinstance(event, platform_events.PlatformSpecificEvent) and event.action == 'callback_query':
|
||||
message_id = event.data.get('message_id')
|
||||
if awaiting_callback and message_id == callback_probe_message_id:
|
||||
callback_query_id = event.data.get('callback_query_id')
|
||||
callback_event.set()
|
||||
|
||||
adapter.register_listener(platform_events.EBAEvent, listener)
|
||||
await adapter.run_async()
|
||||
|
||||
try:
|
||||
print('READY: send a private or group message to the Telegram test bot now.')
|
||||
await asyncio.wait_for(first_message.wait(), timeout=timeout)
|
||||
source = next(event for event in events if isinstance(event, platform_events.MessageReceivedEvent))
|
||||
source_chat_type = chat_type_value(source.chat_type)
|
||||
group_id = group_chat_id or (str(source.chat_id) if source_chat_type == 'group' else None)
|
||||
actor_user_id = str(source.sender.id)
|
||||
target_chat_id = str(source.chat_id)
|
||||
|
||||
await run_api(
|
||||
api_results,
|
||||
'reply_message:text_image_file',
|
||||
lambda: adapter.reply_message(
|
||||
source,
|
||||
platform_message.MessageChain(
|
||||
[
|
||||
platform_message.Plain(text='EBA live reply: text'),
|
||||
platform_message.Image(base64=base64.b64encode(PNG_1X1).decode()),
|
||||
platform_message.File(
|
||||
name='eba-live.txt',
|
||||
size=8,
|
||||
base64='data:text/plain;base64,' + base64.b64encode(b'eba-live').decode(),
|
||||
),
|
||||
]
|
||||
),
|
||||
quote_origin=True,
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'send_message:text_image_file',
|
||||
lambda: adapter.send_message(
|
||||
source_chat_type,
|
||||
target_chat_id,
|
||||
platform_message.MessageChain(
|
||||
[
|
||||
platform_message.Plain(text='EBA live send_message OK'),
|
||||
platform_message.Image(base64=base64.b64encode(PNG_1X1).decode()),
|
||||
platform_message.File(
|
||||
name='eba-send-live.txt',
|
||||
size=13,
|
||||
base64='data:text/plain;base64,' + base64.b64encode(b'eba-send-live').decode(),
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
edit_probe = await run_api(
|
||||
api_results,
|
||||
'bot.send_message:edit_delete_probe',
|
||||
lambda: adapter.bot.send_message(chat_id=target_chat_id, text='EBA edit/delete probe'),
|
||||
)
|
||||
if edit_probe:
|
||||
await run_api(
|
||||
api_results,
|
||||
'edit_message',
|
||||
lambda: adapter.edit_message(
|
||||
source_chat_type,
|
||||
target_chat_id,
|
||||
edit_probe.message_id,
|
||||
platform_message.MessageChain([platform_message.Plain(text='EBA edit probe edited')]),
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'delete_message',
|
||||
lambda: adapter.delete_message(source_chat_type, target_chat_id, edit_probe.message_id),
|
||||
)
|
||||
|
||||
forward_probe = await run_api(
|
||||
api_results,
|
||||
'bot.send_message:forward_probe',
|
||||
lambda: adapter.bot.send_message(chat_id=target_chat_id, text='EBA forward probe'),
|
||||
)
|
||||
if forward_probe:
|
||||
forwarded = await run_api(
|
||||
api_results,
|
||||
'forward_message',
|
||||
lambda: adapter.forward_message(
|
||||
source_chat_type,
|
||||
target_chat_id,
|
||||
forward_probe.message_id,
|
||||
source_chat_type,
|
||||
target_chat_id,
|
||||
),
|
||||
)
|
||||
if forwarded:
|
||||
await run_api(
|
||||
api_results,
|
||||
'delete_message:forwarded_cleanup',
|
||||
lambda: adapter.delete_message(source_chat_type, target_chat_id, forwarded.message_id),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'delete_message:forward_source_cleanup',
|
||||
lambda: adapter.delete_message(source_chat_type, target_chat_id, forward_probe.message_id),
|
||||
)
|
||||
|
||||
document_probe = await run_api(
|
||||
api_results,
|
||||
'bot.send_document:get_file_url_probe',
|
||||
lambda: adapter.bot.send_document(
|
||||
chat_id=target_chat_id,
|
||||
document=telegram.InputFile(b'eba-file-url', filename='eba-file-url.txt'),
|
||||
),
|
||||
)
|
||||
if document_probe and document_probe.document:
|
||||
await run_api(
|
||||
api_results,
|
||||
'get_file_url',
|
||||
lambda: adapter.get_file_url(document_probe.document.file_id),
|
||||
)
|
||||
|
||||
await run_api(
|
||||
api_results,
|
||||
'get_user_info',
|
||||
lambda: adapter.get_user_info(actor_user_id),
|
||||
)
|
||||
await run_expected_error(
|
||||
api_results,
|
||||
'upload_file:not_supported',
|
||||
lambda: adapter.upload_file(b'eba-upload-live', 'eba-upload-live.txt'),
|
||||
'NotSupportedError',
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:send_chat_action',
|
||||
lambda: adapter.call_platform_api('send_chat_action', {'chat_id': target_chat_id, 'action': 'typing'}),
|
||||
)
|
||||
|
||||
callback_probe = await run_api(
|
||||
api_results,
|
||||
'bot.send_message:callback_probe',
|
||||
lambda: adapter.bot.send_message(
|
||||
chat_id=target_chat_id,
|
||||
text='EBA callback probe',
|
||||
reply_markup=telegram.InlineKeyboardMarkup(
|
||||
[[telegram.InlineKeyboardButton('callback probe', callback_data='eba-callback-probe')]]
|
||||
),
|
||||
),
|
||||
)
|
||||
if callback_probe:
|
||||
callback_probe_message_id = callback_probe.message_id
|
||||
awaiting_callback = True
|
||||
callback_event.clear()
|
||||
print('READY: click the callback button to test answer_callback_query.')
|
||||
try:
|
||||
await asyncio.wait_for(callback_event.wait(), timeout=max(15, timeout // 3))
|
||||
except asyncio.TimeoutError:
|
||||
record_api(
|
||||
api_results,
|
||||
'call_platform_api:answer_callback_query',
|
||||
False,
|
||||
error=TimeoutError('callback button was not clicked before timeout'),
|
||||
)
|
||||
else:
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:answer_callback_query',
|
||||
lambda: adapter.call_platform_api(
|
||||
'answer_callback_query',
|
||||
{'callback_query_id': callback_query_id, 'text': 'EBA callback answered'},
|
||||
),
|
||||
)
|
||||
finally:
|
||||
awaiting_callback = False
|
||||
|
||||
if group_id:
|
||||
await run_api(api_results, 'get_group_info', lambda: adapter.get_group_info(group_id))
|
||||
await run_api(api_results, 'get_group_member_list', lambda: adapter.get_group_member_list(group_id))
|
||||
await run_api(
|
||||
api_results,
|
||||
'get_group_member_info',
|
||||
lambda: adapter.get_group_member_info(group_id, actor_user_id),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:get_chat_administrators',
|
||||
lambda: adapter.call_platform_api('get_chat_administrators', {'chat_id': group_id}),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:get_chat_member_count',
|
||||
lambda: adapter.call_platform_api('get_chat_member_count', {'chat_id': group_id}),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:create_chat_invite_link',
|
||||
lambda: adapter.call_platform_api(
|
||||
'create_chat_invite_link', {'chat_id': group_id, 'name': 'eba-probe'}
|
||||
),
|
||||
)
|
||||
|
||||
pin_probe = await run_api(
|
||||
api_results,
|
||||
'bot.send_message:pin_probe',
|
||||
lambda: adapter.bot.send_message(chat_id=group_id, text='EBA pin probe'),
|
||||
)
|
||||
if pin_probe:
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:pin_message',
|
||||
lambda: adapter.call_platform_api(
|
||||
'pin_message',
|
||||
{'chat_id': group_id, 'message_id': pin_probe.message_id, 'disable_notification': True},
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:unpin_message',
|
||||
lambda: adapter.call_platform_api(
|
||||
'unpin_message',
|
||||
{'chat_id': group_id, 'message_id': pin_probe.message_id},
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'delete_message:pin_probe_cleanup',
|
||||
lambda: adapter.delete_message('group', group_id, pin_probe.message_id),
|
||||
)
|
||||
|
||||
if allow_unpin_all:
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:unpin_all_messages',
|
||||
lambda: adapter.call_platform_api('unpin_all_messages', {'chat_id': group_id}),
|
||||
)
|
||||
else:
|
||||
record_api(api_results, 'call_platform_api:unpin_all_messages', False, error=RuntimeError('skipped'))
|
||||
|
||||
if allow_group_mutation:
|
||||
chat = await adapter.bot.get_chat(chat_id=group_id)
|
||||
original_title = chat.title or 'EBA Probe Group'
|
||||
original_description = chat.description or ''
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:set_chat_title',
|
||||
lambda: adapter.call_platform_api(
|
||||
'set_chat_title',
|
||||
{'chat_id': group_id, 'title': f'{original_title} EBA'},
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:set_chat_title:restore',
|
||||
lambda: adapter.call_platform_api('set_chat_title', {'chat_id': group_id, 'title': original_title}),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:set_chat_description',
|
||||
lambda: adapter.call_platform_api(
|
||||
'set_chat_description',
|
||||
{'chat_id': group_id, 'description': 'EBA probe temporary description'},
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:set_chat_description:restore',
|
||||
lambda: adapter.call_platform_api(
|
||||
'set_chat_description',
|
||||
{'chat_id': group_id, 'description': original_description},
|
||||
),
|
||||
)
|
||||
else:
|
||||
record_api(api_results, 'call_platform_api:set_chat_title', False, error=RuntimeError('skipped'))
|
||||
record_api(api_results, 'call_platform_api:set_chat_description', False, error=RuntimeError('skipped'))
|
||||
|
||||
if moderation_user_id:
|
||||
await run_api(
|
||||
api_results,
|
||||
'mute_member',
|
||||
lambda: adapter.mute_member(group_id, moderation_user_id, duration=30),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'unmute_member',
|
||||
lambda: adapter.unmute_member(group_id, moderation_user_id),
|
||||
)
|
||||
else:
|
||||
record_api(api_results, 'mute_member', False, error=RuntimeError('skipped'))
|
||||
record_api(api_results, 'unmute_member', False, error=RuntimeError('skipped'))
|
||||
|
||||
if kick_user_id:
|
||||
await run_api(api_results, 'kick_member', lambda: adapter.kick_member(group_id, kick_user_id))
|
||||
else:
|
||||
record_api(api_results, 'kick_member', False, error=RuntimeError('skipped'))
|
||||
|
||||
if allow_leave_group:
|
||||
await run_api(api_results, 'leave_group', lambda: adapter.leave_group(group_id))
|
||||
else:
|
||||
record_api(api_results, 'leave_group', False, error=RuntimeError('skipped'))
|
||||
else:
|
||||
for name in (
|
||||
'get_group_info',
|
||||
'get_group_member_list',
|
||||
'get_group_member_info',
|
||||
'mute_member',
|
||||
'unmute_member',
|
||||
'kick_member',
|
||||
'leave_group',
|
||||
'call_platform_api:get_chat_administrators',
|
||||
'call_platform_api:get_chat_member_count',
|
||||
'call_platform_api:create_chat_invite_link',
|
||||
'call_platform_api:pin_message',
|
||||
'call_platform_api:unpin_message',
|
||||
'call_platform_api:unpin_all_messages',
|
||||
'call_platform_api:set_chat_title',
|
||||
'call_platform_api:set_chat_description',
|
||||
):
|
||||
record_api(api_results, name, False, error=RuntimeError('skipped: no group chat id'))
|
||||
|
||||
await asyncio.sleep(3)
|
||||
finally:
|
||||
await adapter.kill()
|
||||
summary = {
|
||||
'events': [summarize_event(event) for event in events],
|
||||
'event_types': [event.type for event in events],
|
||||
'api_results': api_results,
|
||||
'api_passed': [result['name'] for result in api_results if result['ok']],
|
||||
'api_failed': [result for result in api_results if not result['ok']],
|
||||
}
|
||||
print('SUMMARY', json.dumps(summary, ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
async def run_callback_probe(token: str, chat_id: str, timeout: int):
|
||||
adapter = TelegramAdapter(
|
||||
{
|
||||
'token': token,
|
||||
'markdown_card': False,
|
||||
'enable-stream-reply': False,
|
||||
},
|
||||
ProbeLogger(),
|
||||
)
|
||||
api_results: list[dict[str, Any]] = []
|
||||
|
||||
callback_probe = await adapter.bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text='EBA callback-only probe',
|
||||
reply_markup=telegram.InlineKeyboardMarkup(
|
||||
[[telegram.InlineKeyboardButton('callback probe', callback_data='eba-callback-only-probe')]]
|
||||
),
|
||||
)
|
||||
deadline = asyncio.get_running_loop().time() + timeout
|
||||
offset: int | None = None
|
||||
try:
|
||||
print('READY: click the callback-only probe button.')
|
||||
callback_query_id: str | None = None
|
||||
while asyncio.get_running_loop().time() < deadline and callback_query_id is None:
|
||||
updates = await adapter.bot.get_updates(
|
||||
offset=offset,
|
||||
timeout=2,
|
||||
allowed_updates=['callback_query'],
|
||||
)
|
||||
for update in updates:
|
||||
offset = update.update_id + 1
|
||||
callback_query = update.callback_query
|
||||
if callback_query is None or callback_query.message is None:
|
||||
continue
|
||||
if callback_query.message.message_id == callback_probe.message_id:
|
||||
callback_query_id = callback_query.id
|
||||
break
|
||||
if callback_query_id is None:
|
||||
raise TimeoutError('callback button was not clicked before timeout')
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:answer_callback_query',
|
||||
lambda: adapter.call_platform_api(
|
||||
'answer_callback_query',
|
||||
{'callback_query_id': callback_query_id, 'text': 'EBA callback answered'},
|
||||
),
|
||||
)
|
||||
finally:
|
||||
print(
|
||||
'SUMMARY',
|
||||
json.dumps(
|
||||
{
|
||||
'api_results': api_results,
|
||||
'api_passed': [result['name'] for result in api_results if result['ok']],
|
||||
'api_failed': [result for result in api_results if not result['ok']],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--token', default=os.getenv('TELEGRAM_BOT_TOKEN', ''))
|
||||
parser.add_argument('--log', default='data/temp/live_telegram_eba_probe.jsonl')
|
||||
parser.add_argument('--timeout', type=int, default=90)
|
||||
parser.add_argument('--group-chat-id', default=os.getenv('TELEGRAM_EBA_GROUP_CHAT_ID'))
|
||||
parser.add_argument('--moderation-user-id', default=os.getenv('TELEGRAM_EBA_MODERATION_USER_ID'))
|
||||
parser.add_argument('--kick-user-id', default=os.getenv('TELEGRAM_EBA_KICK_USER_ID'))
|
||||
parser.add_argument('--allow-group-mutation', action='store_true')
|
||||
parser.add_argument('--allow-unpin-all', action='store_true')
|
||||
parser.add_argument('--allow-leave-group', action='store_true')
|
||||
parser.add_argument('--callback-chat-id', default=os.getenv('TELEGRAM_EBA_CALLBACK_CHAT_ID'))
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.token:
|
||||
raise SystemExit('Set TELEGRAM_BOT_TOKEN or pass --token')
|
||||
|
||||
log_path = Path(args.log)
|
||||
if log_path.exists():
|
||||
log_path.unlink()
|
||||
if args.callback_chat_id:
|
||||
asyncio.run(run_callback_probe(args.token, args.callback_chat_id, args.timeout))
|
||||
return
|
||||
asyncio.run(
|
||||
run_probe(
|
||||
args.token,
|
||||
log_path,
|
||||
args.timeout,
|
||||
args.group_chat_id,
|
||||
args.moderation_user_id,
|
||||
args.kick_user_id,
|
||||
args.allow_group_mutation,
|
||||
args.allow_unpin_all,
|
||||
args.allow_leave_group,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,214 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from quart import Quart, request
|
||||
|
||||
from langbot.pkg.platform.adapters.wecom.adapter import WecomAdapter
|
||||
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
|
||||
|
||||
|
||||
TINY_PNG = (
|
||||
'data:image/png;base64,'
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII='
|
||||
)
|
||||
|
||||
|
||||
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'[warning] {text}')
|
||||
|
||||
async def error(self, text, images=None, message_session_id=None, no_throw=True):
|
||||
print(f'[error] {text}')
|
||||
|
||||
|
||||
def redact(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
redacted = {}
|
||||
for key, item in value.items():
|
||||
if key.lower() in {'secret', 'token', 'encodingaeskey', 'access_token'}:
|
||||
redacted[key] = '<redacted>'
|
||||
else:
|
||||
redacted[key] = redact(item)
|
||||
return redacted
|
||||
if isinstance(value, list):
|
||||
return [redact(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def summarize_event(event: platform_events.EBAEvent) -> dict:
|
||||
data = {
|
||||
'type': event.type,
|
||||
'adapter_name': event.adapter_name,
|
||||
'timestamp': event.timestamp,
|
||||
}
|
||||
for field in ('message_id', 'chat_id', 'chat_type', 'action', 'data'):
|
||||
if hasattr(event, field):
|
||||
value = getattr(event, field)
|
||||
if hasattr(value, 'value'):
|
||||
value = value.value
|
||||
data[field] = redact(value)
|
||||
if hasattr(event, 'sender') and event.sender is not None:
|
||||
data['sender'] = event.sender.model_dump()
|
||||
if hasattr(event, 'message_chain') and event.message_chain is not None:
|
||||
data['message_chain'] = event.message_chain.model_dump()
|
||||
return data
|
||||
|
||||
|
||||
def record_api(results: list[dict[str, Any]], name: str, ok: bool, result: Any = None, error: Exception | None = None):
|
||||
entry = {'name': name, 'ok': ok}
|
||||
if result is not None:
|
||||
entry['result'] = redact(result)
|
||||
if error is not None:
|
||||
entry['error'] = repr(error)
|
||||
results.append(entry)
|
||||
print('WECOM_EBA_API', json.dumps(entry, ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
async def run_api(results: list[dict[str, Any]], name: str, func):
|
||||
try:
|
||||
result = await func()
|
||||
record_api(results, name, True, result)
|
||||
return result
|
||||
except Exception as exc:
|
||||
record_api(results, name, False, error=exc)
|
||||
return None
|
||||
|
||||
|
||||
def config_from_env() -> dict:
|
||||
required = {
|
||||
'corpid': os.getenv('WECOM_CORPID', ''),
|
||||
'secret': os.getenv('WECOM_SECRET', ''),
|
||||
'token': os.getenv('WECOM_TOKEN', ''),
|
||||
'EncodingAESKey': os.getenv('WECOM_ENCODING_AES_KEY', ''),
|
||||
}
|
||||
missing = [key for key, value in required.items() if not value]
|
||||
if missing:
|
||||
raise RuntimeError(f'Missing required WeCom env vars for fields: {missing}')
|
||||
return {
|
||||
**required,
|
||||
'contacts_secret': os.getenv('WECOM_CONTACTS_SECRET', ''),
|
||||
'api_base_url': os.getenv('WECOM_API_BASE_URL', 'https://qyapi.weixin.qq.com/cgi-bin'),
|
||||
}
|
||||
|
||||
|
||||
async def run_probe(args: argparse.Namespace):
|
||||
adapter = WecomAdapter(config_from_env(), ProbeLogger())
|
||||
events: list[platform_events.EBAEvent] = []
|
||||
api_results: list[dict[str, Any]] = []
|
||||
first_message = asyncio.Event()
|
||||
log_path = Path(args.log)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async def listener(event, adapter):
|
||||
events.append(event)
|
||||
with log_path.open('a', encoding='utf-8') as fp:
|
||||
fp.write(json.dumps(summarize_event(event), ensure_ascii=False, default=str) + '\n')
|
||||
print('WECOM_EBA_EVENT', json.dumps(summarize_event(event), ensure_ascii=False, default=str))
|
||||
if isinstance(event, platform_events.MessageReceivedEvent):
|
||||
first_message.set()
|
||||
|
||||
adapter.register_listener(platform_events.EBAEvent, listener)
|
||||
|
||||
app = Quart(__name__)
|
||||
|
||||
@app.route(args.path, methods=['GET', 'POST'])
|
||||
async def callback():
|
||||
return await adapter.handle_unified_webhook(args.bot_uuid, '', request)
|
||||
|
||||
server_task = asyncio.create_task(app.run_task(host=args.host, port=args.port))
|
||||
try:
|
||||
print(f'READY: configure WeCom callback URL to http://{args.host}:{args.port}{args.path}')
|
||||
print('READY: send a real WeCom application message to the bot now.')
|
||||
await asyncio.wait_for(first_message.wait(), timeout=args.timeout)
|
||||
|
||||
source = next(event for event in events if isinstance(event, platform_events.MessageReceivedEvent))
|
||||
raw_event = source.source_platform_object
|
||||
target_id = f'{source.chat_id}|{raw_event.agent_id}'
|
||||
|
||||
if not args.skip_api:
|
||||
await run_api(
|
||||
api_results,
|
||||
'reply_message:text',
|
||||
lambda: adapter.reply_message(
|
||||
source,
|
||||
platform_message.MessageChain([platform_message.Plain(text='WeCom EBA probe reply')]),
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'send_message:text',
|
||||
lambda: adapter.send_message(
|
||||
'person',
|
||||
target_id,
|
||||
platform_message.MessageChain([platform_message.Plain(text='WeCom EBA probe send')]),
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'send_message:image',
|
||||
lambda: adapter.send_message(
|
||||
'person',
|
||||
target_id,
|
||||
platform_message.MessageChain(
|
||||
[
|
||||
platform_message.Plain(text='WeCom EBA probe image'),
|
||||
platform_message.Image(base64=TINY_PNG),
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'get_message',
|
||||
lambda: adapter.get_message('private', source.chat_id, source.message_id),
|
||||
)
|
||||
await run_api(api_results, 'get_user_info', lambda: adapter.get_user_info(source.sender.id))
|
||||
await run_api(api_results, 'get_friend_list', lambda: adapter.get_friend_list())
|
||||
await run_api(api_results, 'call_platform_api:check_access_token', lambda: adapter.call_platform_api('check_access_token', {}))
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:get_user_info',
|
||||
lambda: adapter.call_platform_api('get_user_info', {'user_id': source.sender.id}),
|
||||
)
|
||||
|
||||
summary = {
|
||||
'events': [event.type for event in events],
|
||||
'api_results': api_results,
|
||||
'log_path': str(log_path),
|
||||
}
|
||||
print('WECOM_EBA_SUMMARY', json.dumps(summary, ensure_ascii=False, default=str))
|
||||
return summary
|
||||
finally:
|
||||
server_task.cancel()
|
||||
await adapter.kill()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Live WeCom EBA adapter probe.')
|
||||
parser.add_argument('--host', default='0.0.0.0')
|
||||
parser.add_argument('--port', type=int, default=5312)
|
||||
parser.add_argument('--path', default='/wecom/callback')
|
||||
parser.add_argument('--timeout', type=int, default=180)
|
||||
parser.add_argument('--bot-uuid', default='wecom-eba-live-probe')
|
||||
parser.add_argument('--log', default='data/temp/wecom_eba_live_probe.jsonl')
|
||||
parser.add_argument('--skip-api', action='store_true')
|
||||
args = parser.parse_args()
|
||||
asyncio.run(run_probe(args))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,203 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from quart import Quart, request
|
||||
|
||||
from langbot.pkg.platform.adapters.wecombot.adapter import WecomBotAdapter
|
||||
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'[warning] {text}')
|
||||
|
||||
async def error(self, text, images=None, message_session_id=None, no_throw=True):
|
||||
print(f'[error] {text}')
|
||||
|
||||
|
||||
def redact(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: '<redacted>' if key.lower() in {'secret', 'token', 'encodingaeskey', 'encrypt', 'aeskey'} else redact(item)
|
||||
for key, item in value.items()
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [redact(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def summarize_event(event: platform_events.EBAEvent) -> dict:
|
||||
data = {
|
||||
'type': event.type,
|
||||
'adapter_name': event.adapter_name,
|
||||
'timestamp': event.timestamp,
|
||||
}
|
||||
for field in ('message_id', 'chat_id', 'chat_type', 'action', 'data', 'feedback_id', 'feedback_type'):
|
||||
if hasattr(event, field):
|
||||
value = getattr(event, field)
|
||||
if hasattr(value, 'value'):
|
||||
value = value.value
|
||||
data[field] = redact(value)
|
||||
if hasattr(event, 'sender') and event.sender is not None:
|
||||
data['sender'] = event.sender.model_dump()
|
||||
if hasattr(event, 'group') and event.group is not None:
|
||||
data['group'] = event.group.model_dump()
|
||||
if hasattr(event, 'message_chain') and event.message_chain is not None:
|
||||
data['message_chain'] = event.message_chain.model_dump()
|
||||
return data
|
||||
|
||||
|
||||
def record_api(results: list[dict[str, Any]], name: str, ok: bool, result: Any = None, error: Exception | None = None):
|
||||
entry = {'name': name, 'ok': ok}
|
||||
if result is not None:
|
||||
entry['result'] = redact(result)
|
||||
if error is not None:
|
||||
entry['error'] = repr(error)
|
||||
results.append(entry)
|
||||
print('WECOMBOT_EBA_API', json.dumps(entry, ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
async def run_api(results: list[dict[str, Any]], name: str, func):
|
||||
try:
|
||||
result = await func()
|
||||
record_api(results, name, True, result)
|
||||
return result
|
||||
except Exception as exc:
|
||||
record_api(results, name, False, error=exc)
|
||||
return None
|
||||
|
||||
|
||||
def config_from_env(enable_webhook: bool) -> dict:
|
||||
config = {
|
||||
'BotId': os.getenv('WECOMBOT_BOT_ID', ''),
|
||||
'robot_name': os.getenv('WECOMBOT_ROBOT_NAME', ''),
|
||||
'enable-webhook': enable_webhook,
|
||||
'Secret': os.getenv('WECOMBOT_SECRET', ''),
|
||||
'Token': os.getenv('WECOMBOT_TOKEN', ''),
|
||||
'EncodingAESKey': os.getenv('WECOMBOT_ENCODING_AES_KEY', ''),
|
||||
'Corpid': os.getenv('WECOMBOT_CORPID', ''),
|
||||
'enable-stream-reply': os.getenv('WECOMBOT_ENABLE_STREAM_REPLY', '1') != '0',
|
||||
}
|
||||
required = ['BotId', 'Secret'] if not enable_webhook else ['Token', 'EncodingAESKey', 'Corpid']
|
||||
missing = [key for key in required if not config.get(key)]
|
||||
if missing:
|
||||
raise RuntimeError(f'Missing required WeComBot env vars for fields: {missing}')
|
||||
return config
|
||||
|
||||
|
||||
async def run_probe(args: argparse.Namespace):
|
||||
adapter = WecomBotAdapter(config_from_env(args.webhook), ProbeLogger())
|
||||
events: list[platform_events.EBAEvent] = []
|
||||
api_results: list[dict[str, Any]] = []
|
||||
first_message = asyncio.Event()
|
||||
log_path = Path(args.log)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async def listener(event, adapter):
|
||||
events.append(event)
|
||||
with log_path.open('a', encoding='utf-8') as fp:
|
||||
fp.write(json.dumps(summarize_event(event), ensure_ascii=False, default=str) + '\n')
|
||||
print('WECOMBOT_EBA_EVENT', json.dumps(summarize_event(event), ensure_ascii=False, default=str))
|
||||
if isinstance(event, platform_events.MessageReceivedEvent):
|
||||
first_message.set()
|
||||
|
||||
adapter.register_listener(platform_events.EBAEvent, listener)
|
||||
|
||||
run_task = None
|
||||
server_task = None
|
||||
if args.webhook:
|
||||
app = Quart(__name__)
|
||||
|
||||
@app.route(args.path, methods=['GET', 'POST'])
|
||||
async def callback():
|
||||
return await adapter.handle_unified_webhook(args.bot_uuid, '', request)
|
||||
|
||||
server_task = asyncio.create_task(app.run_task(host=args.host, port=args.port))
|
||||
print(f'READY: configure WeComBot callback URL to http://{args.host}:{args.port}{args.path}')
|
||||
else:
|
||||
run_task = asyncio.create_task(adapter.run_async())
|
||||
print('READY: WeComBot WebSocket long connection started; no webhook URL is required.')
|
||||
|
||||
try:
|
||||
print('READY: send a real WeComBot message to the bot now.')
|
||||
await asyncio.wait_for(first_message.wait(), timeout=args.timeout)
|
||||
|
||||
source = next(event for event in events if isinstance(event, platform_events.MessageReceivedEvent))
|
||||
|
||||
if not args.skip_api:
|
||||
await run_api(
|
||||
api_results,
|
||||
'reply_message:text',
|
||||
lambda: adapter.reply_message(
|
||||
source,
|
||||
platform_message.MessageChain([platform_message.Plain(text='WeComBot EBA probe reply')]),
|
||||
),
|
||||
)
|
||||
if not args.webhook:
|
||||
await run_api(
|
||||
api_results,
|
||||
'send_message:text',
|
||||
lambda: adapter.send_message(
|
||||
'group' if source.chat_type.value == 'group' else 'person',
|
||||
source.chat_id,
|
||||
platform_message.MessageChain([platform_message.Plain(text='WeComBot EBA probe send')]),
|
||||
),
|
||||
)
|
||||
await run_api(api_results, 'get_message', lambda: adapter.get_message(source.chat_type.value, source.chat_id, source.message_id))
|
||||
await run_api(api_results, 'get_user_info', lambda: adapter.get_user_info(source.sender.id))
|
||||
if source.group:
|
||||
await run_api(api_results, 'get_group_info', lambda: adapter.get_group_info(source.group.id))
|
||||
await run_api(api_results, 'get_group_member_list', lambda: adapter.get_group_member_list(source.group.id))
|
||||
await run_api(api_results, 'call_platform_api:is_websocket_mode', lambda: adapter.call_platform_api('is_websocket_mode', {}))
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:get_stream_session_status',
|
||||
lambda: adapter.call_platform_api('get_stream_session_status', {'message_id': source.message_id}),
|
||||
)
|
||||
|
||||
summary = {
|
||||
'events': [event.type for event in events],
|
||||
'api_results': api_results,
|
||||
'log_path': str(log_path),
|
||||
'mode': 'webhook' if args.webhook else 'websocket',
|
||||
}
|
||||
print('WECOMBOT_EBA_SUMMARY', json.dumps(summary, ensure_ascii=False, default=str))
|
||||
return summary
|
||||
finally:
|
||||
if server_task:
|
||||
server_task.cancel()
|
||||
if run_task:
|
||||
run_task.cancel()
|
||||
await adapter.kill()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Live WeComBot EBA adapter probe.')
|
||||
parser.add_argument('--webhook', action='store_true', help='Use webhook mode. Default is WebSocket long connection mode.')
|
||||
parser.add_argument('--host', default='0.0.0.0')
|
||||
parser.add_argument('--port', type=int, default=5313)
|
||||
parser.add_argument('--path', default='/wecombot/callback')
|
||||
parser.add_argument('--timeout', type=int, default=180)
|
||||
parser.add_argument('--bot-uuid', default='wecombot-eba-live-probe')
|
||||
parser.add_argument('--log', default='data/temp/wecombot_eba_live_probe.jsonl')
|
||||
parser.add_argument('--skip-api', action='store_true')
|
||||
args = parser.parse_args()
|
||||
asyncio.run(run_probe(args))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,211 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from quart import Quart, request
|
||||
|
||||
from langbot.pkg.platform.adapters.wecomcs.adapter import WecomCSAdapter
|
||||
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
|
||||
|
||||
|
||||
TINY_PNG = (
|
||||
'data:image/png;base64,'
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII='
|
||||
)
|
||||
|
||||
|
||||
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'[warning] {text}')
|
||||
|
||||
async def error(self, text, images=None, message_session_id=None, no_throw=True):
|
||||
print(f'[error] {text}')
|
||||
|
||||
|
||||
def redact(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
redacted = {}
|
||||
for key, item in value.items():
|
||||
if key.lower() in {'secret', 'token', 'encodingaeskey', 'access_token'}:
|
||||
redacted[key] = '<redacted>'
|
||||
else:
|
||||
redacted[key] = redact(item)
|
||||
return redacted
|
||||
if isinstance(value, list):
|
||||
return [redact(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def summarize_event(event: platform_events.EBAEvent) -> dict:
|
||||
data = {
|
||||
'type': event.type,
|
||||
'adapter_name': event.adapter_name,
|
||||
'timestamp': event.timestamp,
|
||||
}
|
||||
for field in ('message_id', 'chat_id', 'chat_type', 'action', 'data'):
|
||||
if hasattr(event, field):
|
||||
value = getattr(event, field)
|
||||
if hasattr(value, 'value'):
|
||||
value = value.value
|
||||
data[field] = redact(value)
|
||||
if hasattr(event, 'sender') and event.sender is not None:
|
||||
data['sender'] = event.sender.model_dump()
|
||||
if hasattr(event, 'message_chain') and event.message_chain is not None:
|
||||
data['message_chain'] = event.message_chain.model_dump()
|
||||
return data
|
||||
|
||||
|
||||
def record_api(results: list[dict[str, Any]], name: str, ok: bool, result: Any = None, error: Exception | None = None):
|
||||
entry = {'name': name, 'ok': ok}
|
||||
if result is not None:
|
||||
entry['result'] = redact(result)
|
||||
if error is not None:
|
||||
entry['error'] = repr(error)
|
||||
results.append(entry)
|
||||
print('WECOMCS_EBA_API', json.dumps(entry, ensure_ascii=False, default=str))
|
||||
|
||||
|
||||
async def run_api(results: list[dict[str, Any]], name: str, func):
|
||||
try:
|
||||
result = await func()
|
||||
record_api(results, name, True, result)
|
||||
return result
|
||||
except Exception as exc:
|
||||
record_api(results, name, False, error=exc)
|
||||
return None
|
||||
|
||||
|
||||
def config_from_env() -> dict:
|
||||
required = {
|
||||
'corpid': os.getenv('WECOMCS_CORPID') or os.getenv('WECOM_CORPID', ''),
|
||||
'secret': os.getenv('WECOMCS_SECRET') or os.getenv('WECOMCS_KF_SECRET', ''),
|
||||
'token': os.getenv('WECOMCS_TOKEN', ''),
|
||||
'EncodingAESKey': os.getenv('WECOMCS_ENCODING_AES_KEY', ''),
|
||||
}
|
||||
missing = [key for key, value in required.items() if not value]
|
||||
if missing:
|
||||
raise RuntimeError(f'Missing required WeComCS env vars for fields: {missing}')
|
||||
return {
|
||||
**required,
|
||||
'api_base_url': os.getenv('WECOMCS_API_BASE_URL', 'https://qyapi.weixin.qq.com/cgi-bin'),
|
||||
}
|
||||
|
||||
|
||||
async def run_probe(args: argparse.Namespace):
|
||||
adapter = WecomCSAdapter(config_from_env(), ProbeLogger())
|
||||
events: list[platform_events.EBAEvent] = []
|
||||
api_results: list[dict[str, Any]] = []
|
||||
first_message = asyncio.Event()
|
||||
log_path = Path(args.log)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async def listener(event, adapter):
|
||||
events.append(event)
|
||||
with log_path.open('a', encoding='utf-8') as fp:
|
||||
fp.write(json.dumps(summarize_event(event), ensure_ascii=False, default=str) + '\n')
|
||||
print('WECOMCS_EBA_EVENT', json.dumps(summarize_event(event), ensure_ascii=False, default=str))
|
||||
if isinstance(event, platform_events.MessageReceivedEvent):
|
||||
first_message.set()
|
||||
|
||||
adapter.register_listener(platform_events.EBAEvent, listener)
|
||||
|
||||
app = Quart(__name__)
|
||||
|
||||
@app.route(args.path, methods=['GET', 'POST'])
|
||||
async def callback():
|
||||
return await adapter.handle_unified_webhook(args.bot_uuid, '', request)
|
||||
|
||||
server_task = asyncio.create_task(app.run_task(host=args.host, port=args.port))
|
||||
try:
|
||||
print(f'READY: configure WeCom Customer Service callback URL to http://{args.host}:{args.port}{args.path}')
|
||||
print('READY: send a real customer-service message from WeCom/WeChat UI to the bot now.')
|
||||
await asyncio.wait_for(first_message.wait(), timeout=args.timeout)
|
||||
|
||||
source = next(event for event in events if isinstance(event, platform_events.MessageReceivedEvent))
|
||||
|
||||
if not args.skip_api:
|
||||
await run_api(
|
||||
api_results,
|
||||
'reply_message:text',
|
||||
lambda: adapter.reply_message(
|
||||
source,
|
||||
platform_message.MessageChain([platform_message.Plain(text='WeComCS EBA probe reply')]),
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'send_message:text',
|
||||
lambda: adapter.send_message(
|
||||
'person',
|
||||
source.chat_id,
|
||||
platform_message.MessageChain([platform_message.Plain(text='WeComCS EBA probe send')]),
|
||||
),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'send_message:image',
|
||||
lambda: adapter.send_message(
|
||||
'person',
|
||||
source.chat_id,
|
||||
platform_message.MessageChain(
|
||||
[
|
||||
platform_message.Plain(text='WeComCS EBA probe image'),
|
||||
platform_message.Image(base64=TINY_PNG),
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
await run_api(api_results, 'get_message', lambda: adapter.get_message('private', source.chat_id, source.message_id))
|
||||
await run_api(api_results, 'get_user_info', lambda: adapter.get_user_info(source.sender.id))
|
||||
await run_api(api_results, 'get_friend_list', lambda: adapter.get_friend_list())
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:check_access_token',
|
||||
lambda: adapter.call_platform_api('check_access_token', {}),
|
||||
)
|
||||
await run_api(
|
||||
api_results,
|
||||
'call_platform_api:get_customer_info',
|
||||
lambda: adapter.call_platform_api('get_customer_info', {'external_userid': source.sender.id}),
|
||||
)
|
||||
|
||||
summary = {
|
||||
'events': [event.type for event in events],
|
||||
'api_results': api_results,
|
||||
'log_path': str(log_path),
|
||||
}
|
||||
print('WECOMCS_EBA_SUMMARY', json.dumps(summary, ensure_ascii=False, default=str))
|
||||
return summary
|
||||
finally:
|
||||
server_task.cancel()
|
||||
await adapter.kill()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Live WeCom Customer Service EBA adapter probe.')
|
||||
parser.add_argument('--host', default='0.0.0.0')
|
||||
parser.add_argument('--port', type=int, default=5313)
|
||||
parser.add_argument('--path', default='/wecomcs/callback')
|
||||
parser.add_argument('--timeout', type=int, default=180)
|
||||
parser.add_argument('--bot-uuid', default='wecomcs-eba-live-probe')
|
||||
parser.add_argument('--log', default='data/temp/wecomcs_eba_live_probe.jsonl')
|
||||
parser.add_argument('--skip-api', action='store_true')
|
||||
args = parser.parse_args()
|
||||
asyncio.run(run_probe(args))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user