feat(huya): 支持四段式账号导入接码及旋转/文本点选多级验证码闭环过验
- 账号导入:支持'账号----密码----手机号----接码链接'四段式解析与持久化存储 - 旋转验证码 (rotate_auth / xz):逆向分析官方协议,基于圆周边界采样互相关算法秒级计算旋转还原角度 - 文本点选验证码 (dx_auth):内置成语与古诗名句语料,支持题干菜名提取与古诗/成语挖空正则求解及小图动态合成 - 短信二次验证 (sms_auth2):结合 sms_url 自动轮询接码链接并 Hex 编码提交官方过验 - 前端交互:增加接码标签、在线查询短信验证码、一键自动短信登录与测试接码链接功能 - 回归测试:新增旋转验证、短信接码流与文本点选测试用例(173 passed)
This commit is contained in:
+16
-2
@@ -230,8 +230,9 @@ def solve_safe_auth(
|
||||
max_retry: int = 3,
|
||||
device_info: Mapping[str, object] | None = None,
|
||||
account: str = "",
|
||||
sms_url: str = "",
|
||||
) -> dict:
|
||||
"""解 safe_auth 风控滑块/点选。"""
|
||||
"""解 safe_auth 风控滑块/点选/短信。"""
|
||||
from .verification.solver import (
|
||||
HuyaQrAuthRequiredError,
|
||||
HuyaSmsAuthRequiredError,
|
||||
@@ -254,6 +255,7 @@ def solve_safe_auth(
|
||||
page_url=risk_url,
|
||||
use_touch_events=True,
|
||||
account=account,
|
||||
sms_url=sms_url,
|
||||
)
|
||||
try:
|
||||
result = solver.solve(risk_url)
|
||||
@@ -298,6 +300,7 @@ def login_cred_with_flow(
|
||||
device_info: dict | None = None,
|
||||
proxies: dict | None = None,
|
||||
include_device_token: bool = False,
|
||||
sms_url: str = "",
|
||||
) -> tuple[bytes, int] | tuple[bytes, int, str]:
|
||||
"""新注册设备后登录,返回 ``(新鲜 cred, 真实 uid)``。
|
||||
|
||||
@@ -351,7 +354,13 @@ def login_cred_with_flow(
|
||||
raise HuyaAppQrAuthRequiredError(
|
||||
"该账号 App 渠道要求扫码验证(qr_auth),请先在手机虎牙 App 上正常登录一次建立设备信任。"
|
||||
)
|
||||
solve_safe_auth(risk_url, proxies=proxies, device_info=dev, account=account)
|
||||
solve_safe_auth(
|
||||
risk_url,
|
||||
proxies=proxies,
|
||||
device_info=dev,
|
||||
account=account,
|
||||
sms_url=sms_url,
|
||||
)
|
||||
logger.info(f"[huya-app] safe_auth ({kind}) 过验成功,重发 WUP 登录...")
|
||||
continue
|
||||
markers = _response_markers(resp)
|
||||
@@ -464,12 +473,14 @@ class HuyaAppPasswordLogin:
|
||||
timeout: tuple[float, float] | None = None,
|
||||
force_new_device: bool = False,
|
||||
device_info: dict | None = None,
|
||||
sms_url: str = "",
|
||||
):
|
||||
self.username = username.strip()
|
||||
self.password = password
|
||||
self.proxies = dict(proxies) if proxies else None
|
||||
self.timeout = timeout or (10.0, 25.0)
|
||||
self.force_new_device = force_new_device
|
||||
self.sms_url = sms_url.strip()
|
||||
if force_new_device:
|
||||
reset_account_state(self.username)
|
||||
self.device_info = device_info or get_profile(
|
||||
@@ -649,6 +660,7 @@ class HuyaAppPasswordLogin:
|
||||
device_info=self.device_info,
|
||||
proxies=self.proxies,
|
||||
include_device_token=True,
|
||||
sms_url=self.sms_url,
|
||||
)
|
||||
except HuyaAppQrAuthRequiredError as exc:
|
||||
logger.warning(f"[huya-app] 账号 {acct} 登录受阻: {exc}")
|
||||
@@ -936,6 +948,7 @@ def login_huya_app_password(
|
||||
proxies: Mapping[str, str] | None = None,
|
||||
timeout: tuple[float, float] | None = None,
|
||||
force_new_device: bool = False,
|
||||
sms_url: str = "",
|
||||
) -> HuyaLoginResult:
|
||||
"""函数式入口:使用 App 协议执行虎牙密码登录并获取全套 Cookie。"""
|
||||
return HuyaAppPasswordLogin(
|
||||
@@ -944,4 +957,5 @@ def login_huya_app_password(
|
||||
proxies=proxies,
|
||||
timeout=timeout,
|
||||
force_new_device=force_new_device,
|
||||
sms_url=sms_url,
|
||||
).login()
|
||||
|
||||
@@ -48,12 +48,16 @@ class CaptchaCaptureRecord:
|
||||
"点选验证 (dx_auth)"
|
||||
if "dx" in auth_type
|
||||
else (
|
||||
"短信二次验证 (sms_auth)"
|
||||
if "sms" in auth_type
|
||||
"旋转验证 (rotate_auth)"
|
||||
if "rotate" in auth_type or "xz" in auth_type
|
||||
else (
|
||||
"扫码验证 (qr_auth)"
|
||||
if "qr" in auth_type
|
||||
else "滑块验证 (pt_auth)"
|
||||
"短信二次验证 (sms_auth)"
|
||||
if "sms" in auth_type
|
||||
else (
|
||||
"扫码验证 (qr_auth)"
|
||||
if "qr" in auth_type
|
||||
else "滑块验证 (pt_auth)"
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -161,6 +165,93 @@ class CaptchaCaptureRecord:
|
||||
self._log_summary()
|
||||
return folder
|
||||
|
||||
def capture_rotate_data(
|
||||
self,
|
||||
*,
|
||||
code: str,
|
||||
dtu_bytes: bytes,
|
||||
puzzle_bytes: bytes,
|
||||
restored_bg_bytes: bytes | None = None,
|
||||
angle: int | float | None = None,
|
||||
raw_data: dict[str, Any] | None = None,
|
||||
) -> Path:
|
||||
"""捕获旋转验证码的所有图片和元数据。"""
|
||||
folder = self._ensure_target_dir(code)
|
||||
|
||||
raw = raw_data or {}
|
||||
self.meta["code"] = code
|
||||
self.meta["prompts"] = {
|
||||
"title": "安全验证 (旋转验证 rotate_auth)",
|
||||
"default_instruction": "请拖动滑块,将图片旋转至正确位置",
|
||||
"description": raw.get("description") or raw.get("message") or "",
|
||||
"tip": raw.get("tip") or "",
|
||||
}
|
||||
|
||||
images_info = {}
|
||||
if dtu_bytes:
|
||||
(folder / "bg_scrambled.jpg").write_bytes(dtu_bytes)
|
||||
images_info["bg_scrambled"] = "bg_scrambled.jpg"
|
||||
|
||||
if puzzle_bytes:
|
||||
(folder / "puzzle.png").write_bytes(puzzle_bytes)
|
||||
images_info["puzzle"] = "puzzle.png"
|
||||
|
||||
if restored_bg_bytes:
|
||||
(folder / "bg_restored.jpg").write_bytes(restored_bg_bytes)
|
||||
images_info["bg_restored"] = "bg_restored.jpg"
|
||||
|
||||
# 绘制旋转合成标注图
|
||||
if restored_bg_bytes and puzzle_bytes and angle is not None:
|
||||
try:
|
||||
bg = cv2.imdecode(
|
||||
np.frombuffer(restored_bg_bytes, np.uint8), cv2.IMREAD_COLOR
|
||||
)
|
||||
puzzle_raw = cv2.imdecode(
|
||||
np.frombuffer(puzzle_bytes, np.uint8), cv2.IMREAD_UNCHANGED
|
||||
)
|
||||
if bg is not None and puzzle_raw is not None:
|
||||
M = cv2.getRotationMatrix2D((90.0, 90.0), -float(angle), 1.0)
|
||||
rotated = cv2.warpAffine(puzzle_raw, M, (180, 180))
|
||||
alpha = rotated[:, :, 3] / 255.0
|
||||
composite = bg.copy()
|
||||
for c in range(3):
|
||||
composite[0:180, 105:285, c] = (
|
||||
rotated[:, :, c] * alpha
|
||||
+ composite[0:180, 105:285, c] * (1 - alpha)
|
||||
).astype(np.uint8)
|
||||
cv2.putText(
|
||||
composite,
|
||||
f"Angle={int(angle)} deg",
|
||||
(15, 30),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.8,
|
||||
(0, 255, 0),
|
||||
2,
|
||||
)
|
||||
success, buffer = cv2.imencode(".jpg", composite)
|
||||
if success:
|
||||
(folder / "annotated.jpg").write_bytes(buffer.tobytes())
|
||||
images_info["annotated"] = "annotated.jpg"
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug(f"[captcha_capture] 标注旋转图异常: {exc}")
|
||||
|
||||
self.meta["images"] = images_info
|
||||
if angle is not None:
|
||||
self.meta["solution"] = {"type": "rotate", "angle": int(angle)}
|
||||
|
||||
if raw_data:
|
||||
clean_raw = {
|
||||
k: v for k, v in raw_data.items() if not str(v).startswith("data:image")
|
||||
}
|
||||
(folder / "raw_data.json").write_text(
|
||||
json.dumps(clean_raw, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
self._flush_meta()
|
||||
self._log_summary()
|
||||
return folder
|
||||
|
||||
def capture_click_data(
|
||||
self,
|
||||
*,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,986 @@
|
||||
一声何满子
|
||||
一夜征人尽望乡
|
||||
一宿行人自可愁
|
||||
一岁一枯荣
|
||||
一弦一柱思华年
|
||||
一时回首月中看
|
||||
一望关山泪满巾
|
||||
一权寒梅白玉条
|
||||
一树梨花落晚风
|
||||
一树碧无情
|
||||
一水中分白鹭洲
|
||||
一片冰心在玉壶
|
||||
一片降幡出石头
|
||||
一行白鹭上青天
|
||||
一览众山小
|
||||
一路经行处
|
||||
一道残阳铺水中
|
||||
万径人踪灭
|
||||
万户捣衣声
|
||||
万方多难此登临
|
||||
万条垂下绿丝绦
|
||||
万里云罗一雁飞
|
||||
万里悲秋常作客
|
||||
万里长征人未还
|
||||
万里黄河绕黑山
|
||||
三山半落青天外
|
||||
三峡星河影动摇
|
||||
三日入厨下
|
||||
三春白雪归青冢
|
||||
上有黄鹂深树鸣
|
||||
下拂波纹破
|
||||
不及石尤风
|
||||
不堪人事日萧条
|
||||
不寝听金钥
|
||||
不尽长江滚滚来
|
||||
不得到辽西
|
||||
不才明主弃
|
||||
不把双眉斗画长
|
||||
不教胡马度阴山
|
||||
不敢过临洮
|
||||
不敢问来人
|
||||
不望入口复上身
|
||||
不着心源傍古人
|
||||
不知何处吹芦管
|
||||
不知心恨谁
|
||||
不知明镜里
|
||||
不知细叶谁裁出
|
||||
不知转入此中来
|
||||
不知近水花先发
|
||||
不见县门身即乐
|
||||
不见沙场愁杀人
|
||||
不解藏踪迹
|
||||
不论平地与山尖
|
||||
与汝救凶年
|
||||
且免向城卖黄犊
|
||||
世事波上舟
|
||||
世事茫茫难自料
|
||||
业复五铢钱
|
||||
东边日出西边雨
|
||||
东风不与周郎便
|
||||
两三星火是瓜洲
|
||||
两个黄鹂鸣翠柳
|
||||
两岸猿声啼不住
|
||||
两鬓苍苍十指黑
|
||||
中军置酒饮归客
|
||||
临行密密缝
|
||||
为他人作嫁衣裳
|
||||
为谁辛苦为谁甜
|
||||
乌衣巷口夕阳斜
|
||||
乍见翻疑梦
|
||||
乐游原上望昭陵
|
||||
九曲黄河万里沙
|
||||
乡书不可寄
|
||||
乡书何处达
|
||||
乡泪客中尽
|
||||
乱山残雪夜
|
||||
乾坤日夜浮
|
||||
二十一人同入蜀
|
||||
二十四桥明月夜
|
||||
二月春风似剪刀
|
||||
云峰隔水深
|
||||
云开远见洛阳城
|
||||
云母屏风烛影深
|
||||
五千貂锦丧胡尘
|
||||
五日雪纷纷
|
||||
五更疏欲断
|
||||
五更鼓角声悲壮
|
||||
五月虽热麦风清
|
||||
亲朋无一字
|
||||
人世几回伤往事
|
||||
人事音书漫寂寥
|
||||
人家不怨言语别
|
||||
人归山郭暗
|
||||
人归暮雪时
|
||||
人闲桂花落
|
||||
人间四月芳菲尽
|
||||
人间能得几回闻
|
||||
今上岳阳楼
|
||||
今夜偏知春气暖
|
||||
今夜未知何处宿
|
||||
今年开州杀刺吏
|
||||
今日水犹寒
|
||||
今日花开又一年
|
||||
今朝此为别
|
||||
今来县宰加朱绂
|
||||
从今四海为家日
|
||||
从来幽并客
|
||||
他乡复行役
|
||||
他年锦里经祠庙
|
||||
仙人垂两足
|
||||
以我独沉久
|
||||
伐薪烧炭南山中
|
||||
会当凌绝顶
|
||||
估客昼眠知浪静
|
||||
但使龙城飞将在
|
||||
但恐贼路多
|
||||
但见泪痕湿
|
||||
但见群鸥日日来
|
||||
但闻人语响
|
||||
低空有断云
|
||||
何处得秋霜
|
||||
何处还相遇
|
||||
何年致此身
|
||||
何当重相见
|
||||
何当金络脑
|
||||
何日平胡虏
|
||||
何物如人意
|
||||
余亦能高咏
|
||||
依旧烟笼十里堤
|
||||
便是生灵血染成
|
||||
借问酒家何处有
|
||||
偏在洛城东
|
||||
停舫临孤驿
|
||||
停车坐爱枫林晚
|
||||
偷采白莲回
|
||||
先遣小姑尝
|
||||
八年十二月
|
||||
八月萧关道
|
||||
六朝如梦鸟空啼
|
||||
共怜时世俭梳妆
|
||||
关山万里远征人
|
||||
关山征戍远
|
||||
关张无命欲何如
|
||||
关月冷相随
|
||||
农夫犹饿死
|
||||
农饥足旱田
|
||||
冥冥日沉夕
|
||||
冥冥花正开
|
||||
冥冥鸟去迟
|
||||
冰簟银床梦不成
|
||||
决眦入归鸟
|
||||
况彼无衣民
|
||||
况是蔡家亲
|
||||
凄凄去亲爱
|
||||
凄凉宝剑篇
|
||||
凄凉蜀故伎
|
||||
凝情自悄然
|
||||
几度隔山川
|
||||
几行归塞尽
|
||||
凤凰台上凤凰游
|
||||
凤去台空江自流
|
||||
凭君传语报平安
|
||||
凭轩涕泗流
|
||||
出塞复入塞
|
||||
初日郢门山
|
||||
前不见古人
|
||||
前年戍月支
|
||||
前年渝州杀刺史
|
||||
劝君更尽一杯酒
|
||||
功盖三分国
|
||||
势分三足鼎
|
||||
北斗七星高
|
||||
北斗阑干南斗斜
|
||||
北极朝廷终不改
|
||||
北阙休上书
|
||||
北风利如剑
|
||||
北风卷地白草折
|
||||
北风吹雁雪纷纷
|
||||
北风江上寒
|
||||
十二楼中月自明
|
||||
十五即乘潮
|
||||
十室八九贫
|
||||
十年一觉扬中梦
|
||||
千寻铁锁沉江底
|
||||
千山鸟飞绝
|
||||
千朵万朵压枝低
|
||||
千树万树梨花开
|
||||
千秋尚凛然
|
||||
千里江陵一日还
|
||||
千里莺啼绿映红
|
||||
千里黄云白日曛
|
||||
半入江风半入云
|
||||
半匹红纱一丈绫
|
||||
半卷红旗临易水
|
||||
半江瑟瑟半江红
|
||||
卖炭得钱何所营
|
||||
南山归敝庐
|
||||
南朝四百八十寺
|
||||
南浦清江万里桥
|
||||
卧龙跃马终黄土
|
||||
危樯独夜舟
|
||||
却喜晒谷天晴
|
||||
去岁曾经此县城
|
||||
去年花里逢君别
|
||||
去时雪满天山路
|
||||
县民无口不冤声
|
||||
参差连曲陌
|
||||
又疑瑶台镜
|
||||
双泪落君前
|
||||
双袖龙钟泪不干
|
||||
受降城外月如霜
|
||||
古台摇落后
|
||||
古寺残灯石壁间
|
||||
只在芦花浅水边
|
||||
只是当时已惘然
|
||||
只是近黄昏
|
||||
可怜九月初三夜
|
||||
可怜后主还祠庙
|
||||
可怜无定河边骨
|
||||
可怜身上衣正单
|
||||
叶间扑扑秋蛾生
|
||||
同到牵牛织女家
|
||||
名岂文章著
|
||||
名成八阵图
|
||||
后不见来者
|
||||
向晚意不适
|
||||
君看似花处
|
||||
含情欲说宫中事
|
||||
听钟未眠客
|
||||
吴宫花草埋幽径
|
||||
吴楚东南坼
|
||||
吹沙复喷石
|
||||
呼作白玉盘
|
||||
呼儿登山收橡实
|
||||
咸言意气高
|
||||
哀哀夜哭向秋云
|
||||
哥舒夜带刀
|
||||
唯烧蒿棘火
|
||||
唯留一湖水
|
||||
唯见林花落
|
||||
商女不知亡国恨
|
||||
啼时惊妾梦
|
||||
啾啾栖鸟过
|
||||
嗟君此别意何如
|
||||
四海无闲田
|
||||
回乐烽前沙似雪
|
||||
回头却向秦云器
|
||||
回看天际下中流
|
||||
回观村闾间
|
||||
回车叱牛牵向北
|
||||
因风想玉珂
|
||||
圣代即今多雨露
|
||||
坐看牵牛织女星
|
||||
垂死病中惊坐起
|
||||
城下没全师
|
||||
城中增暮寒
|
||||
城里月明时
|
||||
城阙夜千重
|
||||
城高遮得贼
|
||||
塞上燕脂凝夜紫
|
||||
塞外征行无尽日
|
||||
壮发上冲冠
|
||||
壶浆满别筵
|
||||
处处共芦草
|
||||
复照青苔上
|
||||
夕阳下通津
|
||||
夕阳依旧垒
|
||||
夕阳无限好
|
||||
多少楼台烟雨中
|
||||
多病故人疏
|
||||
多难识君迟
|
||||
夜半钟声到客船
|
||||
夜来城外一尺雪
|
||||
夜泊秦淮近酒家
|
||||
夜深还过女墙来
|
||||
夜长人自起
|
||||
夜静春山空
|
||||
大漠无兵阻
|
||||
大漠沙如雪
|
||||
天下谁人不识君
|
||||
天地一沙鸥
|
||||
天地英雄气
|
||||
天寒白屋贫
|
||||
天山雪后海风寒
|
||||
天涯哭此时
|
||||
天涯孤棹还
|
||||
天涯涕泪一身遥
|
||||
天涯霜雪霁寒宵
|
||||
天秋月又满
|
||||
天街小雨润如酥
|
||||
天阶夜色凉如水
|
||||
夷歌数处起渔樵
|
||||
如今直上银河去
|
||||
妇女多在官军中
|
||||
始见沙上鸟
|
||||
姑苏城外寒山寺
|
||||
委曲结绳文
|
||||
嫁与弄潮儿
|
||||
嫁得瞿塘贾
|
||||
嫦娥应悔偷灵药
|
||||
孤壁野僧邻
|
||||
孤帆天际看
|
||||
孤帆远影碧空尽
|
||||
孤灯寒照雨
|
||||
孤灯闻楚角
|
||||
孤独异乡人
|
||||
孤舟蓑笠翁
|
||||
孤飞自可疑
|
||||
官应老病休
|
||||
客从东方来
|
||||
客舍青青柳色新
|
||||
客路青山外
|
||||
宫使驱将惜不得
|
||||
宫花寂寞红
|
||||
家书到隔年
|
||||
寂寂花时闭院门
|
||||
寄卧郊扉久
|
||||
寒云夜夜自飞还
|
||||
寒塘欲下迟
|
||||
寒天古木悲
|
||||
寒灯思旧事
|
||||
寒灯独夜人
|
||||
寒磬满空林
|
||||
寒衣不要宽
|
||||
寒雨连江夜入吴
|
||||
寒食东风御柳斜
|
||||
寥落古行宫
|
||||
对棋陪谢傅
|
||||
将军夜引弓
|
||||
将军角弓不得控
|
||||
将牛何处去
|
||||
尊酒慰离颜
|
||||
小园花乱飞
|
||||
小娃撑小艇
|
||||
小时不识月
|
||||
少孤为客早
|
||||
屈子怨何深
|
||||
山中相送罢
|
||||
山回路转不见君
|
||||
山围故国周遭在
|
||||
山寺桃花始盛开
|
||||
山店门前一妇人
|
||||
山形依旧枕寒流
|
||||
山色有无中
|
||||
山风吹空林
|
||||
岁岁金河复玉关
|
||||
岁暮锄犁傍空室
|
||||
岁暮阴阳催短景
|
||||
岐王宅里寻常见
|
||||
岩上无心云相逐
|
||||
岭外音书绝
|
||||
岱宗夫如何
|
||||
崔九堂前几度闻
|
||||
川原杳何极
|
||||
巫峡啼猿数行泪
|
||||
市南门外泥中歇
|
||||
布絮不蔽身
|
||||
帝乡明日到
|
||||
平明寻白羽
|
||||
平明送客楚江孤
|
||||
平沙万里绝人烟
|
||||
平沙日未没
|
||||
平海夕漫漫
|
||||
平生自有分
|
||||
年年移帐雪中天
|
||||
幽人应未眠
|
||||
庄生晓梦迷蝴蝶
|
||||
应傍战场开
|
||||
庭树不知人去尽
|
||||
建业暮钟时
|
||||
开轩面场圃
|
||||
弹琴复长啸
|
||||
强欲登高去
|
||||
归时每日斜
|
||||
归棹洛阳人
|
||||
归雁洛阳边
|
||||
归马识残旗
|
||||
征人倚戍楼
|
||||
待到重阳日
|
||||
徒令上将挥神笔
|
||||
徒劳恨费声
|
||||
得相能开国
|
||||
徘徊乱绕空
|
||||
心忧炭贱愿天寒
|
||||
心断新丰酒
|
||||
快走踏清秋
|
||||
念天地之悠悠
|
||||
念尔独何之
|
||||
忽如一夜春风来
|
||||
怀君属秋夜
|
||||
怅卧新春白袷衣
|
||||
怜渠直道当时语
|
||||
总为浮云能蔽日
|
||||
总是玉关情
|
||||
悠扬绿萝影
|
||||
惆怅南朝事
|
||||
惊秋风水凉
|
||||
惊飞远映碧山去
|
||||
惟将迟暮供多病
|
||||
惟残一人出骆谷
|
||||
惟见长江天际流
|
||||
愁云惨淡万里凝
|
||||
愁坐夜待晨
|
||||
意恐迟迟归
|
||||
愧君相见频
|
||||
愿君多采撷
|
||||
慈母手中线
|
||||
戎马关山北
|
||||
我亦举家清
|
||||
我家襄水曲
|
||||
所得是沾衣
|
||||
手把文书口称敕
|
||||
打起黄莺儿
|
||||
扣门无犬吠
|
||||
把剑觅徐君
|
||||
把酒话桑麻
|
||||
折戟沉沙铁未销
|
||||
报君黄金台上意
|
||||
报得三春晖
|
||||
报道山中去
|
||||
拟托良媒益自伤
|
||||
掩泣空相向
|
||||
提携玉龙为君死
|
||||
故乡杳无际
|
||||
故人具鸡黍
|
||||
故人殊未来
|
||||
故人江海别
|
||||
故人西辞黄鹤楼
|
||||
故关衰草遍
|
||||
故园东望路漫漫
|
||||
故园芜已平
|
||||
故国三千里
|
||||
故垒萧萧芦荻秋
|
||||
敢将十指夸针巧
|
||||
散入珠帘湿罗幕
|
||||
散步咏凉天
|
||||
数问夜如何
|
||||
断雁警愁眠
|
||||
斯人不可闻
|
||||
新妆宜面下朱楼
|
||||
新知遭薄俗
|
||||
旅馆无良伴
|
||||
无人收废帐
|
||||
无人送酒来
|
||||
无将故人酒
|
||||
无情最是台城柳
|
||||
无边落木萧萧下
|
||||
无限风光尽被占
|
||||
日暮东风怨啼鸟
|
||||
日暮乡关何处是
|
||||
日暮客愁新
|
||||
日暮掩柴扉
|
||||
日暮欲何之
|
||||
日暮汉宫传蜡烛
|
||||
日暮秋风起
|
||||
日暮苍山远
|
||||
日暮飞鸟还
|
||||
旧好隔良缘
|
||||
旧时王谢堂前燕
|
||||
早知潮有信
|
||||
早雁忽为双
|
||||
早雁拂金河
|
||||
时时误拂弦
|
||||
时鸣春涧中
|
||||
明日岁华新
|
||||
明月来相照
|
||||
明朝挂帆席
|
||||
明朝有封事
|
||||
昔人已乘黄鹤去
|
||||
昔日经行人尽去
|
||||
昔日长城战
|
||||
昔时人已没
|
||||
昔闻洞庭水
|
||||
星临万户动
|
||||
星垂平野阔
|
||||
星月满空江
|
||||
春城无处不飞花
|
||||
春愁黯黯独成眠
|
||||
春来发几枝
|
||||
春来还发旧时花
|
||||
春潮带雨晚来急
|
||||
春种一粒粟
|
||||
春草明年绿
|
||||
春风吹又生
|
||||
春风对青冢
|
||||
昨别今已春
|
||||
晋代衣冠成古丘
|
||||
晓汲清湘燃楚竹
|
||||
晓驾炭车辗冰辙
|
||||
晚来天欲雪
|
||||
晚见雁行频
|
||||
晴川历历汉阳树
|
||||
暂时分手莫踌躇
|
||||
暗风吹雨入寒窗
|
||||
暮雨相呼失
|
||||
曲折仍圆旋
|
||||
更有明朝恨
|
||||
更深月色半人家
|
||||
曾驱十万师
|
||||
最是一年春好处
|
||||
月傍九霄多
|
||||
月出惊山鸟
|
||||
月殿影开闻夜漏
|
||||
月涌大江流
|
||||
月落乌啼霜满天
|
||||
有城遮不得
|
||||
望帝春心托杜鹃
|
||||
朝朝误妾期
|
||||
朝朝马策与刀环
|
||||
朝来逢着旧将军
|
||||
朝辞白帝彩云间
|
||||
木落雁南度
|
||||
未必逢矰缴
|
||||
未有涓埃答圣朝
|
||||
未谙姑食性
|
||||
本以高难饱
|
||||
朱雀桥边野草花
|
||||
杜甫天才颇绝伦
|
||||
来舞魏宫前
|
||||
杨柳青青江水平
|
||||
杯粘落花片
|
||||
杳杳种声晚
|
||||
松月夜窗虚
|
||||
板桥人渡泉声
|
||||
极目箫条三两家
|
||||
林暗草惊风
|
||||
林表明霁色
|
||||
枫叶落纷纷
|
||||
柴门闻犬吠
|
||||
树色随关迥
|
||||
桂树何团团
|
||||
梁园日暮乱飞鸦
|
||||
梁父吟成恨有余
|
||||
楚塞三湘接
|
||||
楚江微雨里
|
||||
楚腰纤细掌中轻
|
||||
樽酒家贫只旧醅
|
||||
檐头索索缲车鸣
|
||||
欲去问西家
|
||||
欲得周郎顾
|
||||
欲把一麾江海去
|
||||
欲祭疑君在
|
||||
欸乃一声山水绿
|
||||
正是江南好风景
|
||||
此地别燕丹
|
||||
此地空余黄鹤楼
|
||||
此夕闻君谪九江
|
||||
此情可待成追忆
|
||||
此曲只应天上有
|
||||
此物最相思
|
||||
死生长别离
|
||||
残云归太华
|
||||
残宵犹得梦依稀
|
||||
残月下章台
|
||||
残灯无焰影幢幢
|
||||
残萤西玉露
|
||||
残钟广陵树
|
||||
殿前兵马虽骁雄
|
||||
每寻诗卷似情亲
|
||||
每逢佳节倍思亲
|
||||
水寒风似刀
|
||||
水村山郭酒旗风
|
||||
水精帘卷近秋河
|
||||
永怀愁不寐
|
||||
汗滴禾下土
|
||||
江上几人在
|
||||
江春入旧年
|
||||
江村月落正堪眠
|
||||
江枫渔火对愁眠
|
||||
江流天地外
|
||||
江流石不转
|
||||
江清月近人
|
||||
江皋闻曙钟
|
||||
江雨霏霏江草齐
|
||||
沅湘流不尽
|
||||
没在石棱中
|
||||
沧江好烟月
|
||||
沧海月明珠有泪
|
||||
河声入海遥
|
||||
沾襟比散丝
|
||||
沿洄安得住
|
||||
泛泛入烟雾
|
||||
波澜动远空
|
||||
洗手作羹汤
|
||||
洛阳亲友如相问
|
||||
流水无情草自春
|
||||
流落征南将
|
||||
浦树远含滋
|
||||
浩浩风起波
|
||||
浩然离故关
|
||||
浪淘风簸自天涯
|
||||
浮萍一道开
|
||||
海内风尘诸弟隔
|
||||
海日生残夜
|
||||
海门深不见
|
||||
淮南一叶下
|
||||
淮水东边旧时月
|
||||
深坐颦蛾眉
|
||||
深宫二十年
|
||||
深林人不知
|
||||
深竹暗浮烟
|
||||
深锁春光一院愁
|
||||
清明时节雨纷纷
|
||||
清明有味是无能
|
||||
清瑟怨遥夜
|
||||
渐与骨肉远
|
||||
渔翁夜傍西岩宿
|
||||
渚云低暗度
|
||||
渚清沙白鸟飞回
|
||||
渭城朝雨浥轻尘
|
||||
游子身上衣
|
||||
湖光秋月两相和
|
||||
湛湛碧涟漪
|
||||
溪花与禅意
|
||||
满面尘灰烟火色
|
||||
漠漠帆来重
|
||||
潦倒新停浊酒杯
|
||||
潭面无风镜未磨
|
||||
潮平两岸阔
|
||||
潮打空城寂寞回
|
||||
潮落夜江斜月里
|
||||
瀚海阑干百丈冰
|
||||
灞原风雨定
|
||||
灯下白头人
|
||||
烟波江上使人愁
|
||||
烟笼寒水月笼沙
|
||||
烟花三月下扬州
|
||||
烟销日出不见人
|
||||
烦君最相警
|
||||
燕山月似钩
|
||||
牛困人饥日已高
|
||||
牛渚西江夜
|
||||
牛马千群逐暖川
|
||||
牧童遥指杏花村
|
||||
犹似造书时
|
||||
犹埋云外峰
|
||||
犹是春闺梦里人
|
||||
犹自梦渔樵
|
||||
犹见孤帆一日程
|
||||
狐裘不暖锦衾薄
|
||||
独在异乡为异客
|
||||
独坐幽篁里
|
||||
独夜忆秦关
|
||||
独怆然而涕下
|
||||
独怜幽草涧边生
|
||||
独立三边静
|
||||
独钓寒江雪
|
||||
猿鸟犹疑畏简书
|
||||
玉人何处教吹箫
|
||||
玉垒浮云变古今
|
||||
玉楼天半起笙歌
|
||||
玉铛缄札何由达
|
||||
王孙归不归
|
||||
王浚楼船下益州
|
||||
珠箔飘灯独自归
|
||||
甘棠一无树
|
||||
生儿不象贤
|
||||
田家衣食无厚薄
|
||||
甲光向日金鳞开
|
||||
男声欣欣女颜悦
|
||||
留连戏蝶时时舞
|
||||
留醉与山翁
|
||||
疏雨过中条
|
||||
疑是经冬雪未销
|
||||
疲马卧长坂
|
||||
登舟望秋月
|
||||
白云依静渚
|
||||
白云千载空悠悠
|
||||
白云生处有人家
|
||||
白兔捣药成
|
||||
白发三千丈
|
||||
白发催年老
|
||||
白头宫女在
|
||||
白帝城边古木疏
|
||||
白日落梁州
|
||||
白银盘里一青螺
|
||||
白门寥落意多违
|
||||
白骨乱蓬蒿
|
||||
百年多病独登台
|
||||
的知输得官家足
|
||||
皆共尘沙老
|
||||
盘飧市远无兼味
|
||||
相伴有田父
|
||||
相对亦忘言
|
||||
相悲各问年
|
||||
相欢惟牧童
|
||||
相留畏晓钟
|
||||
相送临高台
|
||||
相送情无限
|
||||
眼穿仍仍欲归
|
||||
矜夸紫骝好
|
||||
知有前期在
|
||||
碛里征人三十万
|
||||
碧天如水夜云轻
|
||||
碧海青天夜夜心
|
||||
碧玉妆成一树高
|
||||
离别正堪悲
|
||||
离披草书字
|
||||
离杯惜共传
|
||||
离离原上草
|
||||
秋入望乡心
|
||||
秋尽江南草木凋
|
||||
秋收万颗子
|
||||
秋来未著花
|
||||
秋雁又南回
|
||||
秋风吹不尽
|
||||
秋风落叶满空山
|
||||
秦时明月汉时关
|
||||
积雪浮云端
|
||||
移家虽带郭
|
||||
移舟泊烟渚
|
||||
税重多贫户
|
||||
穷边有客游
|
||||
空园白露滴
|
||||
空山不见人
|
||||
空山无鸟迹
|
||||
空山松子落
|
||||
空忆谢将军
|
||||
空阶有鸟迹
|
||||
窗含西岭千秋雪
|
||||
竹柏皆冻死
|
||||
筑城须努力
|
||||
管乐有才真不忝
|
||||
粒粒皆辛苦
|
||||
精灵自来去
|
||||
系向牛头充炭值
|
||||
素手玉房前
|
||||
繁华事散逐香尘
|
||||
红叶晚萧萧
|
||||
红楼隔雨相望冷
|
||||
红泥小火炉
|
||||
红豆生南国
|
||||
纵暴略与羌浑同
|
||||
纵然一夜风吹去
|
||||
纷纷暮雪下辕门
|
||||
细草微风岸
|
||||
终南阴岭秀
|
||||
终见降王走传车
|
||||
经冬复立春
|
||||
绕弦风雨哀
|
||||
绝胜烟柳满皇都
|
||||
绿树村边合
|
||||
绿蚁新醅酒
|
||||
缘愁似个长
|
||||
罢归无旧业
|
||||
羁危万里身
|
||||
羁旅长堪醉
|
||||
羁泊欲穷年
|
||||
美人卷珠帘
|
||||
美人相并立琼轩
|
||||
群捕鱼儿溪影中
|
||||
群盗相随剧虎狼
|
||||
翩翩两骑来是谁
|
||||
翻疑梦里逢
|
||||
老农家贫在山住
|
||||
老去恋明时
|
||||
老松欹侧卧
|
||||
老病有孤舟
|
||||
耆老遮归路
|
||||
耕彼故城东
|
||||
耕种山田三四亩
|
||||
肠断未忍扫
|
||||
肯与邻翁相对饮
|
||||
胡天八月即飞雪
|
||||
胡琴琵琶与羌笛
|
||||
胡风冻合鸊鹈泉
|
||||
能饮一杯无
|
||||
自在娇莺恰恰啼
|
||||
自将磨洗认前朝
|
||||
自觉洞庭波
|
||||
自说二女啮臂时
|
||||
自说夫因征战死
|
||||
至今窥牧马
|
||||
舍南舍北皆春水
|
||||
舟人夜语觉潮生
|
||||
船中养犬长食肉
|
||||
良人罢远征
|
||||
艰难苦恨繁霜鬓
|
||||
花径不曾缘客扫
|
||||
花近高楼伤客心
|
||||
花隐掖垣暮
|
||||
芳心向春尽
|
||||
芳草已云暮
|
||||
芳草萋萋鹦鹉洲
|
||||
芳草闭闲门
|
||||
苍苍竹林寺
|
||||
苗疏税多不得食
|
||||
苜蓿峰边逢立春
|
||||
苦恨年年压金线
|
||||
苦战应憔悴
|
||||
茅檐日午鸡鸣
|
||||
茫茫江汉上
|
||||
荆歌艳楚腰
|
||||
荆门九派通
|
||||
草没城边路
|
||||
草色遥看近却无
|
||||
荒居旧业贫
|
||||
荒戍落黄叶
|
||||
荡胸生层云
|
||||
荷笠带斜阳
|
||||
莓苔见屐痕
|
||||
莫嗔焙茶烟暗
|
||||
莫学游侠儿
|
||||
莫愁前路无知己
|
||||
莫教枝上啼
|
||||
莺啼送客闻
|
||||
萧萧枫树林
|
||||
落叶他乡树
|
||||
落帆逗淮镇
|
||||
落日清江里
|
||||
落花时节又逢君
|
||||
落花犹似坠楼人
|
||||
落魄江湖载酒行
|
||||
葫芦河上泪沾巾
|
||||
蓝田日暖玉生烟
|
||||
蓬门今始为君开
|
||||
蓬门未识绮罗香
|
||||
蕃情似此水
|
||||
蕃汉断消息
|
||||
薄宦梗犹泛
|
||||
虫声新透绿窗纱
|
||||
蜻蜓飞上玉搔头
|
||||
蝉鸣空桑林
|
||||
行人去不息
|
||||
行到中庭数花朵
|
||||
行舟绿水前
|
||||
衡阳归雁几封书
|
||||
衣上灞陵雨
|
||||
襄阳好风日
|
||||
西出阳关无故人
|
||||
西山寇盗莫相侵
|
||||
西山白雪三城戍
|
||||
西楼望月几回圆
|
||||
西江贾客珠百斛
|
||||
西风生翠萝
|
||||
角声满天秋色里
|
||||
誓扫匈奴不顾身
|
||||
谁爱风流高格调
|
||||
谁知盘中餐
|
||||
谁言寸草心
|
||||
调角断清秋
|
||||
赢得青楼薄幸名
|
||||
走马西来欲到天
|
||||
跨马出郊时极目
|
||||
路上行人欲断魂
|
||||
路出寒云外
|
||||
身上衣裳口中食
|
||||
身多疾病思田里
|
||||
转于童仆亲
|
||||
轮台东门送君去
|
||||
轻烟散入五侯家
|
||||
轻生一剑知
|
||||
轻罗小扇扑流萤
|
||||
轻舟已过万重山
|
||||
输入官仓化为土
|
||||
辞家见月两回圆
|
||||
过雨看松色
|
||||
近乡情更怯
|
||||
近泪无干土
|
||||
近种篱边菊
|
||||
返影入深林
|
||||
还作江南会
|
||||
还来就菊花
|
||||
远山晴更多
|
||||
远梦归侵晓
|
||||
远目寒山石径斜
|
||||
远路应悲春晼晚
|
||||
迢递三巴路
|
||||
迢递送斜晖
|
||||
迥临村路傍溪桥
|
||||
迷津欲有问
|
||||
造化钟神秀
|
||||
遍插茱萸少一人
|
||||
道是无晴还有晴
|
||||
遗恨失吞吴
|
||||
遥夜泛清瑟
|
||||
遥怜故园菊
|
||||
遥望洞庭山水翠
|
||||
遥知兄弟登高处
|
||||
遥隔楚云端
|
||||
邀我至田家
|
||||
邑有流亡愧俸钱
|
||||
那堪正漂泊
|
||||
那得泪潸然
|
||||
郡邑浮前浦
|
||||
都护铁衣冷难著
|
||||
采山因买斧
|
||||
采得百花成蜜后
|
||||
采莲从小惯
|
||||
野哭几家闻战伐
|
||||
野客漱流时
|
||||
野寺来人少
|
||||
野寺荒台晚
|
||||
野径入桑麻
|
||||
野旷天低树
|
||||
野渡无人舟自横
|
||||
野火烧不尽
|
||||
野蚕作茧人不取
|
||||
金陵津渡小山楼
|
||||
金陵王气黯然收
|
||||
钓罢归来不系船
|
||||
铜雀春深锁二乔
|
||||
银烛秋光冷画屏
|
||||
销愁又几千
|
||||
锄禾日当午
|
||||
锦城丝管日纷纷
|
||||
锦江春色来天地
|
||||
锦瑟无端五十弦
|
||||
长亭酒一瓢
|
||||
长安一片月
|
||||
长安不见使人愁
|
||||
长恨春归无觅处
|
||||
长愿向南流
|
||||
长江独至今
|
||||
长河渐落晓星沉
|
||||
门泊东吴万里船
|
||||
门系钓鱼船
|
||||
问客何为来
|
||||
问言与谁餐
|
||||
闲坐说玄宗
|
||||
闲爱孤云静爱僧
|
||||
闺中只是空相忆
|
||||
闺阁别离难
|
||||
闻道杀人汉水上
|
||||
闻道欲来相问讯
|
||||
闻郎江上唱歌声
|
||||
阴阳割昏晓
|
||||
随山到水源
|
||||
隔江犹唱后庭花
|
||||
隔篱呼取尽馀杯
|
||||
难分此夜中
|
||||
雁下芦洲白
|
||||
雁声远过潇湘去
|
||||
雨中黄叶树
|
||||
雪上空留马行处
|
||||
雪衣雪发青玉嘴
|
||||
霜叶红于二月花
|
||||
霜重鼓寒声不起
|
||||
露似珍珠月似弓
|
||||
露草泣寒虫
|
||||
青天无片云
|
||||
青山独归远
|
||||
青山郭外斜
|
||||
青山隐隐水迢迢
|
||||
青枫江上秋帆远
|
||||
青楼自管弦
|
||||
青海戍头空有月
|
||||
青阳逼岁除
|
||||
静夜四无邻
|
||||
风云长为护储胥
|
||||
风吹城上树
|
||||
风尘何所期
|
||||
风急天高猿啸哀
|
||||
风掣红旗冻不翻
|
||||
风枝惊暗鹊
|
||||
风正一帆悬
|
||||
风送宫嫔笑语和
|
||||
风雪夜归人
|
||||
飏飏燕新乳
|
||||
飒飒如有人
|
||||
飘飘何所似
|
||||
飞入寻常百姓家
|
||||
飞在青云端
|
||||
飞雪带春风
|
||||
食人更肯留妻子
|
||||
饮马渡秋水
|
||||
马上相逢无纸笔
|
||||
驱车登古原
|
||||
驻马别孤坟
|
||||
驻马衔杯问谪居
|
||||
高树晓还密
|
||||
高阁客竟去
|
||||
高风汉阳渡
|
||||
鬓丝生几缕
|
||||
鸣筝金粟柱
|
||||
鹦鹉前头不敢言
|
||||
麦收上场绢在轴
|
||||
黄叶仍风雨
|
||||
黄四娘家花满蹊
|
||||
黄尘足今古
|
||||
黄沙碛里本无春
|
||||
黄衣使者白衫儿
|
||||
黄鹤一去不复返
|
||||
黑云压城城欲催
|
||||
黯黯见临洮
|
||||
齐鲁青未了
|
||||
@@ -9,17 +9,19 @@ import random
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Mapping
|
||||
from datetime import UTC, datetime
|
||||
from http.cookies import SimpleCookie
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from urllib.parse import parse_qs, quote, urlparse
|
||||
|
||||
import re
|
||||
import cv2
|
||||
import execjs
|
||||
import numpy as np
|
||||
import requests
|
||||
from loguru import logger
|
||||
from PIL import Image
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from .capture import CaptchaCaptureRecord
|
||||
from .ocr import HuyaCaptchaOcr, default_ocr
|
||||
@@ -69,6 +71,8 @@ def _cookie_mapping(cookie: Mapping[str, str] | str | None) -> dict[str, str]:
|
||||
class HuyaVerificationSolver:
|
||||
"""处理虎牙 returnCode=10030/10039 后的滑块或点选验证。"""
|
||||
|
||||
_cloze_corpus: set[str] | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cookie: Mapping[str, str] | str | None = None,
|
||||
@@ -84,6 +88,7 @@ class HuyaVerificationSolver:
|
||||
account: str = "",
|
||||
capture_dir: str | Path | None = None,
|
||||
enable_capture: bool = True,
|
||||
sms_url: str = "",
|
||||
):
|
||||
self.cookie = _cookie_mapping(cookie)
|
||||
self.ua = ua or DEFAULT_UA
|
||||
@@ -113,6 +118,7 @@ class HuyaVerificationSolver:
|
||||
self.account = account
|
||||
self.capture_dir = capture_dir
|
||||
self.enable_capture = enable_capture
|
||||
self.sms_url = sms_url
|
||||
self.current_capture: CaptchaCaptureRecord | None = None
|
||||
self.auth_type = ""
|
||||
|
||||
@@ -135,7 +141,7 @@ class HuyaVerificationSolver:
|
||||
@staticmethod
|
||||
def _decode_image(base64_string: str | None) -> bytes:
|
||||
if not base64_string:
|
||||
raise HuyaVerificationError("验证码图片为空")
|
||||
return b""
|
||||
if ";base64," in base64_string:
|
||||
base64_string = base64_string.split(";base64,")[-1]
|
||||
return base64.b64decode(base64_string)
|
||||
@@ -210,6 +216,162 @@ class HuyaVerificationSolver:
|
||||
_, _, _, max_loc = cv2.minMaxLoc(result)
|
||||
return int(max_loc[0])
|
||||
|
||||
@staticmethod
|
||||
def _opencv_rotate_angle(
|
||||
background_bytes: bytes,
|
||||
puzzle_bytes: bytes,
|
||||
default_cx: float = 195.0,
|
||||
default_cy: float = 90.0,
|
||||
) -> int:
|
||||
"""通过圆周边界颜色与边缘连续性计算旋转验证码的最佳旋转角度 (0~359度)。"""
|
||||
bg = cv2.imdecode(np.frombuffer(background_bytes, np.uint8), cv2.IMREAD_COLOR)
|
||||
puzzle_raw = cv2.imdecode(
|
||||
np.frombuffer(puzzle_bytes, np.uint8), cv2.IMREAD_UNCHANGED
|
||||
)
|
||||
if bg is None or puzzle_raw is None:
|
||||
raise HuyaVerificationError("旋转验证码图片解码失败")
|
||||
|
||||
bg_gray = cv2.cvtColor(bg, cv2.COLOR_BGR2GRAY).astype(np.float32)
|
||||
p_gray = cv2.cvtColor(puzzle_raw[:, :, :3], cv2.COLOR_BGR2GRAY).astype(
|
||||
np.float32
|
||||
)
|
||||
|
||||
gray_u8 = cv2.cvtColor(bg, cv2.COLOR_BGR2GRAY)
|
||||
black_mask = (gray_u8 < 25).astype(np.uint8) * 255
|
||||
contours, _ = cv2.findContours(
|
||||
black_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
||||
)
|
||||
cx, cy, r_hole = default_cx, default_cy, 58.0
|
||||
for c in contours:
|
||||
if cv2.contourArea(c) > 3000:
|
||||
(tcx, tcy), tr = cv2.minEnclosingCircle(c)
|
||||
if abs(tcx - default_cx) < 35 and abs(tcy - default_cy) < 25:
|
||||
cx, cy, r_hole = tcx, tcy, tr
|
||||
break
|
||||
|
||||
ph, pw = puzzle_raw.shape[:2]
|
||||
pcx, pcy = pw / 2.0, ph / 2.0
|
||||
|
||||
angles = np.linspace(0, 2 * np.pi, 360, endpoint=False)
|
||||
r_out_list = [r_hole + 2.0, r_hole + 4.0]
|
||||
r_in_list = [r_hole - 5.0, r_hole - 7.0]
|
||||
|
||||
bg_outer_vals_list = []
|
||||
for ro in r_out_list:
|
||||
bx = cx + ro * np.cos(angles)
|
||||
by = cy + ro * np.sin(angles)
|
||||
vals = cv2.remap(
|
||||
bg_gray,
|
||||
bx.astype(np.float32),
|
||||
by.astype(np.float32),
|
||||
cv2.INTER_LINEAR,
|
||||
).flatten()
|
||||
bg_outer_vals_list.append(vals)
|
||||
bg_outer_vals = np.mean(bg_outer_vals_list, axis=0)
|
||||
|
||||
best_deg = 0
|
||||
best_corr = -1.0
|
||||
for deg in range(360):
|
||||
rad = np.radians(deg)
|
||||
p_inner_vals_list = []
|
||||
for ri in r_in_list:
|
||||
px = pcx + ri * np.cos(angles - rad)
|
||||
py = pcy + ri * np.sin(angles - rad)
|
||||
pvals = cv2.remap(
|
||||
p_gray,
|
||||
px.astype(np.float32),
|
||||
py.astype(np.float32),
|
||||
cv2.INTER_LINEAR,
|
||||
).flatten()
|
||||
p_inner_vals_list.append(pvals)
|
||||
p_inner_vals = np.mean(p_inner_vals_list, axis=0)
|
||||
|
||||
corr = np.corrcoef(bg_outer_vals, p_inner_vals)[0, 1]
|
||||
if corr > best_corr:
|
||||
best_corr = corr
|
||||
best_deg = deg
|
||||
|
||||
return best_deg
|
||||
|
||||
@classmethod
|
||||
def _get_cloze_corpus(cls) -> set[str]:
|
||||
if cls._cloze_corpus is None:
|
||||
corpus: set[str] = set()
|
||||
base_dir = Path(__file__).resolve().parent
|
||||
for filename in ("idioms.txt", "poems.txt"):
|
||||
file_path = base_dir / filename
|
||||
if file_path.is_file():
|
||||
try:
|
||||
corpus.update(
|
||||
file_path.read_text(encoding="utf-8").splitlines()
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug(f"[solver] 加载语料 {filename} 失败: {exc}")
|
||||
cls._cloze_corpus = corpus
|
||||
return cls._cloze_corpus
|
||||
|
||||
@classmethod
|
||||
def _find_chinese_font(
|
||||
cls, size: int = 30
|
||||
) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
||||
font_candidates = [
|
||||
"/System/Library/Fonts/PingFang.ttc",
|
||||
"/System/Library/Fonts/STHeiti Medium.ttc",
|
||||
"/System/Library/Fonts/STHeiti Light.ttc",
|
||||
"/System/Library/Fonts/Hiragino Sans GB.ttc",
|
||||
"/System/Library/Fonts/Supplemental/Songti.ttc",
|
||||
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
|
||||
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
|
||||
"C:/Windows/Fonts/msyh.ttc",
|
||||
"C:/Windows/Fonts/simhei.ttf",
|
||||
]
|
||||
for font_path in font_candidates:
|
||||
if Path(font_path).is_file():
|
||||
try:
|
||||
return ImageFont.truetype(font_path, size)
|
||||
except Exception:
|
||||
continue
|
||||
return ImageFont.load_default()
|
||||
|
||||
@classmethod
|
||||
def _synthesize_text_tip(cls, chars: list[str]) -> bytes:
|
||||
"""为没有提示小图的文本点选题目动态合成 44px 高度的提示图像。"""
|
||||
if not chars:
|
||||
return b""
|
||||
font = cls._find_chinese_font(30)
|
||||
char_box_size = 44
|
||||
total_width = len(chars) * char_box_size
|
||||
image = Image.new("RGB", (total_width, char_box_size), (255, 255, 255))
|
||||
draw = ImageDraw.Draw(image)
|
||||
for i, ch in enumerate(chars):
|
||||
offset_x = i * char_box_size + 7
|
||||
offset_y = 5
|
||||
draw.text((offset_x, offset_y), ch, fill=(0, 0, 0), font=font)
|
||||
buf = io.BytesIO()
|
||||
image.save(buf, format="PNG")
|
||||
return buf.getvalue()
|
||||
|
||||
@classmethod
|
||||
def _extract_target_chars_from_question(cls, question: str) -> list[str]:
|
||||
"""从点选题目中提取需要点击的目标汉字(支持完整成语/菜名及古诗/成语填空题)。"""
|
||||
if not question:
|
||||
return []
|
||||
m = re.search(r'["“]([^"”]+)["”]', question)
|
||||
if not m:
|
||||
return []
|
||||
text = m.group(1).strip()
|
||||
if "_" in text:
|
||||
regex = "^" + text.replace("_", "(.)") + "$"
|
||||
compiled = re.compile(regex)
|
||||
for phrase in cls._get_cloze_corpus():
|
||||
matched = compiled.match(phrase)
|
||||
if matched:
|
||||
return list(matched.groups())
|
||||
return []
|
||||
return [ch for ch in text if "\u4e00" <= ch <= "\u9fa5"]
|
||||
|
||||
def _request_json(self, method: str, url: str, **kwargs) -> dict:
|
||||
response = self.session.request(method, url, timeout=self.timeout, **kwargs)
|
||||
response.raise_for_status()
|
||||
@@ -367,6 +529,13 @@ class HuyaVerificationSolver:
|
||||
step=step,
|
||||
raw_params=self.url_param_map,
|
||||
)
|
||||
if self.sms_url:
|
||||
logger.info(
|
||||
"[solver] 虎牙风控在第 {} 步要求短信验证 (sms_auth),已配置接码链接,准备自动发码并接码过验",
|
||||
step,
|
||||
)
|
||||
self.auth_type = "sms_auth"
|
||||
return
|
||||
logger.warning(
|
||||
"[solver] 虎牙风控在第 {} 步要求短信验证 (sms_auth),手机号: {}",
|
||||
step,
|
||||
@@ -377,7 +546,12 @@ class HuyaVerificationSolver:
|
||||
f"虎牙风控要求短信验证(sms_auth),手机号: {mobile}",
|
||||
mobile=mobile,
|
||||
)
|
||||
self.auth_type = "dx_auth" if "dx_auth" in strategy_url_lower else "pt_auth"
|
||||
if "rotate_auth" in strategy_url_lower or "/auth/xz/" in strategy_url_lower:
|
||||
self.auth_type = "rotate_auth"
|
||||
elif "dx_auth" in strategy_url_lower or "/auth/dx/" in strategy_url_lower:
|
||||
self.auth_type = "dx_auth"
|
||||
else:
|
||||
self.auth_type = "pt_auth"
|
||||
if self.enable_capture:
|
||||
self.current_capture = CaptchaCaptureRecord(
|
||||
auth_type=self.auth_type,
|
||||
@@ -412,9 +586,39 @@ class HuyaVerificationSolver:
|
||||
self.code = data["code"]
|
||||
self.huyapk = data["huyapk"]
|
||||
segment = data.get("segment")
|
||||
is_rotate = (
|
||||
self.auth_type == "rotate_auth"
|
||||
or "/auth/xz/" in url
|
||||
or "rotate" in self.strategy_url
|
||||
)
|
||||
if is_rotate:
|
||||
logger.info("虎牙验证类型: 旋转验证码 (rotate_auth / xz)")
|
||||
bg_bytes = self._decode_image(data.get("dtuStr") or data.get("dTuStr"))
|
||||
tip_bytes = self._decode_image(data.get("puzzleStr"))
|
||||
restored_bg = (
|
||||
self._restore_slider_background(bg_bytes, segment, self.huyapk)
|
||||
if segment
|
||||
else bg_bytes
|
||||
)
|
||||
angle = self._opencv_rotate_angle(restored_bg, tip_bytes)
|
||||
logger.info(f"虎牙旋转验证码识别角度: {angle}°")
|
||||
if self.current_capture:
|
||||
try:
|
||||
self.current_capture.capture_rotate_data(
|
||||
code=self.code,
|
||||
dtu_bytes=bg_bytes,
|
||||
puzzle_bytes=tip_bytes,
|
||||
restored_bg_bytes=restored_bg,
|
||||
angle=angle,
|
||||
raw_data=data,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug(f"[solver] 记录旋转验证码捕获数据异常: {exc}")
|
||||
return angle
|
||||
|
||||
if segment:
|
||||
logger.info("虎牙验证类型: 滑块")
|
||||
bg_bytes = self._decode_image(data.get("dtuStr"))
|
||||
bg_bytes = self._decode_image(data.get("dtuStr") or data.get("dTuStr"))
|
||||
tip_bytes = self._decode_image(data.get("puzzleStr"))
|
||||
restored_bg = self._restore_slider_background(
|
||||
bg_bytes, segment, self.huyapk
|
||||
@@ -451,11 +655,31 @@ class HuyaVerificationSolver:
|
||||
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)
|
||||
final_points = [[int(x), int(y) - 44] for x, y in points]
|
||||
bg_raw = data.get("dTuStr") or data.get("dtuStr") or data.get("bgStr")
|
||||
bg_bytes = self._decode_image(bg_raw)
|
||||
if not bg_bytes:
|
||||
raise HuyaVerificationError(
|
||||
f"点选验证码背景图为空: keys={list(data.keys())}"
|
||||
)
|
||||
tip_raw = data.get("hZiStr") or data.get("hziStr") or data.get("puzzleStr")
|
||||
tip_bytes = self._decode_image(tip_raw) if tip_raw else b""
|
||||
if tip_bytes:
|
||||
merged = self._vertical_concat_align_left(bg_bytes, tip_bytes)
|
||||
points = self.ocr.ocr_click_points(merged)
|
||||
final_points = [[int(x), int(y) - 44] for x, y in points]
|
||||
else:
|
||||
question = str(data.get("question") or "")
|
||||
target_chars = self._extract_target_chars_from_question(question)
|
||||
if target_chars:
|
||||
logger.info(f"虎牙点选从题目解析出目标字: {target_chars}")
|
||||
synth_tip = self._synthesize_text_tip(target_chars)
|
||||
merged = self._vertical_concat_align_left(bg_bytes, synth_tip)
|
||||
points = self.ocr.ocr_click_points(merged)
|
||||
final_points = [[int(x), int(y) - 44] for x, y in points]
|
||||
tip_bytes = synth_tip
|
||||
else:
|
||||
logger.info(f"虎牙点选题目未能提取明确目标字: {question}")
|
||||
final_points = []
|
||||
|
||||
if self.current_capture:
|
||||
try:
|
||||
@@ -471,6 +695,34 @@ class HuyaVerificationSolver:
|
||||
|
||||
return final_points
|
||||
|
||||
def _send_rotate_verify(self, url: str, angle: float) -> dict:
|
||||
drag_distance = max(5, int((angle / 360.0) * 260))
|
||||
track_points = generate_slide_track(drag_distance)
|
||||
act_data = {
|
||||
"point": int(round(angle)),
|
||||
"travel": format_track(track_points),
|
||||
"code": self.code,
|
||||
"sceneType": self.scene_type,
|
||||
"endTime": int(track_points[-1][-1]) - int(track_points[0][-1]),
|
||||
}
|
||||
act = self.js_ctx.call("get_act", act_data, self.huyapk)
|
||||
self.behavior_events.append(
|
||||
{
|
||||
"action": self._pointer_action("mousedown"),
|
||||
"id": "",
|
||||
"txt": "",
|
||||
"clientX": track_points[0][0],
|
||||
"clientY": track_points[0][1],
|
||||
"clientWidth": 350,
|
||||
"clientHeight": 268,
|
||||
"isTrusted": True,
|
||||
"timeStamp": random.randint(1800, 5500),
|
||||
"nowTime": int(time.time() * 1000),
|
||||
}
|
||||
)
|
||||
payload = self._build_common_payload({"act": act, "ver": 5})
|
||||
return self._submit_verify(url, payload)
|
||||
|
||||
def _send_slider_verify(self, url: str, point: int) -> dict:
|
||||
track_points = generate_slide_track(point)
|
||||
act_data = {
|
||||
@@ -559,8 +811,81 @@ class HuyaVerificationSolver:
|
||||
logger.debug(f"[solver] 记录提交结果异常: {exc}")
|
||||
return data
|
||||
|
||||
def _solve_sms_strategy(self, wait_seconds: float = 90.0) -> dict:
|
||||
"""自动通过 sms_url 接码并提交虎牙 sms_auth 验证。"""
|
||||
from core.sms_provider import SmsLine, SmsProviderClient, detect_provider
|
||||
|
||||
mobile = self.url_param_map.get("mobile", "?")
|
||||
logger.info(
|
||||
f"[solver] 开始自动接码流程 (手机: {mobile}, 接码URL: {self.sms_url[:40]}...)"
|
||||
)
|
||||
|
||||
# 1. 触发虎牙官方发送短信验证码
|
||||
send_url = "https://udbrtt.huya.com/auth/sms/send-sms-code"
|
||||
send_payload = {
|
||||
"data": {
|
||||
"urlParamMap": self.url_param_map,
|
||||
"ver": 5,
|
||||
}
|
||||
}
|
||||
sent_at = datetime.now(UTC)
|
||||
try:
|
||||
body = json.dumps(send_payload, separators=(",", ":"))
|
||||
send_res = self._request_json("post", send_url, data=body)
|
||||
logger.info(f"[solver] 虎牙短信验证码发送结果: {send_res}")
|
||||
except Exception as exc:
|
||||
logger.warning(f"[solver] 虎牙安全验证发送短信失败: {exc}")
|
||||
raise HuyaVerificationError(f"发送虎牙短信验证码失败: {exc}") from exc
|
||||
|
||||
# 2. 轮询接码链接获取验证码
|
||||
client = SmsProviderClient(timeout=(8, 15))
|
||||
item = SmsLine(
|
||||
phone=mobile,
|
||||
url=self.sms_url,
|
||||
provider=detect_provider(self.sms_url),
|
||||
raw=f"{mobile}----{self.sms_url}",
|
||||
)
|
||||
deadline = time.monotonic() + wait_seconds
|
||||
poll_interval = 2.5
|
||||
sms_code = ""
|
||||
while time.monotonic() < deadline:
|
||||
poll_result = client.poll(item, after_time=sent_at)
|
||||
if poll_result.has_code:
|
||||
sms_code = poll_result.code
|
||||
logger.info(f"[solver] 成功从接码链接收到验证码: {sms_code}")
|
||||
break
|
||||
time.sleep(poll_interval)
|
||||
|
||||
if not sms_code:
|
||||
raise HuyaSmsAuthRequiredError(
|
||||
self.strategy_url,
|
||||
f"接码超时({int(wait_seconds)}秒内未从接码链接获取到新验证码)",
|
||||
mobile=mobile,
|
||||
)
|
||||
|
||||
# 3. 提交验证码给虎牙官方
|
||||
verify_url = "https://udbrtt.huya.com/auth/sms/verify-sms-code"
|
||||
sms_code_hex = sms_code.encode("utf-8").hex()
|
||||
verify_payload = {
|
||||
"encrypt": 1,
|
||||
"data": {
|
||||
"urlParamMap": self.url_param_map,
|
||||
"behavior": self._encode_behavior(),
|
||||
"info": self.sdid,
|
||||
"page": self.page_url,
|
||||
"smsCode": sms_code_hex,
|
||||
"wupData": "",
|
||||
"ver": 5,
|
||||
},
|
||||
}
|
||||
logger.debug(f"[solver] 提交虎牙短信验证码: {sms_code} (hex={sms_code_hex})")
|
||||
return self._submit_verify(verify_url, verify_payload)
|
||||
|
||||
def _solve_current_strategy(self) -> dict:
|
||||
"""提交当前 strategy_url 对应的验证。"""
|
||||
if self.auth_type == "sms_auth":
|
||||
return self._solve_sms_strategy()
|
||||
|
||||
config_payload = self._build_common_payload({"cTuIndex": 0})
|
||||
config = {}
|
||||
gurl = ""
|
||||
@@ -628,7 +953,13 @@ class HuyaVerificationSolver:
|
||||
|
||||
point = self._get_track_point(gurl, point_payload)
|
||||
logger.debug(f"虎牙验证识别结果: {point}")
|
||||
if isinstance(point, list):
|
||||
if (
|
||||
self.auth_type == "rotate_auth"
|
||||
or "/auth/xz/" in vurl
|
||||
or "rotate" in self.strategy_url
|
||||
):
|
||||
last_result = self._send_rotate_verify(vurl, float(point))
|
||||
elif isinstance(point, list):
|
||||
last_result = self._send_click_verify(vurl, point)
|
||||
else:
|
||||
last_result = self._send_slider_verify(vurl, int(point))
|
||||
@@ -682,6 +1013,7 @@ def solve_huya_verification(
|
||||
account: str = "",
|
||||
capture_dir: str | Path | None = None,
|
||||
enable_capture: bool = True,
|
||||
sms_url: str = "",
|
||||
) -> dict:
|
||||
"""函数式入口,便于登录流程直接调用。"""
|
||||
solver = HuyaVerificationSolver(
|
||||
@@ -696,5 +1028,6 @@ def solve_huya_verification(
|
||||
account=account,
|
||||
capture_dir=capture_dir,
|
||||
enable_capture=enable_capture,
|
||||
sms_url=sms_url,
|
||||
)
|
||||
return solver.solve(challenge_payload)
|
||||
|
||||
Reference in New Issue
Block a user