完善虎牙自动注册改密

This commit is contained in:
yml2213
2026-07-06 01:20:22 +08:00
parent af98ea8416
commit db4ca78280
8 changed files with 777 additions and 29 deletions
+100 -2
View File
@@ -9,6 +9,8 @@ from datetime import datetime
from core.sms_provider import SmsLine, SmsProviderClient from core.sms_provider import SmsLine, SmsProviderClient
from .change_password import change_huya_password_with_sms_line, generate_huya_password
from .cookie_utils import cookie_value
from .login import HuyaLoginError from .login import HuyaLoginError
from .sms_login import login_huya_sms, normalize_huya_phone, send_huya_sms_code from .sms_login import login_huya_sms, normalize_huya_phone, send_huya_sms_code
@@ -24,9 +26,16 @@ class HuyaAutoRegisterResult:
message: str message: str
cookie: str = "" cookie: str = ""
code: str = "" code: str = ""
change_code: str = ""
sdid: str = "" sdid: str = ""
normalized_phone: str = "" normalized_phone: str = ""
username: str = ""
uid: str = ""
password: str = ""
sms_url: str = ""
attempts: int = 0 attempts: int = 0
change_attempts: int = 0
password_changed: bool = False
def _sleep_or_stop(stop_event: threading.Event | None, seconds: float) -> bool: def _sleep_or_stop(stop_event: threading.Event | None, seconds: float) -> bool:
@@ -43,6 +52,9 @@ def register_huya_with_sms_line(
item: SmsLine, item: SmsLine,
wait_seconds: float = 180, wait_seconds: float = 180,
poll_interval: float = 5, poll_interval: float = 5,
change_password: bool = True,
password_prefix: str = "hy",
fixed_password: str = "",
stop_event: threading.Event | None = None, stop_event: threading.Event | None = None,
) -> HuyaAutoRegisterResult: ) -> HuyaAutoRegisterResult:
"""使用固定手机号和接码地址完成虎牙短信注册/登录。""" """使用固定手机号和接码地址完成虎牙短信注册/登录。"""
@@ -59,6 +71,7 @@ def register_huya_with_sms_line(
status="stopped", status="stopped",
message="已停止", message="已停止",
normalized_phone=normalized_phone, normalized_phone=normalized_phone,
sms_url=item.url,
) )
sent_at = datetime.now() sent_at = datetime.now()
@@ -72,6 +85,7 @@ def register_huya_with_sms_line(
status="error", status="error",
message=str(exc), message=str(exc),
normalized_phone=normalized_phone, normalized_phone=normalized_phone,
sms_url=item.url,
) )
except Exception as exc: except Exception as exc:
return HuyaAutoRegisterResult( return HuyaAutoRegisterResult(
@@ -81,6 +95,7 @@ def register_huya_with_sms_line(
status="error", status="error",
message=f"发送虎牙短信失败: {exc}", message=f"发送虎牙短信失败: {exc}",
normalized_phone=normalized_phone, normalized_phone=normalized_phone,
sms_url=item.url,
) )
if not code_result.success or not code_result.state: if not code_result.success or not code_result.state:
@@ -92,6 +107,7 @@ def register_huya_with_sms_line(
message=code_result.message or "发送虎牙短信失败", message=code_result.message or "发送虎牙短信失败",
sdid=code_result.sdid, sdid=code_result.sdid,
normalized_phone=normalized_phone, normalized_phone=normalized_phone,
sms_url=item.url,
) )
client = SmsProviderClient() client = SmsProviderClient()
@@ -109,6 +125,7 @@ def register_huya_with_sms_line(
message="已停止", message="已停止",
sdid=code_result.sdid, sdid=code_result.sdid,
normalized_phone=normalized_phone, normalized_phone=normalized_phone,
sms_url=item.url,
attempts=attempts, attempts=attempts,
) )
@@ -132,6 +149,7 @@ def register_huya_with_sms_line(
code=poll_result.code, code=poll_result.code,
sdid=code_result.sdid, sdid=code_result.sdid,
normalized_phone=normalized_phone, normalized_phone=normalized_phone,
sms_url=item.url,
attempts=attempts, attempts=attempts,
) )
except Exception as exc: except Exception as exc:
@@ -144,21 +162,98 @@ def register_huya_with_sms_line(
code=poll_result.code, code=poll_result.code,
sdid=code_result.sdid, sdid=code_result.sdid,
normalized_phone=normalized_phone, normalized_phone=normalized_phone,
sms_url=item.url,
attempts=attempts, attempts=attempts,
) )
if login_result.success and login_result.cookie: if login_result.success and login_result.cookie:
cookie = login_result.cookie
uid = cookie_value(cookie, "udb_uid") or cookie_value(cookie, "yyuid")
username = cookie_value(cookie, "udb_passport") or cookie_value(cookie, "username") or uid
if not change_password:
return HuyaAutoRegisterResult(
phone=phone,
provider=item.provider,
success=True,
status="success",
message="注册/登录成功",
cookie=cookie,
code=poll_result.code,
sdid=login_result.sdid or code_result.sdid,
normalized_phone=normalized_phone,
username=username,
uid=uid,
sms_url=item.url,
attempts=attempts,
)
if not uid:
return HuyaAutoRegisterResult(
phone=phone,
provider=item.provider,
success=False,
status="error",
message="注册成功但 Cookie 中没有识别到虎牙 uid,无法改密",
cookie=cookie,
code=poll_result.code,
sdid=login_result.sdid or code_result.sdid,
normalized_phone=normalized_phone,
username=username,
uid=uid,
sms_url=item.url,
attempts=attempts,
)
password = fixed_password.strip() or generate_huya_password(password_prefix)
change_result = change_huya_password_with_sms_line(
uid=uid,
cookie=cookie,
password=password,
item=item,
wait_seconds=wait_seconds,
poll_interval=poll_interval,
ignore_codes={poll_result.code},
stop_event=stop_event,
)
if not change_result.success:
failed_status = "stopped" if change_result.message == "已停止" else "error"
return HuyaAutoRegisterResult(
phone=phone,
provider=item.provider,
success=False,
status=failed_status,
message=f"注册成功但改密失败: {change_result.message}",
cookie=cookie,
code=poll_result.code,
change_code=change_result.code,
sdid=change_result.sdid or login_result.sdid or code_result.sdid,
normalized_phone=normalized_phone,
username=username,
uid=uid,
password=password,
sms_url=item.url,
attempts=attempts,
change_attempts=change_result.attempts,
)
return HuyaAutoRegisterResult( return HuyaAutoRegisterResult(
phone=phone, phone=phone,
provider=item.provider, provider=item.provider,
success=True, success=True,
status="success", status="success",
message="注册/登录成功", message="注册并改密成功",
cookie=login_result.cookie, cookie=cookie,
code=poll_result.code, code=poll_result.code,
change_code=change_result.code,
sdid=login_result.sdid or code_result.sdid, sdid=login_result.sdid or code_result.sdid,
normalized_phone=normalized_phone, normalized_phone=normalized_phone,
username=username,
uid=uid,
password=password,
sms_url=item.url,
attempts=attempts, attempts=attempts,
change_attempts=change_result.attempts,
password_changed=True,
) )
return HuyaAutoRegisterResult( return HuyaAutoRegisterResult(
phone=phone, phone=phone,
@@ -169,6 +264,7 @@ def register_huya_with_sms_line(
code=poll_result.code, code=poll_result.code,
sdid=login_result.sdid or code_result.sdid, sdid=login_result.sdid or code_result.sdid,
normalized_phone=normalized_phone, normalized_phone=normalized_phone,
sms_url=item.url,
attempts=attempts, attempts=attempts,
) )
@@ -184,6 +280,7 @@ def register_huya_with_sms_line(
message="已停止", message="已停止",
sdid=code_result.sdid, sdid=code_result.sdid,
normalized_phone=normalized_phone, normalized_phone=normalized_phone,
sms_url=item.url,
attempts=attempts, attempts=attempts,
) )
@@ -195,5 +292,6 @@ def register_huya_with_sms_line(
message=f"等待验证码超时: {last_message}", message=f"等待验证码超时: {last_message}",
sdid=code_result.sdid, sdid=code_result.sdid,
normalized_phone=normalized_phone, normalized_phone=normalized_phone,
sms_url=item.url,
attempts=attempts, attempts=attempts,
) )
+530
View File
@@ -0,0 +1,530 @@
"""虎牙短信改密流程。"""
from __future__ import annotations
import json
import random
import string
import threading
import time
import uuid
from collections.abc import Mapping
from dataclasses import dataclass
from datetime import datetime
from urllib.parse import quote, urlsplit, urlunsplit
import requests
from loguru import logger
from core.sms_provider import SmsLine, SmsProviderClient
from .login import (
APP_ID,
APP_SIGN,
DEFAULT_UA,
DF_COLLECT_URL,
DF_TOKEN_URL,
HuyaLoginError,
cookie_mapping,
generate_context,
generate_request_id,
)
CHANGE_PASSWORD_VERSION = "2.5"
CHANGE_PASSWORD_CHECK_URI = "60011"
CHANGE_PASSWORD_SEND_SMS_URI = "60003"
CHANGE_PASSWORD_SUBMIT_URI = "60007"
CHANGE_PASSWORD_CHECK_URL = "https://udbreg.huya.com/web/v2/modifyPswByCheck"
CHANGE_PASSWORD_SEND_SMS_URL = "https://udbreg.huya.com/sms/v2/send/changePsw"
CHANGE_PASSWORD_SUBMIT_URL = "https://udbreg.huya.com/web/v2/changePswBySms"
CHANGE_PASSWORD_PAGE_URL = "https://aq.huya.com/i/modify.html"
CHANGE_PASSWORD_FROM_URL = "https://i.huya.com/"
CHANGE_PASSWORD_PAGE = quote(CHANGE_PASSWORD_FROM_URL, safe="~()*!.'")
LCID = "2052"
@dataclass
class HuyaChangePasswordCodeResult:
"""虎牙改密短信发送结果。"""
success: bool
session_data: str = ""
message: str = ""
raw: dict | None = None
sdid: str = ""
context: str = ""
request_id: str = ""
@dataclass
class HuyaChangePasswordResult:
"""虎牙改密结果。"""
success: bool
message: str = ""
code: str = ""
raw: dict | None = None
sdid: str = ""
context: str = ""
request_id: str = ""
attempts: int = 0
def generate_huya_password(prefix: str = "hy", random_length: int = 8) -> str:
"""生成适合虎牙账号使用的随机密码。"""
clean_prefix = "".join(ch for ch in str(prefix or "hy") if ch.isalnum())[:8] or "hy"
alphabet = string.ascii_lowercase + string.digits
suffix = "".join(random.SystemRandom().choice(alphabet) for _ in range(max(6, random_length)))
return f"{clean_prefix}{suffix}"
def encode_change_password_behavior(stage: str) -> str:
"""生成改密页行为轨迹。"""
now = int(time.time() * 1000) - random.randint(2500, 9000)
elapsed = random.randint(900, 1800)
actions: list[dict] = []
if stage in {"send", "submit"}:
actions.append({
"id": "pmodify.btn.getsms",
"x": random.randint(470, 520),
"y": random.randint(120, 145),
"d": elapsed,
"time": now,
})
if stage == "submit":
for action_id in ("pmodify.input.sms", "pmodify.input.pw", "pmodify.input.sms"):
now += random.randint(600, 3200)
elapsed += random.randint(600, 3200)
actions.append({"id": action_id, "d": elapsed, "time": now})
now += random.randint(800, 2400)
elapsed += random.randint(800, 2400)
actions.append({
"id": "pmodify.btn.submit",
"x": random.randint(395, 445),
"y": random.randint(220, 245),
"d": elapsed,
"time": now,
})
value = {
"furl": CHANGE_PASSWORD_FROM_URL,
"curl": CHANGE_PASSWORD_PAGE_URL,
"user_action": actions,
}
return quote(json.dumps(value, separators=(",", ":"), ensure_ascii=False), safe="~()*!.'")
def _sleep_or_stop(stop_event: threading.Event | None, seconds: float) -> bool:
if seconds <= 0:
return not (stop_event and stop_event.is_set())
if stop_event:
return not stop_event.wait(seconds)
time.sleep(seconds)
return True
def _payload_data(payload: dict) -> dict:
data = payload.get("data")
return data if isinstance(data, dict) else {}
def _session_data(payload: dict, fallback: str = "") -> str:
data = _payload_data(payload)
return str(data.get("sessionData") or data.get("sessiondata") or payload.get("sessionData") or fallback or "")
class HuyaPasswordChanger:
"""虎牙已登录账号短信改密。"""
timeout = (8, 20)
def __init__(
self,
uid: str,
cookie: Mapping[str, str] | str | None = None,
ua: str = DEFAULT_UA,
session: requests.Session | None = None,
proxies: Mapping[str, str] | None = None,
timeout: tuple[float, float] | None = None,
):
self.uid = str(uid or "").strip()
if not self.uid:
raise HuyaLoginError("缺少虎牙 uid,无法修改密码")
self.ua = ua or DEFAULT_UA
self.timeout = timeout or self.timeout
self.request_id = generate_request_id()
self.exchange = self.request_id
self.device_id = uuid.uuid4().hex
self.mid = uuid.uuid4().hex.upper()
self.context = generate_context(self.device_id, self.mid)
self.middle_url = (
f"https://udbreg.huya.com/web/middle/{CHANGE_PASSWORD_VERSION}/"
f"{self.exchange}/https/{self.device_id}"
)
self.sdid = ""
self.session = session or requests.Session()
self.session.trust_env = False
if cookie:
self.session.cookies.update(cookie_mapping(cookie))
if proxies:
self.session.proxies = dict(proxies)
self._setup_headers()
def _setup_headers(self) -> None:
self.session.headers.update({
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Content-Type": "application/json;charset=UTF-8",
"Origin": "https://udbreg.huya.com",
"Pragma": "no-cache",
"Referer": self.middle_url,
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent": self.ua,
"sec-ch-ua": '"Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
})
@staticmethod
def _safe_url(url: str) -> str:
parsed = urlsplit(url)
return urlunsplit((parsed.scheme, parsed.netloc, parsed.path, "", ""))
def _request_json(self, method: str, url: str, source: str, **kwargs) -> dict:
response = self.session.request(method, url, timeout=self.timeout, **kwargs)
logger.debug(f"{method.upper()} {self._safe_url(url)} -> {response.status_code}")
response.raise_for_status()
try:
return response.json()
except json.JSONDecodeError as exc:
preview = response.text[:200].replace("\n", "\\n")
raise HuyaLoginError(f"{source} 返回不是 JSON: {preview}") from exc
def init_udb_middle(self) -> None:
"""初始化改密 middle 页面。"""
for host in ("udblgn.huya.com", "udbreg.huya.com"):
url = f"https://{host}/web/middle/{CHANGE_PASSWORD_VERSION}/{self.exchange}/https/{self.device_id}"
try:
response = self.session.get(url, timeout=self.timeout)
logger.debug(f"GET {self._safe_url(url)} -> {response.status_code}")
except requests.RequestException as exc:
logger.debug(f"虎牙改密 middle 初始化失败: {host}: {exc}")
self.session.headers.update({
"Origin": "https://udbreg.huya.com",
"Referer": self.middle_url,
})
def prepare_device(self) -> str:
"""获取虎牙风控 sdid。"""
token_payload = {"encryptVersion": "1.0.1", "fingerprintVersion": "1.2.41"}
token_res = self._request_json("post", DF_TOKEN_URL, "获取虎牙 df token", json=token_payload)
token = token_res.get("data", {}).get("token")
if not token:
raise HuyaLoginError(f"获取虎牙 df token 失败: {token_res}")
collect_res = self._request_json("post", DF_COLLECT_URL, "获取虎牙 sdid", json={"token": token})
self.sdid = collect_res.get("data", {}).get("sdid", "")
if not self.sdid:
raise HuyaLoginError(f"获取虎牙 sdid 失败: {collect_res}")
return self.sdid
def _solve_verification(self, payload: dict) -> None:
"""处理虎牙风控验证。"""
from .verification import HuyaVerificationSolver
solver = HuyaVerificationSolver(
cookie=self.session.cookies.get_dict(),
ua=self.ua,
sdid=self.sdid,
session=self.session,
proxies=self.session.proxies,
timeout=self.timeout,
)
solver.solve(payload)
def _common_payload(self, uri: str, data: dict) -> dict:
return {
"uri": uri,
"version": CHANGE_PASSWORD_VERSION,
"context": self.context,
"appId": APP_ID,
"appSign": APP_SIGN,
"authId": "",
"sdid": self.sdid,
"lcid": LCID,
"byPass": "3",
"requestId": self.request_id,
"data": data,
}
def _headers_for_uri(self, uri: str) -> dict:
return {
"Origin": "https://udbreg.huya.com",
"Referer": self.middle_url,
"context": self.context,
"lcid": LCID,
"reqid": self.request_id,
"uri": uri,
}
def _check_once(self) -> dict:
data = {
"uid": self.uid,
"behavior": encode_change_password_behavior("check"),
"page": CHANGE_PASSWORD_PAGE,
}
return self._request_json(
"post",
CHANGE_PASSWORD_CHECK_URL,
"虎牙改密前置校验",
json=self._common_payload(CHANGE_PASSWORD_CHECK_URI, data),
headers=self._headers_for_uri(CHANGE_PASSWORD_CHECK_URI),
)
def _send_sms_once(self, session_data: str) -> dict:
data = {
"phone": "",
"sessionData": session_data,
"behavior": encode_change_password_behavior("send"),
"page": CHANGE_PASSWORD_PAGE,
}
return self._request_json(
"post",
CHANGE_PASSWORD_SEND_SMS_URL,
"虎牙改密短信发码",
json=self._common_payload(CHANGE_PASSWORD_SEND_SMS_URI, data),
headers=self._headers_for_uri(CHANGE_PASSWORD_SEND_SMS_URI),
)
def _submit_once(self, password: str, sms_code: str, session_data: str) -> dict:
data = {
"password": password,
"smsCode": sms_code,
"sessionData": session_data,
"behavior": encode_change_password_behavior("submit"),
"page": CHANGE_PASSWORD_PAGE,
}
return self._request_json(
"post",
CHANGE_PASSWORD_SUBMIT_URL,
"虎牙短信改密",
json=self._common_payload(CHANGE_PASSWORD_SUBMIT_URI, data),
headers=self._headers_for_uri(CHANGE_PASSWORD_SUBMIT_URI),
)
def send_code(self) -> HuyaChangePasswordCodeResult:
"""发送改密短信,返回提交改密需要的 sessionData。"""
self.prepare_device()
self.init_udb_middle()
payload = self._check_once()
for index in range(3):
return_code = int(payload.get("returnCode") or 0)
if return_code in (10030, 10039):
logger.info(f"虎牙改密前置校验触发风控: {return_code} ({index + 1}/3)")
self._solve_verification(payload)
time.sleep(0.5)
payload = self._check_once()
continue
if return_code != 0:
return HuyaChangePasswordCodeResult(
success=False,
message=f"虎牙改密前置校验失败: {payload}",
raw=payload,
sdid=self.sdid,
context=self.context,
request_id=self.request_id,
)
break
else:
return HuyaChangePasswordCodeResult(
success=False,
message=f"虎牙改密前置校验风控后仍失败: {payload}",
raw=payload,
sdid=self.sdid,
context=self.context,
request_id=self.request_id,
)
session_data = _session_data(payload)
if not session_data:
return HuyaChangePasswordCodeResult(
success=False,
message=f"虎牙改密前置校验未返回 sessionData: {payload}",
raw=payload,
sdid=self.sdid,
context=self.context,
request_id=self.request_id,
)
payload = self._send_sms_once(session_data)
for index in range(3):
return_code = int(payload.get("returnCode") or 0)
session_data = _session_data(payload, fallback=session_data)
if return_code == 0 and session_data:
return HuyaChangePasswordCodeResult(
success=True,
session_data=session_data,
message="改密短信已发送",
raw=payload,
sdid=self.sdid,
context=self.context,
request_id=self.request_id,
)
if return_code not in (10030, 10039):
return HuyaChangePasswordCodeResult(
success=False,
message=f"虎牙改密短信发码失败: {payload}",
raw=payload,
sdid=self.sdid,
context=self.context,
request_id=self.request_id,
)
logger.info(f"虎牙改密短信发码触发风控: {return_code} ({index + 1}/3)")
self._solve_verification(payload)
time.sleep(0.5)
payload = self._send_sms_once(session_data)
return HuyaChangePasswordCodeResult(
success=False,
message=f"虎牙改密短信发码风控后仍失败: {payload}",
raw=payload,
sdid=self.sdid,
context=self.context,
request_id=self.request_id,
)
def submit_code(self, password: str, sms_code: str, session_data: str) -> HuyaChangePasswordResult:
"""提交改密短信验证码。"""
if not session_data:
raise HuyaLoginError("缺少改密 sessionData,请先发送改密短信")
payload = self._submit_once(password=password, sms_code=sms_code, session_data=session_data)
for index in range(3):
return_code = int(payload.get("returnCode") or 0)
if return_code == 0:
return HuyaChangePasswordResult(
success=True,
message="密码修改成功",
code=sms_code,
raw=payload,
sdid=self.sdid,
context=self.context,
request_id=self.request_id,
attempts=index + 1,
)
if return_code not in (10030, 10039):
return HuyaChangePasswordResult(
success=False,
message=f"虎牙短信改密失败: {payload}",
code=sms_code,
raw=payload,
sdid=self.sdid,
context=self.context,
request_id=self.request_id,
attempts=index + 1,
)
logger.info(f"虎牙短信改密触发风控: {return_code} ({index + 1}/3)")
self._solve_verification(payload)
time.sleep(0.5)
payload = self._submit_once(password=password, sms_code=sms_code, session_data=session_data)
return HuyaChangePasswordResult(
success=False,
message=f"虎牙短信改密风控后仍失败: {payload}",
code=sms_code,
raw=payload,
sdid=self.sdid,
context=self.context,
request_id=self.request_id,
attempts=3,
)
def change_huya_password_with_sms_line(
uid: str,
cookie: Mapping[str, str] | str,
password: str,
item: SmsLine,
wait_seconds: float = 180,
poll_interval: float = 5,
ignore_codes: set[str] | None = None,
stop_event: threading.Event | None = None,
) -> HuyaChangePasswordResult:
"""使用同一手机号接码链接完成改密短信验证。"""
changer = HuyaPasswordChanger(uid=uid, cookie=cookie)
sent_at = datetime.now()
code_result = changer.send_code()
if not code_result.success or not code_result.session_data:
return HuyaChangePasswordResult(
success=False,
message=code_result.message or "虎牙改密短信发码失败",
raw=code_result.raw,
sdid=code_result.sdid,
context=code_result.context,
request_id=code_result.request_id,
)
client = SmsProviderClient()
deadline = time.monotonic() + max(15.0, float(wait_seconds or 180))
interval = max(1.0, float(poll_interval or 5))
attempts = 0
ignored = {str(code) for code in (ignore_codes or set()) if code}
last_message = "等待改密验证码"
while time.monotonic() < deadline:
if stop_event and stop_event.is_set():
return HuyaChangePasswordResult(
success=False,
message="已停止",
sdid=code_result.sdid,
context=code_result.context,
request_id=code_result.request_id,
attempts=attempts,
)
attempts += 1
poll_result = client.poll(item, after_time=sent_at)
last_message = poll_result.message or last_message
if poll_result.has_code:
if poll_result.code in ignored:
last_message = "忽略注册阶段旧验证码"
else:
result = changer.submit_code(
password=password,
sms_code=poll_result.code,
session_data=code_result.session_data,
)
result.attempts = attempts
return result
remaining = deadline - time.monotonic()
if remaining <= 0:
break
if not _sleep_or_stop(stop_event, min(interval, remaining)):
return HuyaChangePasswordResult(
success=False,
message="已停止",
sdid=code_result.sdid,
context=code_result.context,
request_id=code_result.request_id,
attempts=attempts,
)
return HuyaChangePasswordResult(
success=False,
message=f"等待改密验证码超时: {last_message}",
sdid=code_result.sdid,
context=code_result.context,
request_id=code_result.request_id,
attempts=attempts,
)
+2
View File
@@ -359,6 +359,8 @@ def create_auto_register_batch(
concurrency=req.concurrency, concurrency=req.concurrency,
wait_seconds=req.wait_seconds, wait_seconds=req.wait_seconds,
poll_interval=req.poll_interval, poll_interval=req.poll_interval,
password_prefix=req.password_prefix,
fixed_password=req.fixed_password,
) )
thread = threading.Thread(target=runner.run, daemon=True) thread = threading.Thread(target=runner.run, daemon=True)
thread.start() thread.start()
+9
View File
@@ -204,18 +204,26 @@ class HuyaAutoRegisterRequest(BaseModel):
concurrency: int = Field(1, ge=1, le=5) concurrency: int = Field(1, ge=1, le=5)
wait_seconds: float = Field(180, ge=15, le=600) wait_seconds: float = Field(180, ge=15, le=600)
poll_interval: float = Field(5, ge=1, le=30) poll_interval: float = Field(5, ge=1, le=30)
password_prefix: str = Field("hy", max_length=8)
fixed_password: str = Field("", max_length=64)
class HuyaAutoRegisterItemOut(BaseModel): class HuyaAutoRegisterItemOut(BaseModel):
line: int line: int
phone: str phone: str
provider: str provider: str
sms_url: str = ""
status: str status: str
message: str message: str
code: str = "" code: str = ""
change_code: str = ""
attempts: int = 0 attempts: int = 0
change_attempts: int = 0
account_id: Optional[int] = None account_id: Optional[int] = None
username: str = ""
uid: str = "" uid: str = ""
password: str = ""
password_changed: bool = False
cookie: str = "" cookie: str = ""
cookie_preview: str = "" cookie_preview: str = ""
started_at: Optional[datetime] = None started_at: Optional[datetime] = None
@@ -231,6 +239,7 @@ class HuyaAutoRegisterBatchOut(BaseModel):
concurrency: int concurrency: int
wait_seconds: float wait_seconds: float
poll_interval: float poll_interval: float
password_prefix: str = "hy"
total: int total: int
success_count: int success_count: int
failed_count: int failed_count: int
+58 -11
View File
@@ -34,12 +34,18 @@ class HuyaRegisterItemState:
line: int line: int
phone: str phone: str
provider: str provider: str
sms_url: str = ""
status: str = "pending" status: str = "pending"
message: str = "等待开始" message: str = "等待开始"
code: str = "" code: str = ""
change_code: str = ""
attempts: int = 0 attempts: int = 0
change_attempts: int = 0
account_id: int | None = None account_id: int | None = None
username: str = ""
uid: str = "" uid: str = ""
password: str = ""
password_changed: bool = False
cookie: str = "" cookie: str = ""
cookie_preview: str = "" cookie_preview: str = ""
started_at: datetime | None = None started_at: datetime | None = None
@@ -50,12 +56,18 @@ class HuyaRegisterItemState:
"line": self.line, "line": self.line,
"phone": self.phone, "phone": self.phone,
"provider": self.provider, "provider": self.provider,
"sms_url": self.sms_url,
"status": self.status, "status": self.status,
"message": self.message, "message": self.message,
"code": self.code, "code": self.code,
"change_code": self.change_code,
"attempts": self.attempts, "attempts": self.attempts,
"change_attempts": self.change_attempts,
"account_id": self.account_id, "account_id": self.account_id,
"username": self.username,
"uid": self.uid, "uid": self.uid,
"password": self.password,
"password_changed": self.password_changed,
"cookie": self.cookie, "cookie": self.cookie,
"cookie_preview": self.cookie_preview, "cookie_preview": self.cookie_preview,
"started_at": self.started_at, "started_at": self.started_at,
@@ -74,6 +86,8 @@ class HuyaRegisterBatch:
wait_seconds: float wait_seconds: float
poll_interval: float poll_interval: float
items: list[HuyaRegisterItemState] items: list[HuyaRegisterItemState]
password_prefix: str = "hy"
fixed_password: str = ""
status: str = "pending" status: str = "pending"
message: str = "等待开始" message: str = "等待开始"
created_at: datetime = field(default_factory=_now) created_at: datetime = field(default_factory=_now)
@@ -106,7 +120,7 @@ class HuyaRegisterRunner:
success = sum(1 for item in self.batch.items if item.status == "success") success = sum(1 for item in self.batch.items if item.status == "success")
failed = sum(1 for item in self.batch.items if item.status == "error") failed = sum(1 for item in self.batch.items if item.status == "error")
stopped = sum(1 for item in self.batch.items if item.status == "stopped") stopped = sum(1 for item in self.batch.items if item.status == "stopped")
running = sum(1 for item in self.batch.items if item.status in {"sending", "waiting", "logging"}) running = sum(1 for item in self.batch.items if item.status in {"sending", "waiting", "changing", "logging"})
return { return {
"batch_id": self.batch.batch_id, "batch_id": self.batch.batch_id,
"status": self.batch.status, "status": self.batch.status,
@@ -116,6 +130,7 @@ class HuyaRegisterRunner:
"concurrency": self.batch.concurrency, "concurrency": self.batch.concurrency,
"wait_seconds": self.batch.wait_seconds, "wait_seconds": self.batch.wait_seconds,
"poll_interval": self.batch.poll_interval, "poll_interval": self.batch.poll_interval,
"password_prefix": self.batch.password_prefix,
"total": total, "total": total,
"success_count": success, "success_count": success,
"failed_count": failed, "failed_count": failed,
@@ -133,15 +148,21 @@ class HuyaRegisterRunner:
for key, value in updates.items(): for key, value in updates.items():
setattr(item, key, value) setattr(item, key, value)
def _save_cookie(self, result: HuyaAutoRegisterResult) -> tuple[int | None, str]: def _save_cookie(self, result: HuyaAutoRegisterResult) -> tuple[int | None, str, str]:
db = SessionLocal() db = SessionLocal()
try: try:
account = upsert_huya_cookie(db, result.cookie, tag=self.batch.tag, username_hint="") account = upsert_huya_cookie(db, result.cookie, tag=self.batch.tag, username_hint="")
account.game_phone = result.phone account.game_phone = result.phone
if result.username:
account.username = result.username
if result.password:
account.account_password = result.password
if result.password_changed:
account.status = "password_changed"
account.updated_at = _now() account.updated_at = _now()
db.commit() db.commit()
db.refresh(account) db.refresh(account)
return account.id, account.uid or account.yyuid or "" return account.id, account.username or "", account.uid or account.yyuid or ""
finally: finally:
db.close() db.close()
@@ -150,38 +171,60 @@ class HuyaRegisterRunner:
self._set_item(index, status="stopped", message="已停止", finished_at=_now()) self._set_item(index, status="stopped", message="已停止", finished_at=_now())
return return
self._set_item(index, status="sending", message="发送虎牙短信", started_at=_now(), finished_at=None) self._set_item(index, status="sending", message="注册并改密", started_at=_now(), finished_at=None)
result = register_huya_with_sms_line( result = register_huya_with_sms_line(
item, item,
wait_seconds=self.batch.wait_seconds, wait_seconds=self.batch.wait_seconds,
poll_interval=self.batch.poll_interval, poll_interval=self.batch.poll_interval,
password_prefix=self.batch.password_prefix,
fixed_password=self.batch.fixed_password,
stop_event=self._stop, stop_event=self._stop,
) )
account_id = None account_id = None
uid = "" username = result.username
uid = result.uid
message = result.message message = result.message
status = result.status status = result.status
cookie = result.cookie if result.success else "" cookie = result.cookie if result.success else ""
if result.success: if result.success:
self._set_item(index, status="logging", message="保存 Cookie", code=result.code, attempts=result.attempts) self._set_item(
index,
status="logging",
message="保存账号密码",
code=result.code,
change_code=result.change_code,
attempts=result.attempts,
change_attempts=result.change_attempts,
username=result.username,
uid=result.uid,
password=result.password,
password_changed=result.password_changed,
)
try: try:
account_id, uid = self._save_cookie(result) account_id, username, uid = self._save_cookie(result)
except Exception as exc: except Exception as exc:
status = "error" status = "error"
cookie = "" cookie = ""
message = f"Cookie 保存失败: {exc}" message = f"账号保存失败: {exc}"
exposed_cookie = "" if result.password_changed else cookie
self._set_item( self._set_item(
index, index,
status=status, status=status,
message=message, message=message,
code=result.code, code=result.code,
change_code=result.change_code,
attempts=result.attempts, attempts=result.attempts,
change_attempts=result.change_attempts,
account_id=account_id, account_id=account_id,
username=username,
uid=uid, uid=uid,
cookie=normalize_huya_cookie(cookie), password=result.password,
cookie_preview=_cookie_preview(cookie), password_changed=result.password_changed,
cookie=normalize_huya_cookie(exposed_cookie),
cookie_preview=_cookie_preview(exposed_cookie),
finished_at=_now(), finished_at=_now(),
) )
@@ -235,6 +278,8 @@ class HuyaRegisterRegistry:
concurrency: int, concurrency: int,
wait_seconds: float, wait_seconds: float,
poll_interval: float, poll_interval: float,
password_prefix: str = "hy",
fixed_password: str = "",
) -> HuyaRegisterRunner: ) -> HuyaRegisterRunner:
batch_id = uuid.uuid4().hex[:12] batch_id = uuid.uuid4().hex[:12]
batch = HuyaRegisterBatch( batch = HuyaRegisterBatch(
@@ -244,8 +289,10 @@ class HuyaRegisterRegistry:
concurrency=max(1, min(int(concurrency or 1), 5)), concurrency=max(1, min(int(concurrency or 1), 5)),
wait_seconds=max(15.0, float(wait_seconds or 180)), wait_seconds=max(15.0, float(wait_seconds or 180)),
poll_interval=max(1.0, float(poll_interval or 5)), poll_interval=max(1.0, float(poll_interval or 5)),
password_prefix=(password_prefix or "hy").strip()[:8] or "hy",
fixed_password=(fixed_password or "").strip(),
items=[ items=[
HuyaRegisterItemState(line=index + 1, phone=item.phone, provider=item.provider) HuyaRegisterItemState(line=index + 1, phone=item.phone, provider=item.provider, sms_url=item.url)
for index, item in enumerate(sms_lines) for index, item in enumerate(sms_lines)
], ],
) )
+9
View File
@@ -179,18 +179,26 @@ export interface HuyaAutoRegisterRequest {
concurrency?: number; concurrency?: number;
wait_seconds?: number; wait_seconds?: number;
poll_interval?: number; poll_interval?: number;
password_prefix?: string;
fixed_password?: string;
} }
export interface HuyaAutoRegisterItem { export interface HuyaAutoRegisterItem {
line: number; line: number;
phone: string; phone: string;
provider: string; provider: string;
sms_url: string;
status: string; status: string;
message: string; message: string;
code: string; code: string;
change_code: string;
attempts: number; attempts: number;
change_attempts: number;
account_id: number | null; account_id: number | null;
username: string;
uid: string; uid: string;
password: string;
password_changed: boolean;
cookie: string; cookie: string;
cookie_preview: string; cookie_preview: string;
started_at: string | null; started_at: string | null;
@@ -206,6 +214,7 @@ export interface HuyaAutoRegisterBatch {
concurrency: number; concurrency: number;
wait_seconds: number; wait_seconds: number;
poll_interval: number; poll_interval: number;
password_prefix: string;
total: number; total: number;
success_count: number; success_count: number;
failed_count: number; failed_count: number;
@@ -22,6 +22,7 @@ const STATUS_LABELS: Record<string, string> = {
updated: '已更新', updated: '已更新',
password_imported: '待登录', password_imported: '待登录',
login_success: '登录成功', login_success: '登录成功',
password_changed: '已改密',
login_failed: '登录失败', login_failed: '登录失败',
active: '正常', active: '正常',
invalid: '失效', invalid: '失效',
@@ -32,6 +33,7 @@ const STATUS_COLORS: Record<string, string> = {
updated: 'cyan', updated: 'cyan',
password_imported: 'warning', password_imported: 'warning',
login_success: 'success', login_success: 'success',
password_changed: 'success',
login_failed: 'error', login_failed: 'error',
active: 'success', active: 'success',
invalid: 'error', invalid: 'error',
+67 -16
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { import {
Button, Card, Col, Input, InputNumber, message, Row, Space, Statistic, Table, Tag, Typography, Button, Card, Col, Input, InputNumber, message, Row, Segmented, Space, Statistic, Table, Tag, Typography,
} from 'antd'; } from 'antd';
import type { TableProps } from 'antd'; import type { TableProps } from 'antd';
import { DownloadOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons'; import { DownloadOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons';
@@ -16,6 +16,7 @@ const STATUS_LABELS: Record<string, string> = {
sending: '发码', sending: '发码',
waiting: '等待验证码', waiting: '等待验证码',
logging: '保存', logging: '保存',
changing: '改密',
success: '成功', success: '成功',
error: '失败', error: '失败',
stopped: '已停止', stopped: '已停止',
@@ -26,6 +27,7 @@ const STATUS_COLORS: Record<string, string> = {
sending: 'processing', sending: 'processing',
waiting: 'processing', waiting: 'processing',
logging: 'processing', logging: 'processing',
changing: 'processing',
success: 'success', success: 'success',
error: 'error', error: 'error',
stopped: 'warning', stopped: 'warning',
@@ -39,6 +41,9 @@ export default function HuyaRegisterPage() {
const [concurrency, setConcurrency] = useState(1); const [concurrency, setConcurrency] = useState(1);
const [waitSeconds, setWaitSeconds] = useState(180); const [waitSeconds, setWaitSeconds] = useState(180);
const [pollInterval, setPollInterval] = useState(5); const [pollInterval, setPollInterval] = useState(5);
const [passwordMode, setPasswordMode] = useState<'random' | 'fixed'>('random');
const [passwordPrefix, setPasswordPrefix] = useState('hy');
const [fixedPassword, setFixedPassword] = useState('');
const [batch, setBatch] = useState<HuyaAutoRegisterBatch | null>(null); const [batch, setBatch] = useState<HuyaAutoRegisterBatch | null>(null);
const [starting, setStarting] = useState(false); const [starting, setStarting] = useState(false);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
@@ -69,7 +74,7 @@ export default function HuyaRegisterPage() {
}, [batchId, isRunning, refreshBatch]); }, [batchId, isRunning, refreshBatch]);
const successRows = useMemo( const successRows = useMemo(
() => (batch?.items || []).filter((item) => item.status === 'success' && item.cookie), () => (batch?.items || []).filter((item) => item.status === 'success' && item.password && (item.username || item.uid)),
[batch], [batch],
); );
@@ -78,6 +83,10 @@ export default function HuyaRegisterPage() {
message.warning('请先粘贴手机号池'); message.warning('请先粘贴手机号池');
return; return;
} }
if (passwordMode === 'fixed' && !fixedPassword.trim()) {
message.warning('请先填写固定密码');
return;
}
setStarting(true); setStarting(true);
try { try {
const data = await huyaApi.startAutoRegister({ const data = await huyaApi.startAutoRegister({
@@ -86,6 +95,8 @@ export default function HuyaRegisterPage() {
concurrency, concurrency,
wait_seconds: waitSeconds, wait_seconds: waitSeconds,
poll_interval: pollInterval, poll_interval: pollInterval,
password_prefix: passwordMode === 'random' ? (passwordPrefix.trim() || 'hy') : 'hy',
fixed_password: passwordMode === 'fixed' ? fixedPassword.trim() : '',
}); });
setBatch(data); setBatch(data);
message.success('自动注册批次已启动'); message.success('自动注册批次已启动');
@@ -112,15 +123,17 @@ export default function HuyaRegisterPage() {
const handleExportSuccess = () => { const handleExportSuccess = () => {
if (successRows.length === 0) { if (successRows.length === 0) {
message.warning('当前批次没有可导出的成功 CK'); message.warning('当前批次没有可导出的成功账号');
return; return;
} }
const body = successRows.map((item) => `${item.phone}----${item.cookie}`).join('\n'); const body = successRows
.map((item) => `${item.username || item.uid}----${item.password}----${item.phone}----${item.sms_url}`)
.join('\n');
const blob = new Blob([body], { type: 'text/plain;charset=utf-8' }); const blob = new Blob([body], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
link.download = `huya-register-${batchId || 'success'}.txt`; link.download = `huya-register-accounts-${batchId || 'success'}.txt`;
link.click(); link.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}; };
@@ -128,6 +141,17 @@ export default function HuyaRegisterPage() {
const columns: TableProps<HuyaAutoRegisterItem>['columns'] = [ const columns: TableProps<HuyaAutoRegisterItem>['columns'] = [
{ title: '行', dataIndex: 'line', width: 64 }, { title: '行', dataIndex: 'line', width: 64 },
{ title: '手机号', dataIndex: 'phone', width: 150 }, { title: '手机号', dataIndex: 'phone', width: 150 },
{
title: '虎牙号',
width: 150,
render: (_, item) => item.username || item.uid || '-',
},
{
title: '密码',
dataIndex: 'password',
width: 130,
render: (value: string) => value || '-',
},
{ title: '平台', dataIndex: 'provider', width: 90 }, { title: '平台', dataIndex: 'provider', width: 90 },
{ {
title: '状态', title: '状态',
@@ -139,20 +163,18 @@ export default function HuyaRegisterPage() {
</Tag> </Tag>
), ),
}, },
{ title: '验证码', dataIndex: 'code', width: 100 }, { title: '注册码', dataIndex: 'code', width: 100 },
{ title: '轮询', dataIndex: 'attempts', width: 80 }, { title: '改密码', dataIndex: 'change_code', width: 100 },
{
title: '轮询',
width: 90,
render: (_, item) => `${item.attempts}/${item.change_attempts}`,
},
{ {
title: '账号', title: '账号',
width: 160, width: 160,
render: (_, item) => item.uid || (item.account_id ? `#${item.account_id}` : '-'), render: (_, item) => item.uid || (item.account_id ? `#${item.account_id}` : '-'),
}, },
{
title: 'Cookie',
dataIndex: 'cookie_preview',
width: 220,
ellipsis: true,
render: (value: string) => value || '-',
},
{ {
title: '消息', title: '消息',
dataIndex: 'message', dataIndex: 'message',
@@ -223,6 +245,35 @@ export default function HuyaRegisterPage() {
style={{ width: '100%' }} style={{ width: '100%' }}
/> />
</Col> </Col>
<Col xs={24} md={8}>
<Text type="secondary"></Text>
<Space.Compact style={{ width: '100%' }}>
<Segmented
value={passwordMode}
options={[
{ label: '随机', value: 'random' },
{ label: '固定', value: 'fixed' },
]}
onChange={(value) => setPasswordMode(value as 'random' | 'fixed')}
disabled={isRunning}
/>
{passwordMode === 'random' ? (
<Input
value={passwordPrefix}
maxLength={8}
onChange={(event) => setPasswordPrefix(event.target.value)}
disabled={isRunning}
/>
) : (
<Input.Password
value={fixedPassword}
maxLength={64}
onChange={(event) => setFixedPassword(event.target.value)}
disabled={isRunning}
/>
)}
</Space.Compact>
</Col>
<Col xs={24} md={4}> <Col xs={24} md={4}>
<Space style={{ width: '100%', paddingTop: 22 }}> <Space style={{ width: '100%', paddingTop: 22 }}>
<Button <Button
@@ -257,7 +308,7 @@ export default function HuyaRegisterPage() {
</Button> </Button>
<Button icon={<DownloadOutlined />} disabled={successRows.length === 0} onClick={handleExportSuccess}> <Button icon={<DownloadOutlined />} disabled={successRows.length === 0} onClick={handleExportSuccess}>
CK
</Button> </Button>
</Space> </Space>
)} )}
@@ -276,7 +327,7 @@ export default function HuyaRegisterPage() {
dataSource={batch?.items || []} dataSource={batch?.items || []}
loading={refreshing && !isRunning} loading={refreshing && !isRunning}
pagination={{ pageSize: 20, showSizeChanger: true }} pagination={{ pageSize: 20, showSizeChanger: true }}
scroll={{ x: 1280 }} scroll={{ x: 1320 }}
/> />
</Card> </Card>
</Space> </Space>