优化验证码展示

This commit is contained in:
yml2213
2026-09-06 13:02:37 +08:00
parent 8309072120
commit 4f9a6864eb
7 changed files with 972 additions and 30 deletions
+143 -22
View File
@@ -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)