From 45dd7e5a7443769761d2379403c0c0ccec28f076 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 6 Jul 2026 11:45:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=BC=E5=AE=B9=E8=99=8E=E7=89=99=E5=9B=9B?= =?UTF-8?q?=E6=AE=B5=E8=B4=A6=E5=8F=B7=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持虎牙号、密码、手机号、验证码链接导入,并补充美国等国际手机号格式处理。 --- core/huya/sms_login.py | 135 +++++++++++++++++++- core/sms_provider.py | 25 +++- web/backend/routers/huya.py | 2 +- web/backend/services/huya_service.py | 53 +++++++- web/frontend/src/pages/HuyaAccountsPage.tsx | 4 +- web/frontend/src/pages/HuyaRegisterPage.tsx | 9 +- 6 files changed, 203 insertions(+), 25 deletions(-) diff --git a/core/huya/sms_login.py b/core/huya/sms_login.py index 4a40ae0..620c3eb 100644 --- a/core/huya/sms_login.py +++ b/core/huya/sms_login.py @@ -36,6 +36,108 @@ SMS_CODE_URL = "https://udblgn.huya.com/web/v2/smsCode" SMS_LOGIN_URL = "https://udblgn.huya.com/web/v2/smsLogin" DF_TOKEN_URL = "https://df.huya.com/web/df/token" DF_COLLECT_URL = "https://df.huya.com/web/df/collect" +HUYA_COUNTRY_CALLING_CODES = frozenset({ + "1", + "7", + "20", + "27", + "30", + "31", + "32", + "33", + "34", + "36", + "39", + "40", + "41", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "52", + "55", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "81", + "82", + "84", + "86", + "90", + "91", + "92", + "93", + "94", + "95", + "98", + "212", + "213", + "216", + "218", + "234", + "254", + "351", + "352", + "353", + "354", + "355", + "356", + "357", + "358", + "359", + "370", + "371", + "372", + "373", + "374", + "375", + "376", + "377", + "378", + "380", + "381", + "382", + "385", + "386", + "387", + "389", + "420", + "421", + "852", + "853", + "855", + "856", + "886", + "960", + "961", + "962", + "963", + "964", + "965", + "966", + "967", + "968", + "971", + "972", + "973", + "974", + "975", + "976", + "977", + "992", + "993", + "994", + "995", + "996", + "998", +}) @dataclass @@ -52,13 +154,36 @@ class HuyaSmsCodeResult: request_id: str = "" +def _split_country_calling_code(digits: str) -> tuple[str, str]: + """从国际号码中拆出国家/地区区号。""" + for size in (3, 2, 1): + code = digits[:size] + if code in HUYA_COUNTRY_CALLING_CODES and len(digits) > size: + return code, digits[size:] + return "", digits + + +def _format_huya_international_phone(code: str, national_number: str) -> str: + """转换成虎牙短信接口使用的三位区号格式。""" + return f"{code.zfill(3)}{national_number}" + + def normalize_huya_phone(phone: str) -> str: - """归一化虎牙短信接口手机号格式。""" + """归一化虎牙短信接口手机号格式,兼容国内号和显式国际区号。""" raw = str(phone or "").strip() digits = "".join(ch for ch in raw if ch.isdigit()) - if raw.startswith("+1") and len(digits) == 11: - return f"001{digits[1:]}" - if digits.startswith("001") and len(digits) == 13: + if not digits: + return "" + if raw.startswith("+"): + code, national_number = _split_country_calling_code(digits) + if code: + return _format_huya_international_phone(code, national_number) + if digits.startswith("00") and len(digits) > 4: + code, national_number = _split_country_calling_code(digits[2:]) + if code: + return _format_huya_international_phone(code, national_number) + return digits + if digits.startswith("0") and len(digits) > 11: return digits if digits.startswith("086"): return digits @@ -66,6 +191,8 @@ def normalize_huya_phone(phone: str) -> str: return f"0{digits}" if len(digits) == 11: return f"086{digits}" + if len(digits) == 10: + return f"001{digits}" return digits diff --git a/core/sms_provider.py b/core/sms_provider.py index 0e19636..6e29b31 100644 --- a/core/sms_provider.py +++ b/core/sms_provider.py @@ -6,6 +6,7 @@ import json import re from dataclasses import dataclass from datetime import datetime +from urllib.parse import urlparse import requests @@ -42,24 +43,36 @@ class SmsPollResult: def parse_sms_lines(text: str) -> list[SmsLine]: - """解析手机号池,每行格式:手机号----查询URL。""" + """解析手机号池,兼容手机号----查询URL、账号----密码----手机号----查询URL。""" rows: list[SmsLine] = [] for raw_line in (text or "").splitlines(): line = raw_line.strip() if not line or line.startswith("#"): continue - parts = [part.strip() for part in line.split("----", 1)] - if len(parts) != 2 or not parts[0] or not parts[1]: + parts = [part.strip() for part in line.split("----")] + if len(parts) >= 4 and _looks_like_url("----".join(parts[3:])): + phone, url = parts[2], "----".join(parts[3:]).strip() + elif len(parts) >= 2 and _looks_like_url("----".join(parts[1:])): + phone, url = parts[0], "----".join(parts[1:]).strip() + else: + continue + if not phone or not url: continue rows.append(SmsLine( - phone=parts[0], - url=parts[1], - provider=detect_provider(parts[1]), + phone=phone, + url=url, + provider=detect_provider(url), raw=line, )) return rows +def _looks_like_url(value: str) -> bool: + """判断文本是否像 URL。""" + parsed = urlparse(str(value or "").strip()) + return parsed.scheme in {"http", "https"} and bool(parsed.netloc) + + def detect_provider(url: str) -> str: """根据 URL 判断接码平台类型。""" text = (url or "").lower() diff --git a/web/backend/routers/huya.py b/web/backend/routers/huya.py index 1a3add3..709a302 100644 --- a/web/backend/routers/huya.py +++ b/web/backend/routers/huya.py @@ -351,7 +351,7 @@ def create_auto_register_batch( _require_huya_perm(current, "huya:import") sms_lines = parse_sms_lines(req.text) if not sms_lines: - raise HTTPException(status_code=400, detail="没有识别到有效手机号,格式为:手机号----短信查询URL") + raise HTTPException(status_code=400, detail="没有识别到有效手机号,格式为:手机号----短信查询URL 或 虎牙号----密码----手机号----短信查询URL") proxy_config = db.query(ProxyConfig).first() if req.use_proxy else None runner = huya_register_registry.create( diff --git a/web/backend/services/huya_service.py b/web/backend/services/huya_service.py index 8b9b74e..bcb24f6 100644 --- a/web/backend/services/huya_service.py +++ b/web/backend/services/huya_service.py @@ -2,7 +2,9 @@ import csv import uuid +from dataclasses import dataclass from datetime import datetime, timezone +from urllib.parse import urlparse from sqlalchemy.orm import Session @@ -94,8 +96,25 @@ def parse_huya_cookie_line(line: str) -> dict | None: } -def split_huya_password_line(line: str) -> tuple[str, str, str] | None: - """拆分虎牙账号密码行,返回账号、密码、预置 Cookie。""" +@dataclass +class HuyaPasswordLine: + """虎牙账号密码导入行。""" + + username: str + password: str + cookie: str = "" + phone: str = "" + sms_url: str = "" + + +def _looks_like_url(value: str) -> bool: + """判断文本是否像 URL。""" + parsed = urlparse(str(value or "").strip()) + return parsed.scheme in {"http", "https"} and bool(parsed.netloc) + + +def split_huya_password_line(line: str) -> HuyaPasswordLine | None: + """拆分虎牙账号密码行,兼容账号----密码----手机号----验证码链接。""" raw = (line or "").strip() if not raw or raw.startswith("#"): return None @@ -116,10 +135,29 @@ def split_huya_password_line(line: str) -> tuple[str, str, str] | None: username = parts[0].strip() password = parts[1].strip() - cookie = "----".join(part.strip() for part in parts[2:] if part.strip()) + cookie = "" + phone = "" + sms_url = "" + extra = [part.strip() for part in parts[2:] if part.strip()] + if len(extra) >= 2 and _looks_like_url(extra[1]): + phone = extra[0] + sms_url = extra[1] + cookie = "----".join(extra[2:]) + elif extra and _looks_like_url(extra[-1]) and len(extra) >= 2: + phone = extra[-2] + sms_url = extra[-1] + cookie = "----".join(extra[:-2]) + elif extra: + cookie = "----".join(extra) if not username or not password: return None - return username, password, cookie + return HuyaPasswordLine( + username=username, + password=password, + cookie=cookie, + phone=phone, + sms_url=sms_url, + ) def import_huya_password_accounts(db: Session, text: str, tag: str = "") -> tuple[int, int]: @@ -135,7 +173,9 @@ def import_huya_password_accounts(db: Session, text: str, tag: str = "") -> tupl skipped += 1 continue - username, password, cookie = parsed + username = parsed.username + password = parsed.password + cookie = parsed.cookie account = db.query(HuyaAccount).filter(HuyaAccount.username == username).first() if account is None: account = HuyaAccount( @@ -144,6 +184,7 @@ def import_huya_password_accounts(db: Session, text: str, tag: str = "") -> tupl username=username, account_password=password, cookie=normalize_huya_cookie(cookie) if cookie else "", + game_phone=parsed.phone, tag=tag, status="password_imported", ) @@ -152,6 +193,8 @@ def import_huya_password_accounts(db: Session, text: str, tag: str = "") -> tupl account.account_password = password if cookie: account.cookie = normalize_huya_cookie(cookie) + if parsed.phone: + account.game_phone = parsed.phone if tag: account.tag = tag if not account.cookie: diff --git a/web/frontend/src/pages/HuyaAccountsPage.tsx b/web/frontend/src/pages/HuyaAccountsPage.tsx index fa37a58..ab19f79 100644 --- a/web/frontend/src/pages/HuyaAccountsPage.tsx +++ b/web/frontend/src/pages/HuyaAccountsPage.tsx @@ -716,7 +716,7 @@ export default function HuyaAccountsPage() { rows={10} value={passwordImportText} onChange={(e) => setPasswordImportText(e.target.value)} - placeholder="每行一条:账号----密码;如需预置 Cookie:账号----密码----Cookie" + placeholder="每行一条:账号----密码;也支持:虎牙号----密码----手机号----验证码链接" disabled={passwordImporting} />