feat: 微信引擎v2全功能(96actions/98API) + 导航缓存系统 + 操作日志

- 引擎v1→v2: 54→96个actions, 新增14模块(解封/视频号/扫一扫/支付/通话/群发/搜一搜/运动/位置/表情/朋友圈增强/语音/文件/设置)
- API端点: 55→98个, 23个功能分类
- 新建 wechat_nav_cache.py: 导航路径缓存+页面元素录制+操作日志+自动纠错
- 真机录制Tab精确坐标(y=2148), 我/设置/账号与安全页面元素
- 全局容错: _retry/_recover_state/_verify_action
- 导航方法重构: 缓存路径优先, 消除重复UI探索
- 微信全功能矩阵文档 v8.0.56

Made-with: Cursor
This commit is contained in:
卡若
2026-03-14 16:41:03 +08:00
parent 374e5b6f4a
commit 38392d6ee0
7 changed files with 3059 additions and 178 deletions

View File

@@ -406,12 +406,18 @@ def _parse_contacts_from_xml(xml: str, limit: int) -> list:
async def _anti_ban_guard(device_id: str, platform: str, action: str, content: Optional[str] = None) -> dict:
"""
防封守卫:在执行任何操作前进行防封检查。
返回 {"pass": True/False, "error": str, "content": str(处理后的内容)}
返回 {"pass": True/False, "reason": str, "content": str(处理后的内容)}
检查链:时段 → 生命周期 → L1全局 → L2设备 → L3动作 → 敏感词 → 内容差异化
"""
from services.anti_ban_alert import (
alert_daily_limit, alert_outside_hours, alert_sensitive_content,
)
from services.rate_limiter import OutsideOperationHours
try:
lifecycle_check = await account_lifecycle.check_allowed(device_id, platform, action)
if not lifecycle_check["allowed"]:
return {"pass": False, "error": lifecycle_check["reason"], "content": content}
return {"pass": False, "reason": lifecycle_check["reason"], "content": content}
except Exception as e:
logger.warning(f"[anti_ban] 生命周期检查异常(跳过): {e}")
@@ -420,8 +426,12 @@ async def _anti_ban_guard(device_id: str, platform: str, action: str, content: O
wait = await rate_limiter.check_and_wait(device_id, platform, action, is_new_account=is_new)
if wait > 0:
logger.info(f"[anti_ban] {device_id}/{platform}.{action} 等待了 {wait:.1f}s")
except OutsideOperationHours as e:
asyncio.ensure_future(alert_outside_hours(device_id, action))
return {"pass": False, "reason": str(e), "content": content}
except DailyLimitExceeded as e:
return {"pass": False, "error": str(e), "content": content}
asyncio.ensure_future(alert_daily_limit(device_id, platform, action, -1, -1))
return {"pass": False, "reason": str(e), "content": content}
except Exception as e:
logger.warning(f"[anti_ban] 限流检查异常(跳过): {e}")
@@ -430,9 +440,10 @@ async def _anti_ban_guard(device_id: str, platform: str, action: str, content: O
sensitive = has_sensitive_words(content)
if sensitive:
logger.warning(f"[anti_ban] 检测到敏感词: {sensitive}")
asyncio.ensure_future(alert_sensitive_content(device_id, platform, sensitive))
safe_content = diversify_content(content, add_invisible=True, add_emoji=False)
return {"pass": True, "error": "", "content": safe_content}
return {"pass": True, "reason": "", "content": safe_content}
async def _execute_skill(device_id: str, platform: str, action: str, params: dict, timeout: int = 30) -> dict:
@@ -529,7 +540,7 @@ async def send_message(req: SendMessageRequest):
# ---- 防封守卫 ----
guard = await _anti_ban_guard(req.device_id, req.platform.value, "send_message", req.content)
if not guard["pass"]:
return {"code": 200, "data": {"success": False, "error": guard["error"], "error_code": "anti_ban_blocked"}, "channel_used": "none"}
return {"code": 200, "data": {"success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}, "channel_used": "none"}
req.content = guard["content"]
mode = _get_device_mode(req.device_id)
@@ -614,7 +625,7 @@ async def batch_send_message(req: BatchSendMessageRequest):
"""
guard = await _anti_ban_guard(req.device_id, req.platform.value, "batch_send", req.content)
if not guard["pass"]:
return {"code": 200, "data": {"success": False, "error": guard["error"], "error_code": "anti_ban_blocked"}, "channel_used": "none"}
return {"code": 200, "data": {"success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}, "channel_used": "none"}
req.content = guard["content"]
_check_device_online(req.device_id)
@@ -644,9 +655,12 @@ async def batch_send_message(req: BatchSendMessageRequest):
async def reply_comment(req: ReplyCommentRequest):
"""回复评论(抖音/小红书等)"""
_check_device_online(req.device_id)
guard = await _anti_ban_guard(req.device_id, req.platform.value, "comment", req.content)
if not guard["pass"]:
return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}
result = await _execute_skill(
req.device_id, req.platform.value, "reply_comment",
{"video_id": req.video_id, "comment_id": req.comment_id, "content": req.content}
{"video_id": req.video_id, "comment_id": req.comment_id, "content": guard.get("content", req.content)}
)
payload = result.get("data") if result.get("data") is not None else result
return {
@@ -665,7 +679,7 @@ async def add_friend(req: AddFriendRequest):
"""添加好友"""
guard = await _anti_ban_guard(req.device_id, req.platform.value, "add_friend", req.message)
if not guard["pass"]:
return {"code": 200, "data": {"success": False, "error": guard["error"], "error_code": "anti_ban_blocked"}, "channel_used": "none"}
return {"code": 200, "data": {"success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}, "channel_used": "none"}
req.message = guard["content"] or req.message
_check_device_online(req.device_id)
@@ -739,14 +753,15 @@ async def delete_friend(req: DeleteFriendRequest):
@router.post("/friend/batch-add", response_model=dict, tags=["好友管理"])
async def batch_add_friend(req: BatchAddFriendRequest):
"""批量添加好友"""
_check_device_online(req.device_id)
guard = await _anti_ban_guard(req.device_id, req.platform.value, "add_friend", req.message)
if not guard["pass"]:
return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}
result = await _execute_skill(
req.device_id, req.platform.value, "batch_add_friend",
{
"user_ids": req.user_ids,
"message": req.message,
"message": guard.get("content", req.message),
"interval": req.interval
},
timeout=len(req.user_ids) * 15
@@ -873,14 +888,15 @@ async def set_group_name(req: SetGroupNameRequest):
@router.post("/group/send-message", response_model=dict, tags=["群聊管理"])
async def send_group_message(req: GroupMessageRequest):
"""发送群消息"""
_check_device_online(req.device_id)
guard = await _anti_ban_guard(req.device_id, req.platform.value, "send_group_message", req.content)
if not guard["pass"]:
return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}
result = await _execute_skill(
req.device_id, req.platform.value, "send_group_message",
{
"group_id": req.group_id,
"content": req.content,
"content": guard.get("content", req.content),
"msg_type": req.msg_type.value,
"media_url": req.media_url,
"at_all": req.at_all,
@@ -1073,7 +1089,7 @@ async def post_moments(req: PostMomentsRequest):
"""发布朋友圈/瞬间(统一返回 200业务失败用 success=false 表示,避免 502"""
guard = await _anti_ban_guard(req.device_id, req.platform.value, "post_moments", req.content)
if not guard["pass"]:
return {"code": 200, "success": False, "error": guard["error"], "data": {}, "channel_used": "none"}
return {"code": 200, "success": False, "error": guard["reason"], "data": {}, "channel_used": "none"}
req.content = guard["content"]
try:
@@ -1122,14 +1138,14 @@ async def post_moments(req: PostMomentsRequest):
@router.post("/moments/like", response_model=dict, tags=["朋友圈管理"])
async def like_moments(req: LikeMomentsRequest):
"""点赞朋友圈"""
_check_device_online(req.device_id)
guard = await _anti_ban_guard(req.device_id, req.platform.value, "like_moments")
if not guard["pass"]:
return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}
result = await _execute_skill(
req.device_id, req.platform.value, "like_moments",
{"user_id": req.user_id, "post_index": req.post_index}
)
return {
"code": 200,
"data": result.get("data", {}),
@@ -1140,15 +1156,16 @@ async def like_moments(req: LikeMomentsRequest):
@router.post("/moments/comment", response_model=dict, tags=["朋友圈管理"])
async def comment_moments(req: CommentMomentsRequest):
"""评论朋友圈"""
_check_device_online(req.device_id)
guard = await _anti_ban_guard(req.device_id, req.platform.value, "comment_moments", req.comment)
if not guard["pass"]:
return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}
result = await _execute_skill(
req.device_id, req.platform.value, "comment_moments",
{
"user_id": req.user_id,
"post_index": req.post_index,
"comment": req.comment,
"comment": guard.get("content", req.comment),
"reply_to": req.reply_to
}
)
@@ -1571,6 +1588,416 @@ async def follow_official_account(device_id: str, platform: Platform, account_na
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 十六、解封与限制管理
# =============================================================================
@router.post("/account/unblock-self", response_model=dict, tags=["账号安全"])
async def unblock_self(device_id: str, platform: Platform):
"""自助解封"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "unblock_self", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/account/unblock-appeal", response_model=dict, tags=["账号安全"])
async def unblock_appeal(device_id: str, platform: Platform):
"""申诉解封"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "unblock_appeal", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.get("/account/restrictions", response_model=dict, tags=["账号安全"])
async def check_restrictions(device_id: str, platform: Platform):
"""检查当前功能限制"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "check_restrictions", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/account/appeal-restriction", response_model=dict, tags=["账号安全"])
async def appeal_restriction(device_id: str, platform: Platform):
"""申诉功能限制"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "appeal_restriction", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/account/unblock-sms", response_model=dict, tags=["账号安全"])
async def unblock_with_sms(device_id: str, platform: Platform, phone: str = ""):
"""短信验证解封"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "unblock_with_sms", {"phone": phone})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 十七、视频号
# =============================================================================
@router.get("/video-channel/list", response_model=dict, tags=["视频号"])
async def get_video_list(device_id: str, platform: Platform, limit: int = 10):
"""获取视频号列表"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "get_video_list", {"limit": limit})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/video-channel/like", response_model=dict, tags=["视频号"])
async def like_video(device_id: str, platform: Platform, index: int = 0):
"""点赞视频"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "like_video", {"index": index})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/video-channel/comment", response_model=dict, tags=["视频号"])
async def comment_video(device_id: str, platform: Platform, index: int = 0, comment: str = ""):
"""评论视频"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "comment_video", {"index": index, "comment": comment})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/video-channel/follow", response_model=dict, tags=["视频号"])
async def follow_video_creator(device_id: str, platform: Platform, index: int = 0):
"""关注视频号创作者"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "follow_video_creator", {"index": index})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/video-channel/share", response_model=dict, tags=["视频号"])
async def share_video(device_id: str, platform: Platform, index: int = 0, to_id: str = ""):
"""分享视频"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "share_video", {"index": index, "to_id": to_id})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 十八、扫一扫
# =============================================================================
@router.post("/scan/qr-code", response_model=dict, tags=["扫一扫"])
async def scan_qr_code(device_id: str, platform: Platform):
"""扫描二维码"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "scan_qr_code", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/scan/add-friend", response_model=dict, tags=["扫一扫"])
async def scan_add_friend(device_id: str, platform: Platform):
"""扫码加好友"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "scan_add_friend", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.get("/scan/my-qr", response_model=dict, tags=["扫一扫"])
async def show_my_qr(device_id: str, platform: Platform):
"""显示我的二维码"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "show_my_qr", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/scan/extract-qr", response_model=dict, tags=["扫一扫"])
async def extract_qr_from_image(device_id: str, platform: Platform):
"""从图片识别二维码"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "extract_qr_from_image", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 十九、支付增强
# =============================================================================
@router.get("/payment/code", response_model=dict, tags=["支付"])
async def show_payment_code(device_id: str, platform: Platform):
"""显示付款码"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "show_payment_code", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/payment/receive", response_model=dict, tags=["支付"])
async def receive_payment(device_id: str, platform: Platform, amount: str = "", desc: str = ""):
"""收款"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "receive_payment", {"amount": amount, "desc": desc})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.get("/payment/wallet", response_model=dict, tags=["支付"])
async def view_wallet(device_id: str, platform: Platform):
"""查看钱包"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "view_wallet", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.get("/payment/transactions", response_model=dict, tags=["支付"])
async def view_transactions(device_id: str, platform: Platform, limit: int = 20):
"""查看账单"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "view_transactions", {"limit": limit})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/payment/receive-red-packet", response_model=dict, tags=["支付"])
async def receive_red_packet(device_id: str, platform: Platform, from_id: str = ""):
"""领取红包"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "receive_red_packet", {"from_id": from_id})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 二十、通话
# =============================================================================
@router.post("/call/voice", response_model=dict, tags=["通话"])
async def voice_call(device_id: str, platform: Platform, user_id: str = ""):
"""语音通话"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "voice_call", {"user_id": user_id})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/call/video", response_model=dict, tags=["通话"])
async def video_call(device_id: str, platform: Platform, user_id: str = ""):
"""视频通话"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "video_call", {"user_id": user_id})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 二十一、群发助手
# =============================================================================
class MassSendRequest(BaseModel):
device_id: str
platform: Platform
content: str
user_ids: List[str]
@router.post("/mass-send", response_model=dict, tags=["群发助手"])
async def mass_send(req: MassSendRequest):
"""群发消息"""
_check_device_online(req.device_id)
guard = await _anti_ban_guard(req.device_id, req.platform.value, "batch_send", req.content)
if not guard["pass"]:
return {"code": 200, "success": False, "error": guard["reason"], "error_code": "anti_ban_blocked"}
result = await _execute_skill(
req.device_id, req.platform.value, "mass_send",
{"content": guard.get("content", req.content), "user_ids": req.user_ids}
)
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 二十二、搜一搜/看一看
# =============================================================================
@router.get("/search/wechat", response_model=dict, tags=["搜一搜"])
async def wechat_search(device_id: str, platform: Platform, keyword: str = ""):
"""搜一搜"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "wechat_search", {"keyword": keyword})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.get("/discover/top-stories", response_model=dict, tags=["看一看"])
async def top_stories(device_id: str, platform: Platform):
"""看一看"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "top_stories", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 二十三、微信运动
# =============================================================================
@router.get("/wechat-sport/steps", response_model=dict, tags=["微信运动"])
async def get_steps(device_id: str, platform: Platform):
"""获取步数"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "get_steps", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/wechat-sport/like-steps", response_model=dict, tags=["微信运动"])
async def like_steps(device_id: str, platform: Platform, user_id: str = ""):
"""点赞步数"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "like_steps", {"user_id": user_id})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 二十四、位置分享
# =============================================================================
@router.post("/location/send", response_model=dict, tags=["位置"])
async def send_location(device_id: str, platform: Platform, user_id: str = ""):
"""发送位置"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "send_location", {"user_id": user_id})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/location/share-realtime", response_model=dict, tags=["位置"])
async def share_real_time_location(device_id: str, platform: Platform, user_id: str = ""):
"""共享实时位置"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "share_real_time_location", {"user_id": user_id})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 二十五、表情管理
# =============================================================================
@router.post("/emoji/send", response_model=dict, tags=["表情"])
async def send_emoji(device_id: str, platform: Platform, user_id: str = "", emoji_name: str = ""):
"""发送表情"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "send_emoji", {"user_id": user_id, "emoji_name": emoji_name})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.get("/emoji/stickers", response_model=dict, tags=["表情"])
async def get_sticker_list(device_id: str, platform: Platform):
"""获取表情包列表"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "get_sticker_list", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 二十六、朋友圈增强
# =============================================================================
@router.post("/moments/set-cover", response_model=dict, tags=["朋友圈管理"])
async def set_moments_cover(device_id: str, platform: Platform):
"""设置朋友圈封面"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "set_moments_cover", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/moments/set-privacy", response_model=dict, tags=["朋友圈管理"])
async def set_moments_privacy(device_id: str, platform: Platform, days: int = 180):
"""设置朋友圈可见天数"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "set_moments_privacy", {"days": days})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/moments/share-link", response_model=dict, tags=["朋友圈管理"])
async def share_link_to_moments(device_id: str, platform: Platform, url: str = "", title: str = ""):
"""分享链接到朋友圈"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "forward_moments_link", {"url": url, "title": title})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 二十七、语音消息
# =============================================================================
@router.post("/message/voice", response_model=dict, tags=["消息管理"])
async def send_voice_message(device_id: str, platform: Platform, user_id: str = "", duration: int = 3):
"""发送语音消息"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "send_voice_message", {"user_id": user_id, "duration": duration})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 二十八、文件管理
# =============================================================================
@router.post("/file/send", response_model=dict, tags=["文件管理"])
async def send_file(device_id: str, platform: Platform, user_id: str = ""):
"""发送文件"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "send_file_from_chat", {"user_id": user_id})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/file/download", response_model=dict, tags=["文件管理"])
async def download_file(device_id: str, platform: Platform, user_id: str = ""):
"""下载文件"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "download_file", {"user_id": user_id})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 二十九、设置管理
# =============================================================================
@router.post("/settings/do-not-disturb", response_model=dict, tags=["设置"])
async def toggle_dnd(device_id: str, platform: Platform, enable: bool = True):
"""勿扰模式"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "toggle_do_not_disturb", {"enable": enable})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/settings/clear-cache", response_model=dict, tags=["设置"])
async def clear_cache(device_id: str, platform: Platform):
"""清理缓存"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "clear_cache", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/settings/check-update", response_model=dict, tags=["设置"])
async def check_update(device_id: str, platform: Platform):
"""检查更新"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "check_for_update", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/settings/logout", response_model=dict, tags=["设置"])
async def logout(device_id: str, platform: Platform):
"""退出登录"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "logout", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
@router.post("/settings/switch-account", response_model=dict, tags=["设置"])
async def switch_account(device_id: str, platform: Platform):
"""切换账号"""
_check_device_online(device_id)
result = await _execute_skill(device_id, platform.value, "switch_account", {})
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": Channel.SDK_CONTROL.value}
# =============================================================================
# 内部实现方法
# =============================================================================
@@ -1813,3 +2240,83 @@ async def _send_via_agent(req: SendMessageRequest) -> dict:
return {"success": False, "error": err or "ADB 执行失败"}
return {"success": False, "error": "设备离线"}
# ========== AF12: 风控中心监控 ==========
@router.get("/anti-ban/dashboard")
async def anti_ban_dashboard():
"""防封风控中心:汇总全局限流/账号生命周期/指纹碰撞状态"""
import time as _time
from services.rate_limiter import rate_limiter as _rl
devices = await device_manager.get_all_devices(limit=500)
total_devices = len(devices)
online = sum(1 for d in devices if d.get("status") == "online")
seen_fp: dict = {}
fp_collision_devices = []
for d in devices:
fp = d.get("fingerprint_hash", "")
if not fp:
continue
did = d.get("device_id", "")
seen_fp.setdefault(fp, []).append(did)
for fp, dids in seen_fp.items():
if len(dids) > 1:
for did in dids:
fp_collision_devices.append({
"device_id": did,
"collided_with": [x for x in dids if x != did],
})
lifecycle_summary = []
for d in devices[:50]:
did = d.get("device_id", "")
plat = d.get("platform", "wechat")
phase_info = await account_lifecycle.get_phase(did, plat)
rules = await account_lifecycle.get_rules(did, plat)
lifecycle_summary.append({
"device_id": did,
"phase": phase_info.value if hasattr(phase_info, "value") else str(phase_info),
"max_daily_add_friend": rules.get("max_daily_add_friend", "N/A"),
"max_daily_message": rules.get("max_daily_send_message", "N/A"),
})
return {
"timestamp": int(_time.time()),
"devices": {"total": total_devices, "online": online},
"fingerprint_collisions": fp_collision_devices,
"account_lifecycle": lifecycle_summary,
}
@router.get("/anti-ban/device/{device_id}")
async def anti_ban_device_detail(device_id: str):
"""单设备防封详情"""
device = await device_manager.get_device(device_id)
if not device:
raise HTTPException(status_code=404, detail="设备不存在")
plat = device.get("platform", "wechat")
phase = await account_lifecycle.get_phase(device_id, plat)
rules = await account_lifecycle.get_rules(device_id, plat)
fp = device.get("fingerprint_hash", "")
collision_result = {"collision": False, "collided_with": []}
if fp and device_manager.db:
collision_result = await device_manager.check_fingerprint_collision(
device_id, device.get("fingerprint_info", {})
)
return {
"device_id": device_id,
"status": device.get("status", "unknown"),
"fingerprint_hash": fp,
"fingerprint_collision": collision_result,
"account_phase": phase.value if hasattr(phase, "value") else str(phase),
"phase_rules": rules,
"last_heartbeat": str(device.get("last_heartbeat", "")),
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,438 @@
"""
微信 UI 导航缓存系统 v1.0
—— 记录所有页面路径、元素坐标,避免重复探索,支持自动纠错
核心理念:
1. 首次探索时录制页面结构到 JSON持久化
2. 后续操作直接查缓存,跳过 uiautomator dump
3. 缓存失效(点击后页面不符预期)自动触发重新探索
4. 每个 action 的操作步骤作为 "路径模板" 存储
"""
import json
import time
import os
import re
import logging
from typing import Dict, Any, List, Optional, Tuple
from pathlib import Path
logger = logging.getLogger(__name__)
CACHE_DIR = Path(__file__).parent.parent.parent / "data" / "nav_cache"
LOG_DIR = Path(__file__).parent.parent.parent / "data" / "operation_logs"
class PageSignature:
"""页面特征签名——用于快速验证当前在哪个页面"""
def __init__(self, name: str, must_have: List[str] = None,
must_not_have: List[str] = None, title: str = None):
self.name = name
self.must_have = must_have or []
self.must_not_have = must_not_have or []
self.title = title
def match(self, texts: List[str]) -> bool:
text_set = set(texts)
for t in self.must_have:
if t not in text_set:
return False
for t in self.must_not_have:
if t in text_set:
return False
if self.title and self.title not in text_set:
return False
return True
def to_dict(self):
return {
"name": self.name,
"must_have": self.must_have,
"must_not_have": self.must_not_have,
"title": self.title,
}
@classmethod
def from_dict(cls, d):
return cls(d["name"], d.get("must_have", []),
d.get("must_not_have", []), d.get("title"))
class NavStep:
"""单步导航操作"""
def __init__(self, action: str, target: str = None,
coords: List[int] = None, wait: float = 1.0,
verify_page: str = None, fallback: str = None):
self.action = action # tap_text / tap_coord / tap_tab / scroll_up / scroll_down / back / swipe
self.target = target # 文本目标 or tab名
self.coords = coords # [x, y] 精确坐标(缓存命中时用)
self.wait = wait
self.verify_page = verify_page # 期望到达的页面签名名
self.fallback = fallback # 失败后备用动作
def to_dict(self):
return {k: v for k, v in {
"action": self.action, "target": self.target,
"coords": self.coords, "wait": self.wait,
"verify_page": self.verify_page, "fallback": self.fallback,
}.items() if v is not None}
@classmethod
def from_dict(cls, d):
return cls(**d)
class NavPath:
"""一条完整的导航路径(从微信首页到目标页面)"""
def __init__(self, name: str, steps: List[NavStep] = None,
target_page: str = None):
self.name = name
self.steps = steps or []
self.target_page = target_page
self.success_count = 0
self.fail_count = 0
self.last_success = None
def to_dict(self):
return {
"name": self.name,
"steps": [s.to_dict() for s in self.steps],
"target_page": self.target_page,
"success_count": self.success_count,
"fail_count": self.fail_count,
"last_success": self.last_success,
}
@classmethod
def from_dict(cls, d):
p = cls(d["name"], [NavStep.from_dict(s) for s in d.get("steps", [])],
d.get("target_page"))
p.success_count = d.get("success_count", 0)
p.fail_count = d.get("fail_count", 0)
p.last_success = d.get("last_success")
return p
class WeChatNavCache:
"""导航缓存管理器"""
def __init__(self, wechat_version: str = "8.0.56",
screen_w: int = 1080, screen_h: int = 2400):
self.version = wechat_version
self.screen_w = screen_w
self.screen_h = screen_h
CACHE_DIR.mkdir(parents=True, exist_ok=True)
self.cache_file = CACHE_DIR / f"nav_v{wechat_version}_{screen_w}x{screen_h}.json"
self.pages: Dict[str, Dict] = {}
self.paths: Dict[str, NavPath] = {}
self.signatures: Dict[str, PageSignature] = {}
self.element_cache: Dict[str, Dict[str, Any]] = {}
self._load()
if not self.pages:
self._init_builtin()
def _load(self):
if self.cache_file.exists():
try:
data = json.loads(self.cache_file.read_text(encoding="utf-8"))
for name, page in data.get("pages", {}).items():
self.pages[name] = page
for name, path in data.get("paths", {}).items():
self.paths[name] = NavPath.from_dict(path)
for name, sig in data.get("signatures", {}).items():
self.signatures[name] = PageSignature.from_dict(sig)
self.element_cache = data.get("element_cache", {})
logger.info(f"导航缓存已加载: {len(self.pages)} 页面, "
f"{len(self.paths)} 路径, {len(self.element_cache)} 元素")
except Exception as e:
logger.warning(f"加载缓存失败: {e}")
def save(self):
data = {
"version": self.version,
"screen": f"{self.screen_w}x{self.screen_h}",
"updated": time.strftime("%Y-%m-%d %H:%M:%S"),
"pages": self.pages,
"paths": {k: v.to_dict() for k, v in self.paths.items()},
"signatures": {k: v.to_dict() for k, v in self.signatures.items()},
"element_cache": self.element_cache,
}
self.cache_file.write_text(
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def _init_builtin(self):
"""初始化内置的已知页面数据(基于真机录制 2026-03-14"""
# ─── 底部 Tab 坐标(精确录制) ───
tab_y = 2148
self.element_cache["tabs"] = {
"微信": {"center": [135, tab_y], "bounds": [0, 2119, 270, 2176]},
"通讯录": {"center": [405, tab_y], "bounds": [270, 2119, 540, 2176]},
"发现": {"center": [675, tab_y], "bounds": [540, 2119, 810, 2176]},
"": {"center": [945, tab_y], "bounds": [810, 2119, 1080, 2176]},
}
# ─── 页面特征签名 ───
sigs = {
"home": PageSignature("home", must_have=[], title=None),
"me": PageSignature("me", must_have=["服务", "收藏", "设置"]),
"settings": PageSignature("settings", must_have=["账号与安全"],
title="设置"),
"account_security": PageSignature(
"account_security", must_have=["微信号", "手机号", "微信密码"],
title="账号与安全"),
"safety_center": PageSignature(
"safety_center", must_have=["微信安全中心"]),
"contacts": PageSignature("contacts", must_have=["新的朋友"]),
"discover": PageSignature("discover", must_have=["朋友圈", "视频号"]),
}
for name, sig in sigs.items():
self.signatures[name] = sig
# ─── "我"页面元素坐标(精确录制) ───
self.pages["me"] = {
"recorded_at": "2026-03-14 16:32",
"elements": {
"头像区": {"center": [608, 298]},
"微信号": {"center": [643, 398], "text": "微信号Lytiao1"},
"状态": {"center": [379, 507]},
"3个朋友": {"center": [676, 507]},
"服务": {"center": [197, 706]},
"收藏": {"center": [197, 883]},
"朋友圈": {"center": [220, 1037]},
"视频号": {"center": [220, 1191]},
"订单与卡包": {"center": [266, 1345]},
"表情": {"center": [197, 1499]},
"设置": {"center": [197, 1676]},
},
}
# ─── 设置页面元素坐标 ───
self.pages["settings"] = {
"recorded_at": "2026-03-14 16:32",
"needs_scroll_to_top": True,
"elements": {
"账号与安全": {"center": [250, 300], "note": "顶部滑后第一项"},
"青少年模式": {"center": [250, 479]},
"关怀模式": {"center": [158, 631]},
"新消息通知": {"center": [204, 800]},
"聊天": {"center": [112, 958]},
"通用": {"center": [158, 1112]},
"隐私": {"center": [89, 1257], "section_header": True},
"朋友权限": {"center": [112, 1423]},
"个人信息与权限": {"center": [135, 1574]},
"个人信息收集清单": {"center": [158, 1725]},
"第三方信息共享清单": {"center": [181, 1876]},
"插件": {"center": [89, 2018]},
"关于微信": {"center": [89, 2168]},
},
}
# ─── 账号与安全页面 ───
self.pages["account_security"] = {
"recorded_at": "2026-03-14 16:33",
"elements": {
"微信号": {"center": [112, 300], "value_center": [603, 300]},
"手机号": {"center": [112, 451], "value_center": [603, 451]},
"微信密码": {"center": [135, 625]},
"声音锁": {"center": [112, 778]},
"应急联系人": {"center": [158, 953]},
"登录过的设备": {"center": [181, 1104]},
"更多安全设置": {"center": [181, 1257]},
"微信安全中心": {"center": [513, 1423]},
},
}
# ─── 常用导航路径 ───
self.paths["to_me"] = NavPath("to_me", [
NavStep("tap_tab", "", [945, tab_y], 1.5, "me"),
], "me")
self.paths["to_settings"] = NavPath("to_settings", [
NavStep("tap_tab", "", [945, tab_y], 1.5, "me"),
NavStep("scroll_up", None, None, 0.5),
NavStep("tap_text", "设置", [197, 1676], 2.0, "settings"),
NavStep("scroll_to_top", None, None, 0.5),
], "settings")
self.paths["to_account_security"] = NavPath("to_account_security", [
NavStep("tap_tab", "", [945, tab_y], 1.5, "me"),
NavStep("scroll_up", None, None, 0.5),
NavStep("tap_text", "设置", [197, 1676], 2.0, "settings"),
NavStep("scroll_to_top", None, None, 0.5),
NavStep("tap_text", "账号与安全", [250, 300], 2.0, "account_security"),
], "account_security")
self.paths["to_safety_center"] = NavPath("to_safety_center", [
NavStep("tap_tab", "", [945, tab_y], 1.5, "me"),
NavStep("scroll_up", None, None, 0.5),
NavStep("tap_text", "设置", [197, 1676], 2.0, "settings"),
NavStep("scroll_to_top", None, None, 0.5),
NavStep("tap_text", "账号与安全", [250, 300], 2.0, "account_security"),
NavStep("tap_text", "微信安全中心", [513, 1423], 3.0, "safety_center"),
], "safety_center")
self.paths["to_discover"] = NavPath("to_discover", [
NavStep("tap_tab", "发现", [675, tab_y], 1.5, "discover"),
], "discover")
self.paths["to_contacts"] = NavPath("to_contacts", [
NavStep("tap_tab", "通讯录", [405, tab_y], 1.5, "contacts"),
], "contacts")
self.save()
logger.info("内置导航缓存已初始化并保存")
def get_tab_coord(self, tab_name: str) -> Optional[List[int]]:
tabs = self.element_cache.get("tabs", {})
if tab_name in tabs:
return tabs[tab_name]["center"]
return None
def get_element_coord(self, page: str, element: str) -> Optional[List[int]]:
page_data = self.pages.get(page, {})
elements = page_data.get("elements", {})
if element in elements:
return elements[element].get("center")
return None
def get_path(self, path_name: str) -> Optional[NavPath]:
return self.paths.get(path_name)
def record_page(self, page_name: str, elements: Dict[str, Dict]):
"""录制/更新一个页面的元素数据"""
self.pages[page_name] = {
"recorded_at": time.strftime("%Y-%m-%d %H:%M"),
"elements": elements,
}
self.save()
logger.info(f"页面 [{page_name}] 已录制: {len(elements)} 个元素")
def record_path_result(self, path_name: str, success: bool):
"""记录路径执行结果"""
path = self.paths.get(path_name)
if path:
if success:
path.success_count += 1
path.last_success = time.strftime("%Y-%m-%d %H:%M:%S")
else:
path.fail_count += 1
self.save()
def update_element_coord(self, page: str, element: str,
new_center: List[int]):
"""自动纠错:更新元素坐标"""
if page not in self.pages:
self.pages[page] = {"elements": {}}
if "elements" not in self.pages[page]:
self.pages[page]["elements"] = {}
if element not in self.pages[page]["elements"]:
self.pages[page]["elements"][element] = {}
old = self.pages[page]["elements"][element].get("center")
self.pages[page]["elements"][element]["center"] = new_center
self.pages[page]["elements"][element]["updated_at"] = \
time.strftime("%Y-%m-%d %H:%M")
self.save()
logger.info(f"坐标纠错: [{page}].{element} {old} -> {new_center}")
class OperationLogger:
"""操作日志记录器——每次操作自动记录步骤/耗时/结果"""
def __init__(self, device_id: str = ""):
LOG_DIR.mkdir(parents=True, exist_ok=True)
self.device_id = device_id
self.log_file = LOG_DIR / f"ops_{time.strftime('%Y%m%d')}.jsonl"
self._current_op = None
def start_operation(self, action: str, params: dict):
self._current_op = {
"action": action,
"params": {k: v for k, v in params.items()
if k not in ("password", "pwd", "new_pwd", "old_pwd")},
"device": self.device_id,
"started_at": time.strftime("%Y-%m-%d %H:%M:%S"),
"start_ts": time.time(),
"steps": [],
}
def log_step(self, step_type: str, detail: str = "",
coords: List[int] = None, success: bool = True):
if not self._current_op:
return
self._current_op["steps"].append({
"type": step_type,
"detail": detail,
"coords": coords,
"success": success,
"ts": round(time.time() - self._current_op["start_ts"], 2),
})
def end_operation(self, success: bool, result: dict = None,
error: str = None):
if not self._current_op:
return
self._current_op["success"] = success
self._current_op["duration_ms"] = round(
(time.time() - self._current_op["start_ts"]) * 1000)
self._current_op["ended_at"] = time.strftime("%Y-%m-%d %H:%M:%S")
if result:
self._current_op["result_summary"] = str(result)[:200]
if error:
self._current_op["error"] = error
del self._current_op["start_ts"]
try:
with open(self.log_file, "a", encoding="utf-8") as f:
f.write(json.dumps(self._current_op, ensure_ascii=False) + "\n")
except Exception as e:
logger.warning(f"写入操作日志失败: {e}")
op = self._current_op
self._current_op = None
return op
def get_recent_ops(self, limit: int = 20) -> List[dict]:
"""读取最近的操作记录"""
if not self.log_file.exists():
return []
ops = []
try:
with open(self.log_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
ops.append(json.loads(line))
except Exception:
pass
return ops[-limit:]
def get_action_stats(self) -> Dict[str, Dict]:
"""统计每个 action 的成功率和平均耗时"""
ops = self.get_recent_ops(1000)
stats: Dict[str, Dict] = {}
for op in ops:
action = op.get("action", "unknown")
if action not in stats:
stats[action] = {"total": 0, "success": 0,
"total_ms": 0, "errors": []}
stats[action]["total"] += 1
if op.get("success"):
stats[action]["success"] += 1
stats[action]["total_ms"] += op.get("duration_ms", 0)
if op.get("error"):
stats[action]["errors"].append(op["error"][:50])
for action, s in stats.items():
s["success_rate"] = (
f"{s['success']/s['total']*100:.0f}%" if s["total"] else "N/A")
s["avg_ms"] = (
s["total_ms"] // s["total"] if s["total"] else 0)
return stats