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:
huanghuoguoguo
2026-06-22 23:06:02 +08:00
174 changed files with 27232 additions and 3832 deletions
+233
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())
+420
View File
@@ -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()
+158
View File
@@ -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()
+171
View File
@@ -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()
+161
View File
@@ -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()
+588
View File
@@ -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()
+214
View File
@@ -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()
+203
View File
@@ -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()
+211
View File
@@ -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()