Files
workphone-sdk/sdk/tests/test_wechat_full_e2e.py
Manus AI 42fe10401a 1
1
2026-05-24 17:50:04 +08:00

615 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
微信全功能 E2E 端到端测试脚本 — 96个action / 29个功能模块
验证完整链路: 网页/API → unified.py → 通道路由 → Agent(Hook/u2) 或 ADB引擎 → 微信APP
前置条件:
1. SDK 运行: localhost:8899 (cd sdk/app && python3 main.py)
2. 设备连接: USB ADB 或 Agent WebSocket
3. 微信已登录
4. 安全联系人: 文件传输助手 (每台微信必有)
用法:
python3 test_wechat_full_e2e.py # 运行安全测试组(只读/文件传输助手)
python3 test_wechat_full_e2e.py --full # 运行全量测试(含写操作)
python3 test_wechat_full_e2e.py --group message # 只运行消息组
python3 test_wechat_full_e2e.py --action send_message # 只运行单个action
环境变量:
SDK_BASE_URL: SDK地址 (默认 http://localhost:8899)
SDK_DEVICE_ID: 设备ID (默认自动检测)
SDK_E2E_TO_ID: 测试联系人 (默认 文件传输助手)
"""
import httpx
import asyncio
import json
import time
import os
import sys
import argparse
from typing import Dict, Any, List, Tuple
from dataclasses import dataclass, field
from datetime import datetime
BASE_URL = os.environ.get("SDK_BASE_URL", "http://localhost:8899")
DEVICE_ID = os.environ.get("SDK_DEVICE_ID", "")
TO_ID = os.environ.get("SDK_E2E_TO_ID", "文件传输助手")
TIMEOUT = 60
@dataclass
class CheckResult:
action: str
group: str
channel: str
success: bool
elapsed_ms: int
response: dict
error: str = ""
@dataclass
class CheckReport:
results: List[CheckResult] = field(default_factory=list)
start_time: float = 0
end_time: float = 0
@property
def total(self): return len(self.results)
@property
def passed(self): return sum(1 for r in self.results if r.success)
@property
def failed(self): return self.total - self.passed
@property
def success_rate(self): return f"{self.passed/self.total*100:.1f}%" if self.total else "N/A"
@property
def avg_time(self): return sum(r.elapsed_ms for r in self.results) // max(self.total, 1)
def print_summary(self):
elapsed = self.end_time - self.start_time
print("\n" + "=" * 70)
print(f" 微信全功能 E2E 测试报告")
print(f" {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 70)
print(f" 总计: {self.total} 通过: {self.passed} 失败: {self.failed} 成功率: {self.success_rate}")
print(f" 总耗时: {elapsed:.1f}s 平均单操作: {self.avg_time}ms")
print("-" * 70)
groups = {}
for r in self.results:
groups.setdefault(r.group, []).append(r)
for grp, items in groups.items():
ok = sum(1 for i in items if i.success)
print(f"\n [{grp}] {ok}/{len(items)} 通过")
for r in items:
icon = "" if r.success else ""
ch = f"[{r.channel}]" if r.channel else ""
err = f"{r.error[:60]}" if r.error else ""
print(f" {icon} {r.action:<30} {r.elapsed_ms:>5}ms {ch:<12}{err}")
print("\n" + "=" * 70)
if self.failed:
print("\n 失败详情:")
for r in self.results:
if not r.success:
print(f"\n [{r.group}] {r.action}")
print(f" 错误: {r.error}")
print(f" 响应: {json.dumps(r.response, ensure_ascii=False)[:200]}")
TS = int(time.time())
ALL_TESTS: Dict[str, List[Tuple[str, str, str, dict, bool]]] = {
"消息": [
("send_message", "POST", "/api/v3/message/send",
{"to_id": TO_ID, "content": f"[E2E] {datetime.now()}", "msg_type": "text"}, True),
("get_messages", "POST", "/api/v3/message/list",
{"conversation_id": TO_ID, "limit": 5}, True),
("forward_message", "POST", "/api/v3/message/forward",
{"to_id": TO_ID, "content": "转发测试"}, True),
("recall_message", "POST", "/api/v3/message/recall", {}, True),
("send_card", "POST", "/api/v3/message/send-card",
{"to_id": TO_ID, "card_wxid": TO_ID}, True),
],
"好友": [
("get_contacts", "GET", "/api/v3/contacts", {"limit": 10}, True),
("search_contact", "GET", "/api/v3/contacts/search", {"keyword": "文件"}, True),
("friend_info", "GET", "/api/v3/friend/info", {"user_id": TO_ID}, True),
],
"群聊": [
("get_groups", "GET", "/api/v3/group/list", {"limit": 10}, True),
("get_group_members","GET", "/api/v3/group/members", {"group_id": ""}, True),
],
"标签": [
("get_tags", "GET", "/api/v3/tag/list", {}, True),
],
"朋友圈": [
("get_moments", "POST", "/api/v3/moments/list", {"limit": 5}, True),
],
"个人": [
("get_profile", "GET", "/api/v3/profile/get", {}, True),
],
"安全": [
("account_status", "GET", "/api/v3/account/status", {}, True),
("safety_center", "GET", "/api/v3/account/safety-center", {}, True),
("restrictions", "GET", "/api/v3/account/restrictions", {}, True),
],
"收藏": [
("get_favorites", "GET", "/api/v3/favorites/list", {"limit": 5}, True),
],
"聊天设置": [
("set_chat_top", "POST", "/api/v3/chat/set-top",
{"user_id": TO_ID, "enable": True}, True),
("set_mute_chat", "POST", "/api/v3/chat/set-mute",
{"user_id": TO_ID, "enable": False}, True),
],
"视频号": [
("video_list", "GET", "/api/v3/video-channel/list", {}, True),
],
"扫一扫": [
("my_qr", "GET", "/api/v3/scan/my-qr", {}, True),
],
"支付": [
("payment_code", "GET", "/api/v3/payment/code", {}, True),
("wallet", "GET", "/api/v3/payment/wallet", {}, True),
("transactions", "GET", "/api/v3/payment/transactions", {}, True),
],
"搜索": [
("wechat_search", "GET", "/api/v3/search/wechat", {"keyword": "微信"}, True),
],
"发现": [
("top_stories", "GET", "/api/v3/discover/top-stories", {}, True),
],
"运动": [
("get_steps", "GET", "/api/v3/wechat-sport/steps", {}, True),
],
"表情": [
("stickers", "GET", "/api/v3/emoji/stickers", {}, True),
],
"Hook": [
("hook_actions", "GET", "/api/v3/hook/actions", {}, True),
],
}
WRITE_TESTS: Dict[str, List[Tuple[str, str, str, dict, bool]]] = {
"消息写": [
("batch_send", "POST", "/api/v3/message/batch-send",
{"to_ids": [TO_ID], "content": f"[批量E2E] {TS}", "msg_type": "text"}, True),
("send_voice", "POST", "/api/v3/message/voice",
{"to_id": TO_ID, "duration": 3}, True),
],
"好友写": [
("set_remark", "POST", "/api/v3/friend/set-remark",
{"user_id": TO_ID, "remark": f"E2E_{TS}"}, True),
],
"标签写": [
("create_tag", "POST", "/api/v3/tag/create",
{"tag_name": f"e2e_tag_{TS}"}, True),
("get_users_by_tag","POST", "/api/v3/tag/users",
{"tag_name": f"e2e_tag_{TS}"}, True),
],
"个人写": [
("set_signature", "POST", "/api/v3/profile/set-signature",
{"signature": f"E2E测试 {TS}"}, True),
],
"朋友圈写": [
("post_moments", "POST", "/api/v3/moments/post",
{"content": f"[E2E测试] {datetime.now().strftime('%H:%M')}"}, True),
("like_moments", "POST", "/api/v3/moments/like",
{"user_id": TO_ID, "post_index": 0}, True),
("comment_moments", "POST", "/api/v3/moments/comment",
{"user_id": TO_ID, "post_index": 0, "comment": "E2E测试评论"}, True),
("set_privacy", "POST", "/api/v3/moments/set-privacy",
{"privacy_type": "all"}, True),
],
"聊天写": [
("clear_history", "POST", "/api/v3/chat/clear-history",
{"user_id": TO_ID}, True),
],
"收藏写": [
("add_favorite", "POST", "/api/v3/favorites/add",
{"content": "E2E收藏测试", "content_type": "text"}, True),
],
"视频号写": [
("like_video", "POST", "/api/v3/video-channel/like",
{"index": 0}, True),
],
"运动写": [
("like_steps", "POST", "/api/v3/wechat-sport/like-steps",
{"user_id": TO_ID}, True),
],
"位置写": [
("send_location", "POST", "/api/v3/location/send",
{"to_id": TO_ID, "latitude": 31.23, "longitude": 121.47, "name": "测试位置"}, True),
],
"表情写": [
("send_emoji", "POST", "/api/v3/emoji/send",
{"to_id": TO_ID, "emoji_name": "微笑"}, True),
],
"设置写": [
("do_not_disturb", "POST", "/api/v3/settings/do-not-disturb",
{"enable": False}, True),
("clear_cache", "POST", "/api/v3/settings/clear-cache", {}, True),
("check_update", "POST", "/api/v3/settings/check-update", {}, True),
],
"小程序写": [
("open_miniprogram","POST", "/api/v3/miniprogram/open",
{"name": "微信支付"}, True),
],
"文件写": [
("send_file", "POST", "/api/v3/file/send",
{"to_id": TO_ID, "file_path": "/sdcard/test.txt"}, True),
],
}
# 矩阵 REST 补全(~97 端点 已有 50 action
MATRIX_REST_TESTS: Dict[str, List[Tuple[str, str, str, dict, bool]]] = {
"消息补": [
("comment_reply", "POST", "/api/v3/comment/reply",
{"video_id": "1", "comment_id": "1", "content": "probe"}, True),
],
"好友补": [
("friend_add", "POST", "/api/v3/friend/add",
{"user_id": "28533368", "message": "matrix-e2e"}, True),
("friend_accept", "POST", "/api/v3/friend/accept", {"user_id": "probe"}, True),
("friend_delete", "POST", "/api/v3/friend/delete", {"user_id": "invalid_probe_wxid"}, True),
("friend_batch_add", "POST", "/api/v3/friend/batch-add",
{"user_ids": ["28533368"], "message": "batch-probe"}, True),
],
"群聊补": [
("group_create", "POST", "/api/v3/group/create",
{"group_name": f"e2e_{TS}", "member_ids": [TO_ID, TO_ID]}, True),
("group_invite", "POST", "/api/v3/group/invite",
{"group_id": "", "member_ids": [TO_ID]}, True),
("group_remove", "POST", "/api/v3/group/remove",
{"group_id": "", "member_ids": [TO_ID]}, True),
("group_set_notice", "POST", "/api/v3/group/set-notice",
{"group_id": "", "notice": "e2e notice"}, True),
("group_set_name", "POST", "/api/v3/group/set-name",
{"group_id": "", "group_name": "e2e-group"}, True),
("group_send_message", "POST", "/api/v3/group/send-message",
{"group_id": "", "content": "e2e group msg"}, True),
("group_set_welcome", "POST", "/api/v3/group/set-welcome",
{"group_id": "", "welcome_text": "welcome probe"}, True),
("group_quit", "POST", "/api/v3/group/quit", {"group_id": ""}, True),
],
"标签补": [
("tag_add", "POST", "/api/v3/tag/add", {"user_id": TO_ID, "tags": [f"e2e_tag_{TS}"]}, True),
("tag_remove", "POST", "/api/v3/tag/remove", {"user_id": TO_ID, "tags": [f"e2e_tag_{TS}"]}, True),
("tag_delete", "POST", "/api/v3/tag/delete", {"tag_name": f"e2e_tag_{TS}"}, True),
],
"朋友圈补": [
("moments_delete", "POST", "/api/v3/moments/delete", {"post_index": 0}, True),
("moments_set_cover", "POST", "/api/v3/moments/set-cover", {}, True),
("moments_share_link", "POST", "/api/v3/moments/share-link",
{"content": "https://example.com probe"}, True),
],
"个人补": [
("set_nickname", "POST", "/api/v3/profile/set-nickname", {"nickname": "e2e_probe"}, True),
("set_avatar", "POST", "/api/v3/profile/set-avatar", {"image_path": "/sdcard/test.jpg"}, True),
("set_gender", "POST", "/api/v3/profile/set-gender", {"gender": "1"}, True),
("set_region", "POST", "/api/v3/profile/set-region", {"region": "福建 厦门"}, True),
],
"安全补": [
("account_unblock", "POST", "/api/v3/account/unblock", {}, True),
("change_password", "POST", "/api/v3/account/change-password",
{"old_pwd": "old", "new_pwd": "new"}, True),
("unblock_self", "POST", "/api/v3/account/unblock-self", {}, True),
("unblock_appeal", "POST", "/api/v3/account/unblock-appeal", {}, True),
("appeal_restriction", "POST", "/api/v3/account/appeal-restriction", {}, True),
("unblock_sms", "POST", "/api/v3/account/unblock-sms", {"phone": "13800138000"}, True),
],
"支付补": [
("send_red_packet", "POST", "/api/v3/payment/red-packet",
{"to_id": TO_ID, "amount": "0.01", "message": "probe"}, True),
("payment_transfer", "POST", "/api/v3/payment/transfer",
{"to_id": TO_ID, "amount": "0.01", "description": "probe"}, True),
("payment_receive", "POST", "/api/v3/payment/receive", {"amount": "0.01", "desc": "probe"}, True),
("receive_red_packet", "POST", "/api/v3/payment/receive-red-packet", {"from_id": TO_ID}, True),
],
"视频号补": [
("video_comment", "POST", "/api/v3/video-channel/comment",
{"index": 0, "comment": "probe"}, True),
("video_follow", "POST", "/api/v3/video-channel/follow", {"index": 0}, True),
("video_share", "POST", "/api/v3/video-channel/share",
{"index": 0, "to_id": TO_ID}, True),
],
"扫一扫补": [
("scan_qr", "POST", "/api/v3/scan/qr-code", {"qr_content": "probe"}, True),
("scan_add_friend", "POST", "/api/v3/scan/add-friend", {"qr_content": "probe"}, True),
("scan_extract_qr", "POST", "/api/v3/scan/extract-qr", {"image_path": "/sdcard/test.jpg"}, True),
],
"通话补": [
("call_voice", "POST", "/api/v3/call/voice", {"to_id": TO_ID}, True),
("call_video", "POST", "/api/v3/call/video", {"to_id": TO_ID}, True),
],
"群发": [
("mass_send", "POST", "/api/v3/mass-send",
{"user_ids": [TO_ID], "content": f"mass {TS}"}, True),
],
"位置补": [
("share_realtime_location", "POST", "/api/v3/location/share-realtime",
{"to_id": TO_ID, "duration_minutes": 5}, True),
],
"文件补": [
("file_download", "POST", "/api/v3/file/download", {"user_id": TO_ID}, True),
],
"设置补": [
("settings_logout", "POST", "/api/v3/settings/logout", {}, True),
("switch_account", "POST", "/api/v3/settings/switch-account", {}, True),
],
"公众号": [
("follow_official", "POST", "/api/v3/official-account/follow", {"account_name": "probe"}, True),
],
}
async def prefetch_group_id(client: httpx.AsyncClient) -> str:
"""取 chatroom 表存在的群 ID若无则尝试建群仍无则返回 probe idIntent 探针)。"""
pre = await run_test(client, "get_groups", "GET", "/api/v3/group/list", {"limit": 20}, "预检")
groups = (pre.response.get("data") or {}).get("groups") or []
for g in groups:
gid = g.get("group_id") or g.get("username") or ""
if not gid:
continue
chk = await run_test(client, "get_group_members", "GET", "/api/v3/group/members",
{"group_id": gid}, "预检")
inner = chk.response.get("data") or {}
if inner.get("members") is not None or inner.get("success"):
return gid
self_wxid = ""
try:
pr = await run_test(client, "account_status", "GET", "/api/v3/account/status", {}, "预检")
inner = pr.response.get("data") or {}
self_wxid = inner.get("wxid") or inner.get("username") or ""
except Exception:
pass
members = ["filehelper"]
if self_wxid and self_wxid not in members:
members.append(self_wxid)
if len(members) < 2:
members.append("filehelper")
cr = await run_test(
client, "group_create", "POST", "/api/v3/group/create",
{"group_name": f"e2e_{TS}", "member_ids": members[:3]}, "预检",
)
data = cr.response.get("data") or {}
gid = data.get("group_id") or (data.get("group") or {}).get("group_id") or ""
if gid:
return gid
return f"probe_e2e_{TS}@chatroom"
def _inject_group_id(params: dict, group_id: str) -> None:
if group_id and "group_id" in params and not params.get("group_id"):
params["group_id"] = group_id
def _device_list_from_api_json(data: dict) -> list:
"""兼容 data 为设备数组或 { devices: [] }"""
if not isinstance(data, dict):
return []
raw = data.get("data")
if isinstance(raw, list):
return raw
if isinstance(raw, dict) and isinstance(raw.get("devices"), list):
return raw["devices"]
if isinstance(data.get("devices"), list):
return data["devices"]
return []
async def detect_device(client: httpx.AsyncClient) -> str:
"""自动检测在线设备"""
global DEVICE_ID
if DEVICE_ID:
return DEVICE_ID
try:
resp = await client.get(f"{BASE_URL}/api/v3/devices", timeout=10)
devices = _device_list_from_api_json(resp.json())
if devices:
DEVICE_ID = devices[0].get("device_id", devices[0].get("serial", str(devices[0])))
return DEVICE_ID
except Exception:
pass
try:
resp = await client.get(f"{BASE_URL}/api/v3/adb/devices", timeout=10)
devices = _device_list_from_api_json(resp.json())
if devices:
DEVICE_ID = devices[0].get("serial", devices[0].get("device_id", str(devices[0])))
return DEVICE_ID
except Exception:
pass
DEVICE_ID = "unknown"
return DEVICE_ID
async def run_test(client: httpx.AsyncClient, action: str, method: str,
endpoint: str, params: dict, group: str) -> CheckResult:
"""执行单个测试POST 自动兼容 body/query 两种参数模式)"""
params["device_id"] = DEVICE_ID
params["platform"] = "wechat"
start = time.time()
try:
url = f"{BASE_URL}{endpoint}"
if method == "GET":
qs = "&".join(f"{k}={v}" for k, v in params.items() if v is not None)
resp = await client.get(f"{url}?{qs}" if qs else url, timeout=TIMEOUT)
else:
resp = await client.post(url, json=params, timeout=TIMEOUT)
if resp.status_code == 422:
qs = "&".join(f"{k}={v}" for k, v in params.items() if v is not None)
resp = await client.post(f"{url}?{qs}" if qs else url, timeout=TIMEOUT)
elapsed = int((time.time() - start) * 1000)
data = resp.json()
channel = data.get("channel_used", data.get("data", {}).get("channel", "unknown"))
ok = resp.status_code == 200 and data.get("code", 200) in (200, 0, None)
if isinstance(data.get("data"), dict) and data["data"].get("success") is False:
ok = False
error = "" if ok else (data.get("message", data.get("error", str(data)))[:100])
return CheckResult(action=action, group=group, channel=str(channel),
success=ok, elapsed_ms=elapsed, response=data, error=error)
except Exception as e:
elapsed = int((time.time() - start) * 1000)
return CheckResult(action=action, group=group, channel="error",
success=False, elapsed_ms=elapsed, response={}, error=str(e)[:100])
async def check_health(client: httpx.AsyncClient) -> bool:
"""检查SDK是否在线"""
try:
resp = await client.get(f"{BASE_URL}/health", timeout=10)
data = resp.json()
print(f" SDK状态: {data.get('status', 'unknown')}")
print(f" 设备在线: {data.get('devices_online', 0)}")
return True
except Exception as e:
print(f" SDK连接失败: {e}")
return False
async def main():
parser = argparse.ArgumentParser(description="微信全功能E2E测试")
parser.add_argument("--full", action="store_true", help="运行全量测试(含写操作)")
parser.add_argument("--matrix", action="store_true", help="矩阵 REST 全端点(含 --full + 补全端点)")
parser.add_argument("--group", type=str, help="只运行指定组")
parser.add_argument("--action", type=str, help="只运行指定action")
parser.add_argument("--base-url", type=str, help="SDK地址")
parser.add_argument("--device-id", type=str, help="设备ID")
args = parser.parse_args()
global BASE_URL, DEVICE_ID
if args.base_url:
BASE_URL = args.base_url
if args.device_id:
DEVICE_ID = args.device_id
report = CheckReport()
report.start_time = time.time()
print("\n" + "=" * 70)
print(" 微信全功能 E2E 端到端测试")
print(" 96个action / 29个功能模块 / 3种控制通道")
print("=" * 70)
async with httpx.AsyncClient() as client:
print("\n[0] 环境检查")
if not await check_health(client):
print("\n 请先启动SDK: cd sdk/app && python3 main.py")
return
await detect_device(client)
print(f" 设备ID: {DEVICE_ID}")
print(f" 测试联系人: {TO_ID}")
tests = dict(ALL_TESTS)
if args.full or args.matrix:
tests.update(WRITE_TESTS)
os.environ["SDK_MATRIX_VERIFY"] = "1"
if args.matrix:
tests.update(MATRIX_REST_TESTS)
mode = "矩阵 REST 全端点" if args.matrix else ("全量(含写操作)" if args.full else "安全(只读+文件传输助手)")
print(f" 测试模式: {mode}")
if args.group:
tests = {k: v for k, v in tests.items() if k == args.group}
if not tests:
print(f"\n 未找到测试组: {args.group}")
print(f" 可用组: {', '.join(list(ALL_TESTS.keys()) + list(WRITE_TESTS.keys()))}")
return
if args.action:
filtered = {}
for grp, items in tests.items():
matching = [t for t in items if t[0] == args.action]
if matching:
filtered[grp] = matching
tests = filtered
if not tests:
print(f"\n 未找到action: {args.action}")
return
total_actions = sum(len(v) for v in tests.values())
print(f"\n 即将测试 {total_actions} 个 action ({len(tests)} 组)")
print("-" * 70)
# 预取最近消息 msgId供 forward/recall 真机链路
last_msg_id = None
try:
pre = await run_test(client, "get_messages", "POST", "/api/v3/message/list",
{"conversation_id": "filehelper", "limit": 3}, "预检")
msgs = (pre.response.get("data") or {}).get("messages") or []
if not msgs and isinstance(pre.response.get("data"), dict):
inner = pre.response["data"].get("data") or pre.response["data"]
msgs = inner.get("messages") or []
if msgs:
last_msg_id = str(msgs[0].get("id") or msgs[0].get("msgId") or msgs[0].get("msg_svr_id") or "")
print(f" 预检 msg_svr_id={last_msg_id}")
except Exception as e:
print(f" 预检 get_messages 跳过: {e}")
group_id = ""
if args.matrix:
group_id = await prefetch_group_id(client)
print(f" 预检 group_id={group_id or '(空)'}")
for group_name, group_tests in tests.items():
print(f"\n[{group_name}] 测试中...")
for action, method, endpoint, params, safe in group_tests:
p = dict(params)
if args.matrix:
_inject_group_id(p, group_id)
if action == "forward_message" and last_msg_id:
p["msg_svr_id"] = last_msg_id
if action == "recall_message" and last_msg_id:
p["msg_svr_id"] = last_msg_id
if action == "friend_info":
p["user_id"] = "filehelper"
result = await run_test(client, action, method, endpoint, p, group_name)
report.results.append(result)
icon = "" if result.success else ""
print(f" {icon} {action:<30} {result.elapsed_ms:>5}ms [{result.channel}]")
if not result.success:
print(f"{result.error[:80]}")
await asyncio.sleep(1.0)
report.end_time = time.time()
report.print_summary()
report_path = os.path.join(os.path.dirname(__file__), "..",
"data", "e2e_report.json")
try:
os.makedirs(os.path.dirname(report_path), exist_ok=True)
with open(report_path, "w", encoding="utf-8") as f:
json.dump({
"timestamp": datetime.now().isoformat(),
"device_id": DEVICE_ID,
"total": report.total,
"passed": report.passed,
"failed": report.failed,
"success_rate": report.success_rate,
"avg_time_ms": report.avg_time,
"results": [
{
"action": r.action, "group": r.group,
"channel": r.channel, "success": r.success,
"elapsed_ms": r.elapsed_ms, "error": r.error
} for r in report.results
]
}, f, ensure_ascii=False, indent=2)
print(f"\n 报告已保存: {report_path}")
except Exception as e:
print(f"\n 保存报告失败: {e}")
sys.exit(0 if report.failed == 0 else 1)
if __name__ == "__main__":
asyncio.run(main())