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}
/>
- 导入后账号会出现在列表里,勾选需要登录的账号再点“登录选中”。
+ 四段格式会保存手机号;验证码链接用于接码池兼容,不会写入账号表。
diff --git a/web/frontend/src/pages/HuyaRegisterPage.tsx b/web/frontend/src/pages/HuyaRegisterPage.tsx
index b351b7d..73775cc 100644
--- a/web/frontend/src/pages/HuyaRegisterPage.tsx
+++ b/web/frontend/src/pages/HuyaRegisterPage.tsx
@@ -97,12 +97,7 @@ function readStoredBatchId() {
}
export default function HuyaRegisterPage() {
- const initialFormRef = useRef(null);
- if (initialFormRef.current === null) {
- initialFormRef.current = readStoredForm();
- }
- const initialForm = initialFormRef.current;
-
+ const [initialForm] = useState(readStoredForm);
const [text, setText] = useState(initialForm.text);
const [tag, setTag] = useState(initialForm.tag);
const [concurrency, setConcurrency] = useState(initialForm.concurrency);
@@ -311,7 +306,7 @@ export default function HuyaRegisterPage() {
rows={9}
value={text}
onChange={(event) => setText(event.target.value)}
- placeholder="手机号----短信查询URL"
+ placeholder="手机号----短信查询URL;也支持:虎牙号----密码----手机号----验证码链接"
disabled={isRunning}
/>