diff --git a/core/huya/auto_register.py b/core/huya/auto_register.py index 1520c88..7a9866c 100644 --- a/core/huya/auto_register.py +++ b/core/huya/auto_register.py @@ -9,6 +9,8 @@ from datetime import datetime 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 .sms_login import login_huya_sms, normalize_huya_phone, send_huya_sms_code @@ -24,9 +26,16 @@ class HuyaAutoRegisterResult: message: str cookie: str = "" code: str = "" + change_code: str = "" sdid: str = "" normalized_phone: str = "" + username: str = "" + uid: str = "" + password: str = "" + sms_url: str = "" attempts: int = 0 + change_attempts: int = 0 + password_changed: bool = False def _sleep_or_stop(stop_event: threading.Event | None, seconds: float) -> bool: @@ -43,6 +52,9 @@ def register_huya_with_sms_line( item: SmsLine, wait_seconds: float = 180, poll_interval: float = 5, + change_password: bool = True, + password_prefix: str = "hy", + fixed_password: str = "", stop_event: threading.Event | None = None, ) -> HuyaAutoRegisterResult: """使用固定手机号和接码地址完成虎牙短信注册/登录。""" @@ -59,6 +71,7 @@ def register_huya_with_sms_line( status="stopped", message="已停止", normalized_phone=normalized_phone, + sms_url=item.url, ) sent_at = datetime.now() @@ -72,6 +85,7 @@ def register_huya_with_sms_line( status="error", message=str(exc), normalized_phone=normalized_phone, + sms_url=item.url, ) except Exception as exc: return HuyaAutoRegisterResult( @@ -81,6 +95,7 @@ def register_huya_with_sms_line( status="error", message=f"发送虎牙短信失败: {exc}", normalized_phone=normalized_phone, + sms_url=item.url, ) 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 "发送虎牙短信失败", sdid=code_result.sdid, normalized_phone=normalized_phone, + sms_url=item.url, ) client = SmsProviderClient() @@ -109,6 +125,7 @@ def register_huya_with_sms_line( message="已停止", sdid=code_result.sdid, normalized_phone=normalized_phone, + sms_url=item.url, attempts=attempts, ) @@ -132,6 +149,7 @@ def register_huya_with_sms_line( code=poll_result.code, sdid=code_result.sdid, normalized_phone=normalized_phone, + sms_url=item.url, attempts=attempts, ) except Exception as exc: @@ -144,21 +162,98 @@ def register_huya_with_sms_line( code=poll_result.code, sdid=code_result.sdid, normalized_phone=normalized_phone, + sms_url=item.url, attempts=attempts, ) 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( phone=phone, provider=item.provider, success=True, status="success", - message="注册/登录成功", - cookie=login_result.cookie, + message="注册并改密成功", + cookie=cookie, code=poll_result.code, + change_code=change_result.code, sdid=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, + password_changed=True, ) return HuyaAutoRegisterResult( phone=phone, @@ -169,6 +264,7 @@ def register_huya_with_sms_line( code=poll_result.code, sdid=login_result.sdid or code_result.sdid, normalized_phone=normalized_phone, + sms_url=item.url, attempts=attempts, ) @@ -184,6 +280,7 @@ def register_huya_with_sms_line( message="已停止", sdid=code_result.sdid, normalized_phone=normalized_phone, + sms_url=item.url, attempts=attempts, ) @@ -195,5 +292,6 @@ def register_huya_with_sms_line( message=f"等待验证码超时: {last_message}", sdid=code_result.sdid, normalized_phone=normalized_phone, + sms_url=item.url, attempts=attempts, ) diff --git a/core/huya/change_password.py b/core/huya/change_password.py new file mode 100644 index 0000000..b1ff2a9 --- /dev/null +++ b/core/huya/change_password.py @@ -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, + ) diff --git a/web/backend/routers/huya.py b/web/backend/routers/huya.py index 037b234..ee7f148 100644 --- a/web/backend/routers/huya.py +++ b/web/backend/routers/huya.py @@ -359,6 +359,8 @@ def create_auto_register_batch( concurrency=req.concurrency, wait_seconds=req.wait_seconds, poll_interval=req.poll_interval, + password_prefix=req.password_prefix, + fixed_password=req.fixed_password, ) thread = threading.Thread(target=runner.run, daemon=True) thread.start() diff --git a/web/backend/schemas.py b/web/backend/schemas.py index 082fb1c..0032f03 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -204,18 +204,26 @@ class HuyaAutoRegisterRequest(BaseModel): concurrency: int = Field(1, ge=1, le=5) wait_seconds: float = Field(180, ge=15, le=600) 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): line: int phone: str provider: str + sms_url: str = "" status: str message: str code: str = "" + change_code: str = "" attempts: int = 0 + change_attempts: int = 0 account_id: Optional[int] = None + username: str = "" uid: str = "" + password: str = "" + password_changed: bool = False cookie: str = "" cookie_preview: str = "" started_at: Optional[datetime] = None @@ -231,6 +239,7 @@ class HuyaAutoRegisterBatchOut(BaseModel): concurrency: int wait_seconds: float poll_interval: float + password_prefix: str = "hy" total: int success_count: int failed_count: int diff --git a/web/backend/services/huya_register_runner.py b/web/backend/services/huya_register_runner.py index 170d205..53c2c81 100644 --- a/web/backend/services/huya_register_runner.py +++ b/web/backend/services/huya_register_runner.py @@ -34,12 +34,18 @@ class HuyaRegisterItemState: line: int phone: str provider: str + sms_url: str = "" status: str = "pending" message: str = "等待开始" code: str = "" + change_code: str = "" attempts: int = 0 + change_attempts: int = 0 account_id: int | None = None + username: str = "" uid: str = "" + password: str = "" + password_changed: bool = False cookie: str = "" cookie_preview: str = "" started_at: datetime | None = None @@ -50,12 +56,18 @@ class HuyaRegisterItemState: "line": self.line, "phone": self.phone, "provider": self.provider, + "sms_url": self.sms_url, "status": self.status, "message": self.message, "code": self.code, + "change_code": self.change_code, "attempts": self.attempts, + "change_attempts": self.change_attempts, "account_id": self.account_id, + "username": self.username, "uid": self.uid, + "password": self.password, + "password_changed": self.password_changed, "cookie": self.cookie, "cookie_preview": self.cookie_preview, "started_at": self.started_at, @@ -74,6 +86,8 @@ class HuyaRegisterBatch: wait_seconds: float poll_interval: float items: list[HuyaRegisterItemState] + password_prefix: str = "hy" + fixed_password: str = "" status: str = "pending" message: str = "等待开始" 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") 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") - 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 { "batch_id": self.batch.batch_id, "status": self.batch.status, @@ -116,6 +130,7 @@ class HuyaRegisterRunner: "concurrency": self.batch.concurrency, "wait_seconds": self.batch.wait_seconds, "poll_interval": self.batch.poll_interval, + "password_prefix": self.batch.password_prefix, "total": total, "success_count": success, "failed_count": failed, @@ -133,15 +148,21 @@ class HuyaRegisterRunner: for key, value in updates.items(): 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() try: account = upsert_huya_cookie(db, result.cookie, tag=self.batch.tag, username_hint="") 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() db.commit() 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: db.close() @@ -150,38 +171,60 @@ class HuyaRegisterRunner: self._set_item(index, status="stopped", message="已停止", finished_at=_now()) 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( item, wait_seconds=self.batch.wait_seconds, poll_interval=self.batch.poll_interval, + password_prefix=self.batch.password_prefix, + fixed_password=self.batch.fixed_password, stop_event=self._stop, ) account_id = None - uid = "" + username = result.username + uid = result.uid message = result.message status = result.status cookie = result.cookie if result.success else "" 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: - account_id, uid = self._save_cookie(result) + account_id, username, uid = self._save_cookie(result) except Exception as exc: status = "error" cookie = "" - message = f"Cookie 保存失败: {exc}" + message = f"账号保存失败: {exc}" + + exposed_cookie = "" if result.password_changed else cookie self._set_item( index, status=status, message=message, code=result.code, + change_code=result.change_code, attempts=result.attempts, + change_attempts=result.change_attempts, account_id=account_id, + username=username, uid=uid, - cookie=normalize_huya_cookie(cookie), - cookie_preview=_cookie_preview(cookie), + password=result.password, + password_changed=result.password_changed, + cookie=normalize_huya_cookie(exposed_cookie), + cookie_preview=_cookie_preview(exposed_cookie), finished_at=_now(), ) @@ -235,6 +278,8 @@ class HuyaRegisterRegistry: concurrency: int, wait_seconds: float, poll_interval: float, + password_prefix: str = "hy", + fixed_password: str = "", ) -> HuyaRegisterRunner: batch_id = uuid.uuid4().hex[:12] batch = HuyaRegisterBatch( @@ -244,8 +289,10 @@ class HuyaRegisterRegistry: concurrency=max(1, min(int(concurrency or 1), 5)), wait_seconds=max(15.0, float(wait_seconds or 180)), 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=[ - 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) ], ) diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index b559535..cd2ef06 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -179,18 +179,26 @@ export interface HuyaAutoRegisterRequest { concurrency?: number; wait_seconds?: number; poll_interval?: number; + password_prefix?: string; + fixed_password?: string; } export interface HuyaAutoRegisterItem { line: number; phone: string; provider: string; + sms_url: string; status: string; message: string; code: string; + change_code: string; attempts: number; + change_attempts: number; account_id: number | null; + username: string; uid: string; + password: string; + password_changed: boolean; cookie: string; cookie_preview: string; started_at: string | null; @@ -206,6 +214,7 @@ export interface HuyaAutoRegisterBatch { concurrency: number; wait_seconds: number; poll_interval: number; + password_prefix: string; total: number; success_count: number; failed_count: number; diff --git a/web/frontend/src/pages/HuyaAccountsPage.tsx b/web/frontend/src/pages/HuyaAccountsPage.tsx index d9e224c..fa37a58 100644 --- a/web/frontend/src/pages/HuyaAccountsPage.tsx +++ b/web/frontend/src/pages/HuyaAccountsPage.tsx @@ -22,6 +22,7 @@ const STATUS_LABELS: Record = { updated: '已更新', password_imported: '待登录', login_success: '登录成功', + password_changed: '已改密', login_failed: '登录失败', active: '正常', invalid: '失效', @@ -32,6 +33,7 @@ const STATUS_COLORS: Record = { updated: 'cyan', password_imported: 'warning', login_success: 'success', + password_changed: 'success', login_failed: 'error', active: 'success', invalid: 'error', diff --git a/web/frontend/src/pages/HuyaRegisterPage.tsx b/web/frontend/src/pages/HuyaRegisterPage.tsx index 462bcf8..be0048b 100644 --- a/web/frontend/src/pages/HuyaRegisterPage.tsx +++ b/web/frontend/src/pages/HuyaRegisterPage.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; 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'; import type { TableProps } from 'antd'; import { DownloadOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons'; @@ -16,6 +16,7 @@ const STATUS_LABELS: Record = { sending: '发码', waiting: '等待验证码', logging: '保存', + changing: '改密', success: '成功', error: '失败', stopped: '已停止', @@ -26,6 +27,7 @@ const STATUS_COLORS: Record = { sending: 'processing', waiting: 'processing', logging: 'processing', + changing: 'processing', success: 'success', error: 'error', stopped: 'warning', @@ -39,6 +41,9 @@ export default function HuyaRegisterPage() { const [concurrency, setConcurrency] = useState(1); const [waitSeconds, setWaitSeconds] = useState(180); 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(null); const [starting, setStarting] = useState(false); const [refreshing, setRefreshing] = useState(false); @@ -69,7 +74,7 @@ export default function HuyaRegisterPage() { }, [batchId, isRunning, refreshBatch]); 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], ); @@ -78,6 +83,10 @@ export default function HuyaRegisterPage() { message.warning('请先粘贴手机号池'); return; } + if (passwordMode === 'fixed' && !fixedPassword.trim()) { + message.warning('请先填写固定密码'); + return; + } setStarting(true); try { const data = await huyaApi.startAutoRegister({ @@ -86,6 +95,8 @@ export default function HuyaRegisterPage() { concurrency, wait_seconds: waitSeconds, poll_interval: pollInterval, + password_prefix: passwordMode === 'random' ? (passwordPrefix.trim() || 'hy') : 'hy', + fixed_password: passwordMode === 'fixed' ? fixedPassword.trim() : '', }); setBatch(data); message.success('自动注册批次已启动'); @@ -112,15 +123,17 @@ export default function HuyaRegisterPage() { const handleExportSuccess = () => { if (successRows.length === 0) { - message.warning('当前批次没有可导出的成功 CK'); + message.warning('当前批次没有可导出的成功账号'); 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 url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = `huya-register-${batchId || 'success'}.txt`; + link.download = `huya-register-accounts-${batchId || 'success'}.txt`; link.click(); URL.revokeObjectURL(url); }; @@ -128,6 +141,17 @@ export default function HuyaRegisterPage() { const columns: TableProps['columns'] = [ { title: '行', dataIndex: 'line', width: 64 }, { 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: '状态', @@ -139,20 +163,18 @@ export default function HuyaRegisterPage() { ), }, - { title: '验证码', dataIndex: 'code', width: 100 }, - { title: '轮询', dataIndex: 'attempts', width: 80 }, + { title: '注册码', dataIndex: 'code', width: 100 }, + { title: '改密码', dataIndex: 'change_code', width: 100 }, + { + title: '轮询', + width: 90, + render: (_, item) => `${item.attempts}/${item.change_attempts}`, + }, { title: '账号', width: 160, render: (_, item) => item.uid || (item.account_id ? `#${item.account_id}` : '-'), }, - { - title: 'Cookie', - dataIndex: 'cookie_preview', - width: 220, - ellipsis: true, - render: (value: string) => value || '-', - }, { title: '消息', dataIndex: 'message', @@ -223,6 +245,35 @@ export default function HuyaRegisterPage() { style={{ width: '100%' }} /> + + 密码设置 + + setPasswordMode(value as 'random' | 'fixed')} + disabled={isRunning} + /> + {passwordMode === 'random' ? ( + setPasswordPrefix(event.target.value)} + disabled={isRunning} + /> + ) : ( + setFixedPassword(event.target.value)} + disabled={isRunning} + /> + )} + + )} @@ -276,7 +327,7 @@ export default function HuyaRegisterPage() { dataSource={batch?.items || []} loading={refreshing && !isRunning} pagination={{ pageSize: 20, showSizeChanger: true }} - scroll={{ x: 1280 }} + scroll={{ x: 1320 }} />