优化验证码展示
This commit is contained in:
@@ -10,6 +10,7 @@ import time
|
||||
import uuid
|
||||
from collections.abc import Mapping
|
||||
from http.cookies import SimpleCookie
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from urllib.parse import parse_qs, quote, urlparse
|
||||
|
||||
@@ -20,6 +21,7 @@ import requests
|
||||
from loguru import logger
|
||||
from PIL import Image
|
||||
|
||||
from .capture import CaptchaCaptureRecord
|
||||
from .ocr import HuyaCaptchaOcr, default_ocr
|
||||
from .track import format_track, generate_slide_track
|
||||
|
||||
@@ -43,11 +45,14 @@ class HuyaQrAuthRequiredError(HuyaVerificationError):
|
||||
|
||||
|
||||
class HuyaSmsAuthRequiredError(HuyaVerificationError):
|
||||
"""风控策略为短信验证(dx_auth),需接码/收码后提交。"""
|
||||
"""风控策略为短信验证(sms_auth/sms_auth2),需接码/收码后提交。"""
|
||||
|
||||
def __init__(self, challenge_payload: dict | str, message: str = ""):
|
||||
def __init__(
|
||||
self, challenge_payload: dict | str, message: str = "", mobile: str = ""
|
||||
):
|
||||
self.challenge_payload = challenge_payload
|
||||
super().__init__(message or "虎牙风控要求短信验证")
|
||||
self.mobile = mobile
|
||||
super().__init__(message or f"虎牙风控要求短信验证,手机号: {mobile or '?'}")
|
||||
|
||||
|
||||
def _cookie_mapping(cookie: Mapping[str, str] | str | None) -> dict[str, str]:
|
||||
@@ -76,6 +81,9 @@ class HuyaVerificationSolver:
|
||||
app_id: str = "5002",
|
||||
page_url: str | None = None,
|
||||
use_touch_events: bool = False,
|
||||
account: str = "",
|
||||
capture_dir: str | Path | None = None,
|
||||
enable_capture: bool = True,
|
||||
):
|
||||
self.cookie = _cookie_mapping(cookie)
|
||||
self.ua = ua or DEFAULT_UA
|
||||
@@ -102,6 +110,12 @@ class HuyaVerificationSolver:
|
||||
self.timeout = timeout
|
||||
self.ocr = ocr or default_ocr()
|
||||
|
||||
self.account = account
|
||||
self.capture_dir = capture_dir
|
||||
self.enable_capture = enable_capture
|
||||
self.current_capture: CaptchaCaptureRecord | None = None
|
||||
self.auth_type = ""
|
||||
|
||||
self.huyapk = ""
|
||||
self.scene_type = ""
|
||||
self.code = ""
|
||||
@@ -275,7 +289,7 @@ class HuyaVerificationSolver:
|
||||
except (KeyError, IndexError, TypeError) as exc:
|
||||
raise HuyaVerificationError("虎牙响应中没有 strategys[0].data") from exc
|
||||
|
||||
def _parse_strategy(self, challenge_payload: dict | str) -> None:
|
||||
def _parse_strategy(self, challenge_payload: dict | str, step: int = 1) -> None:
|
||||
strategy_url = self._extract_strategy_url(challenge_payload)
|
||||
|
||||
query_params = parse_qs(urlparse(strategy_url).query)
|
||||
@@ -306,15 +320,71 @@ class HuyaVerificationSolver:
|
||||
# 实测(2026-08-25): strategy=64 时 pt_auth.html 是滑块、qr_auth.html 才是扫码,
|
||||
# 旧的 `strategy==64 → 扫码` 硬编码会把可自动化的滑块误杀成死路。
|
||||
is_qr_auth = "qr_auth" in strategy_url_lower
|
||||
if is_qr_auth and "dx_auth" not in strategy_url_lower:
|
||||
account_id = self.account or self.url_param_map.get("mobile", "")
|
||||
if is_qr_auth:
|
||||
if self.enable_capture:
|
||||
cap = CaptchaCaptureRecord(
|
||||
auth_type="qr_auth",
|
||||
account=account_id,
|
||||
page_url=self.strategy_url,
|
||||
scene_type=self.scene_type,
|
||||
output_base_dir=Path(self.capture_dir)
|
||||
if self.capture_dir
|
||||
else None,
|
||||
)
|
||||
cap.capture_qr_auth_data(
|
||||
mobile=self.url_param_map.get("mobile", "?"),
|
||||
page_url=self.strategy_url,
|
||||
scene_type=self.scene_type,
|
||||
step=step,
|
||||
raw_params=self.url_param_map,
|
||||
)
|
||||
logger.warning(
|
||||
"[solver] 虎牙风控在第 {} 步要求扫码验证 (qr_auth),需 App 确认,账号: {}",
|
||||
step,
|
||||
self.url_param_map.get("mobile", "?"),
|
||||
)
|
||||
raise HuyaQrAuthRequiredError(
|
||||
"虎牙风控要求扫码验证(qr_auth),需已登录App设备确认,"
|
||||
f"账号: {self.url_param_map.get('mobile', '?')}"
|
||||
)
|
||||
if "dx_auth" in strategy_url_lower:
|
||||
if "sms_auth" in strategy_url_lower:
|
||||
mobile = self.url_param_map.get("mobile", "?")
|
||||
if self.enable_capture:
|
||||
cap = CaptchaCaptureRecord(
|
||||
auth_type="sms_auth",
|
||||
account=account_id,
|
||||
page_url=self.strategy_url,
|
||||
scene_type=self.scene_type,
|
||||
output_base_dir=Path(self.capture_dir)
|
||||
if self.capture_dir
|
||||
else None,
|
||||
)
|
||||
cap.capture_sms_auth_data(
|
||||
mobile=mobile,
|
||||
page_url=self.strategy_url,
|
||||
scene_type=self.scene_type,
|
||||
step=step,
|
||||
raw_params=self.url_param_map,
|
||||
)
|
||||
logger.warning(
|
||||
"[solver] 虎牙风控在第 {} 步要求短信验证 (sms_auth),手机号: {}",
|
||||
step,
|
||||
mobile,
|
||||
)
|
||||
raise HuyaSmsAuthRequiredError(
|
||||
challenge_payload,
|
||||
f"虎牙风控要求短信验证(dx_auth),手机号: {self.url_param_map.get('mobile', '?')}",
|
||||
f"虎牙风控要求短信验证(sms_auth),手机号: {mobile}",
|
||||
mobile=mobile,
|
||||
)
|
||||
self.auth_type = "dx_auth" if "dx_auth" in strategy_url_lower else "pt_auth"
|
||||
if self.enable_capture:
|
||||
self.current_capture = CaptchaCaptureRecord(
|
||||
auth_type=self.auth_type,
|
||||
account=account_id,
|
||||
page_url=self.strategy_url,
|
||||
scene_type=self.scene_type,
|
||||
output_base_dir=Path(self.capture_dir) if self.capture_dir else None,
|
||||
)
|
||||
self.behavior_events = []
|
||||
|
||||
@@ -357,18 +427,49 @@ class HuyaVerificationSolver:
|
||||
opencv_x = self._opencv_slider_x(tip_bytes, restored_bg)
|
||||
if abs(opencv_x - yolo_x) < 10:
|
||||
logger.debug("虎牙滑块识别距离: yolo={}, opencv={}", yolo_x, opencv_x)
|
||||
return int((opencv_x + yolo_x) / 2)
|
||||
logger.debug(
|
||||
"虎牙滑块识别距离: yolo={}, opencv={}, 使用yolo", yolo_x, opencv_x
|
||||
)
|
||||
return yolo_x
|
||||
final_x = int((opencv_x + yolo_x) / 2)
|
||||
else:
|
||||
logger.debug(
|
||||
"虎牙滑块识别距离: yolo={}, opencv={}, 使用yolo", yolo_x, opencv_x
|
||||
)
|
||||
final_x = yolo_x
|
||||
|
||||
logger.info("虎牙验证类型: 点选")
|
||||
if self.current_capture:
|
||||
try:
|
||||
self.current_capture.capture_slider_data(
|
||||
code=self.code,
|
||||
dtu_bytes=bg_bytes,
|
||||
puzzle_bytes=tip_bytes,
|
||||
restored_bg_bytes=restored_bg,
|
||||
composite_bytes=composite,
|
||||
point_x=final_x,
|
||||
raw_data=data,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug(f"[solver] 记录滑块捕获数据异常: {exc}")
|
||||
|
||||
return final_x
|
||||
|
||||
logger.info("虎牙验证类型: 点选 (dx_auth)")
|
||||
bg_bytes = self._decode_image(data.get("dTuStr"))
|
||||
tip_bytes = self._decode_image(data.get("hZiStr"))
|
||||
merged = self._vertical_concat_align_left(bg_bytes, tip_bytes)
|
||||
points = self.ocr.ocr_click_points(merged)
|
||||
return [[int(x), int(y) - 44] for x, y in points]
|
||||
final_points = [[int(x), int(y) - 44] for x, y in points]
|
||||
|
||||
if self.current_capture:
|
||||
try:
|
||||
self.current_capture.capture_click_data(
|
||||
code=self.code,
|
||||
dtu_bytes=bg_bytes,
|
||||
hzi_bytes=tip_bytes,
|
||||
points=final_points,
|
||||
raw_data=data,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug(f"[solver] 记录点选捕获数据异常: {exc}")
|
||||
|
||||
return final_points
|
||||
|
||||
def _send_slider_verify(self, url: str, point: int) -> dict:
|
||||
track_points = generate_slide_track(point)
|
||||
@@ -451,6 +552,11 @@ class HuyaVerificationSolver:
|
||||
bool(data.get("authId")),
|
||||
bool(data.get("nextAuthUrl")),
|
||||
)
|
||||
if self.current_capture:
|
||||
try:
|
||||
self.current_capture.record_submit_result(data)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug(f"[solver] 记录提交结果异常: {exc}")
|
||||
return data
|
||||
|
||||
def _solve_current_strategy(self) -> dict:
|
||||
@@ -542,15 +648,24 @@ class HuyaVerificationSolver:
|
||||
return last_result
|
||||
|
||||
def solve(self, challenge_payload: dict | str) -> dict:
|
||||
"""提交虎牙风控验证,返回 vurl 响应中的 data。"""
|
||||
self._parse_strategy(challenge_payload)
|
||||
result = self._solve_current_strategy()
|
||||
|
||||
next_auth_url = result.get("nextAuthUrl") if isinstance(result, dict) else None
|
||||
if next_auth_url:
|
||||
logger.info("虎牙滑块后继续点选验证")
|
||||
self._parse_strategy(next_auth_url)
|
||||
"""提交虎牙风控验证,支持多级风控流转 (如 pt_auth -> dx_auth -> ...)。"""
|
||||
current_challenge: dict | str = challenge_payload
|
||||
result: dict = {}
|
||||
for step in range(5):
|
||||
self._parse_strategy(current_challenge, step=step + 1)
|
||||
result = self._solve_current_strategy()
|
||||
if not isinstance(result, dict):
|
||||
break
|
||||
if result.get("authId"):
|
||||
logger.info(f"虎牙验证在第 {step + 1} 步成功获取 authId")
|
||||
break
|
||||
next_auth_url = result.get("nextAuthUrl")
|
||||
if not next_auth_url:
|
||||
break
|
||||
logger.info(
|
||||
f"虎牙验证第 {step + 1} 步完成,进入下一阶段验证: {next_auth_url[:80]}..."
|
||||
)
|
||||
current_challenge = next_auth_url
|
||||
return result
|
||||
|
||||
|
||||
@@ -564,6 +679,9 @@ def solve_huya_verification(
|
||||
app_id: str = "5002",
|
||||
page_url: str | None = None,
|
||||
use_touch_events: bool = False,
|
||||
account: str = "",
|
||||
capture_dir: str | Path | None = None,
|
||||
enable_capture: bool = True,
|
||||
) -> dict:
|
||||
"""函数式入口,便于登录流程直接调用。"""
|
||||
solver = HuyaVerificationSolver(
|
||||
@@ -575,5 +693,8 @@ def solve_huya_verification(
|
||||
app_id=app_id,
|
||||
page_url=page_url,
|
||||
use_touch_events=use_touch_events,
|
||||
account=account,
|
||||
capture_dir=capture_dir,
|
||||
enable_capture=enable_capture,
|
||||
)
|
||||
return solver.solve(challenge_payload)
|
||||
|
||||
Reference in New Issue
Block a user