feat(huya): 整合 App 协议登录获取 Cookie 全链路并与 Web 登录解耦
- core/huya: 新增 app_login, wup_encoder, nonce_forge, cert_forge, envelope_forge, device_profile, udb_aes - core/huya/__init__.py: 导出 login_huya_app_password 与 HuyaAppPasswordLogin - web/backend: 新增 /accounts/app-password-login 与 /accounts/app-password-login/selected 路由及 Schema,与原 Web 密码登录独立分开 - web/frontend: 增加 App 登录 API 与前端界面“App 登录选中”操作,弹窗结果明确区分 - tests: 新增 test_huya_app_login.py 单元测试覆盖全链路
This commit is contained in:
+200
-5
@@ -12,8 +12,11 @@ from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session, defer, joinedload
|
||||
|
||||
from core.huya import (
|
||||
HuyaAppLoginError,
|
||||
HuyaAppQrAuthRequiredError,
|
||||
HuyaCredentialError,
|
||||
HuyaLoginError,
|
||||
login_huya_app_password,
|
||||
login_huya_password,
|
||||
login_huya_sms,
|
||||
send_huya_sms_code,
|
||||
@@ -51,6 +54,7 @@ from ..schemas import (
|
||||
HuyaGoodsOut,
|
||||
HuyaPasswordAccountImport,
|
||||
HuyaPasswordLoginRequest,
|
||||
HuyaAppPasswordLoginRequest,
|
||||
HuyaPasswordLoginSelectedRequest,
|
||||
HuyaRechargeGoodsOut,
|
||||
HuyaRegisterSuccessLogOut,
|
||||
@@ -528,7 +532,7 @@ def password_login_account(
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""使用账号密码登录虎牙,成功后保存 Cookie。"""
|
||||
"""使用 Web 方式账号密码登录虎牙(旧版),成功后保存 Cookie。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
try:
|
||||
result = login_huya_password(
|
||||
@@ -541,17 +545,63 @@ def password_login_account(
|
||||
except HuyaLoginError as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=f"虎牙密码登录失败: {exc}") from exc
|
||||
raise HTTPException(status_code=502, detail=f"虎牙 Web 密码登录失败: {exc}") from exc
|
||||
|
||||
if not result.success or not result.cookie:
|
||||
raise HTTPException(status_code=502, detail=result.message or "虎牙密码登录失败")
|
||||
raise HTTPException(status_code=502, detail=result.message or "虎牙 Web 密码登录失败")
|
||||
|
||||
try:
|
||||
account = upsert_huya_cookie(db, result.cookie, tag=req.tag, username_hint=req.username)
|
||||
if hasattr(account, "account_password") and req.password:
|
||||
account.account_password = req.password
|
||||
account.status = "active"
|
||||
db.commit()
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
return {
|
||||
"message": "登录成功,Cookie 已保存",
|
||||
"message": "Web 登录成功,Cookie 已保存",
|
||||
"success": True,
|
||||
"account": _account_out(account),
|
||||
"sdid": result.sdid,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/accounts/app-password-login")
|
||||
def app_password_login_account(
|
||||
req: HuyaAppPasswordLoginRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""使用 App 协议密码登录虎牙(推荐),自动过滑块并保存 Cookie。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
try:
|
||||
result = login_huya_app_password(
|
||||
username=req.username.strip(),
|
||||
password=req.password,
|
||||
force_new_device=req.force_new_device,
|
||||
)
|
||||
except HuyaAppQrAuthRequiredError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except HuyaCredentialError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except (HuyaAppLoginError, HuyaLoginError) as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=f"虎牙 App 密码登录失败: {exc}") from exc
|
||||
|
||||
if not result.success or not result.cookie:
|
||||
raise HTTPException(status_code=502, detail=result.message or "虎牙 App 密码登录失败")
|
||||
|
||||
try:
|
||||
account = upsert_huya_cookie(db, result.cookie, tag=req.tag, username_hint=req.username)
|
||||
if hasattr(account, "account_password") and req.password:
|
||||
account.account_password = req.password
|
||||
account.status = "active"
|
||||
db.commit()
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
return {
|
||||
"message": "App 协议登录成功,Cookie 已保存",
|
||||
"success": True,
|
||||
"account": _account_out(account),
|
||||
"sdid": result.sdid,
|
||||
@@ -918,7 +968,152 @@ def password_login_selected_accounts(
|
||||
})
|
||||
|
||||
return {
|
||||
"message": f"登录完成:成功 {success_count} 条,失败 {failed_count} 条",
|
||||
"message": f"Web 登录完成:成功 {success_count} 条,失败 {failed_count} 条",
|
||||
"success": True,
|
||||
"count": success_count,
|
||||
"failed": failed_count,
|
||||
"results": results,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/accounts/app-password-login/selected")
|
||||
def app_password_login_selected_accounts(
|
||||
req: HuyaPasswordLoginSelectedRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""对已导入的虎牙账号使用 App 协议批量执行密码登录(推荐)。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
selected_ids = _selected_huya_account_ids(db, current, req)
|
||||
if not selected_ids:
|
||||
raise HTTPException(status_code=400, detail="请选择虎牙账号")
|
||||
|
||||
accounts = (
|
||||
_visible_huya_accounts_query(db, current)
|
||||
.filter(HuyaAccount.id.in_(selected_ids))
|
||||
.all()
|
||||
)
|
||||
account_map = {account.id: account for account in accounts}
|
||||
results = []
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
include_cookie = _can_view_huya_cookie(current)
|
||||
|
||||
for account_id in selected_ids:
|
||||
account = account_map.get(account_id)
|
||||
if account is None:
|
||||
failed_count += 1
|
||||
results.append({
|
||||
"line": account_id,
|
||||
"username": "",
|
||||
"success": False,
|
||||
"message": "账号不存在或无权登录",
|
||||
})
|
||||
continue
|
||||
|
||||
username = (account.username or "").strip()
|
||||
password = (account.account_password or "").strip()
|
||||
if not username or not password:
|
||||
failed_count += 1
|
||||
results.append({
|
||||
"line": account.id,
|
||||
"username": username,
|
||||
"success": False,
|
||||
"message": "该账号未导入密码",
|
||||
"account": _account_out(account, include_cookie=include_cookie),
|
||||
})
|
||||
continue
|
||||
|
||||
try:
|
||||
result = login_huya_app_password(
|
||||
username=username,
|
||||
password=password,
|
||||
force_new_device=req.force_new_device,
|
||||
)
|
||||
if not result.success or not result.cookie:
|
||||
account.status = "login_failed"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
failed_count += 1
|
||||
results.append({
|
||||
"line": account.id,
|
||||
"username": username,
|
||||
"success": False,
|
||||
"message": result.message or "虎牙 App 密码登录失败",
|
||||
"account": _account_out(account, include_cookie=include_cookie),
|
||||
"sdid": result.sdid,
|
||||
})
|
||||
continue
|
||||
|
||||
saved = save_huya_login_cookie_to_account(
|
||||
db,
|
||||
account,
|
||||
result.cookie,
|
||||
tag=account.tag or "",
|
||||
username_hint=username,
|
||||
)
|
||||
saved.status = "active"
|
||||
db.commit()
|
||||
success_count += 1
|
||||
results.append({
|
||||
"line": account.id,
|
||||
"username": username,
|
||||
"success": True,
|
||||
"message": "App 登录成功,Cookie 已保存",
|
||||
"account": _account_out(saved, include_cookie=include_cookie),
|
||||
"sdid": result.sdid,
|
||||
})
|
||||
except HuyaAppQrAuthRequiredError as exc:
|
||||
account.status = "login_failed"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
failed_count += 1
|
||||
results.append({
|
||||
"line": account.id,
|
||||
"username": username,
|
||||
"success": False,
|
||||
"message": str(exc),
|
||||
"account": _account_out(account, include_cookie=include_cookie),
|
||||
})
|
||||
except HuyaCredentialError as exc:
|
||||
account.status = "login_failed"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
failed_count += 1
|
||||
results.append({
|
||||
"line": account.id,
|
||||
"username": username,
|
||||
"success": False,
|
||||
"message": str(exc),
|
||||
"account": _account_out(account, include_cookie=include_cookie),
|
||||
})
|
||||
except (HuyaAppLoginError, HuyaLoginError, ValueError) as exc:
|
||||
account.status = "login_failed"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
failed_count += 1
|
||||
results.append({
|
||||
"line": account.id,
|
||||
"username": username,
|
||||
"success": False,
|
||||
"message": str(exc),
|
||||
"account": _account_out(account, include_cookie=include_cookie),
|
||||
})
|
||||
except Exception as exc:
|
||||
account.status = "login_failed"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
failed_count += 1
|
||||
results.append({
|
||||
"line": account.id,
|
||||
"username": username,
|
||||
"success": False,
|
||||
"message": f"虎牙 App 密码登录失败: {exc}",
|
||||
"account": _account_out(account, include_cookie=include_cookie),
|
||||
})
|
||||
|
||||
return {
|
||||
"message": f"App 登录完成:成功 {success_count} 条,失败 {failed_count} 条",
|
||||
"success": True,
|
||||
"count": success_count,
|
||||
"failed": failed_count,
|
||||
|
||||
+10
-1
@@ -261,13 +261,21 @@ class HuyaCookieImport(BaseModel):
|
||||
|
||||
|
||||
class HuyaPasswordLoginRequest(BaseModel):
|
||||
"""虎牙账号密码登录并保存 Cookie。"""
|
||||
"""虎牙账号 Web 密码登录并保存 Cookie(旧版)。"""
|
||||
username: str = Field(..., min_length=1, max_length=128)
|
||||
password: str = Field(..., min_length=1, max_length=128)
|
||||
tag: str = ""
|
||||
cookie: str = ""
|
||||
|
||||
|
||||
class HuyaAppPasswordLoginRequest(BaseModel):
|
||||
"""虎牙账号 App 协议密码登录并保存 Cookie(推荐)。"""
|
||||
username: str = Field(..., min_length=1, max_length=128)
|
||||
password: str = Field(..., min_length=1, max_length=128)
|
||||
tag: str = ""
|
||||
force_new_device: bool = False
|
||||
|
||||
|
||||
class HuyaSmsCodeRequest(BaseModel):
|
||||
"""发送虎牙短信验证码。"""
|
||||
phone: str = Field(..., min_length=5, max_length=32)
|
||||
@@ -386,6 +394,7 @@ class HuyaPasswordLoginSelectedRequest(BaseModel):
|
||||
tag: str = ""
|
||||
assigned_only: bool = False
|
||||
has_cookie: bool = False
|
||||
force_new_device: bool = False
|
||||
|
||||
|
||||
class HuyaAccountOut(BaseModel):
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
HuyaPasswordAccountImportResult,
|
||||
HuyaPasswordLoginBatchResult,
|
||||
HuyaPasswordLoginRequest,
|
||||
HuyaAppPasswordLoginRequest,
|
||||
HuyaPasswordLoginResult,
|
||||
HuyaPasswordLoginSelectedRequest,
|
||||
HuyaRechargeGoodsItem,
|
||||
@@ -49,6 +50,10 @@ export const huyaApi = {
|
||||
api.post<HuyaPasswordLoginResult, HuyaPasswordLoginResult>('/huya/accounts/password-login', data),
|
||||
passwordLoginSelected: (data: HuyaPasswordLoginSelectedRequest) =>
|
||||
api.post<HuyaPasswordLoginBatchResult, HuyaPasswordLoginBatchResult>('/huya/accounts/password-login/selected', data),
|
||||
appPasswordLogin: (data: HuyaAppPasswordLoginRequest) =>
|
||||
api.post<HuyaPasswordLoginResult, HuyaPasswordLoginResult>('/huya/accounts/app-password-login', data),
|
||||
appPasswordLoginSelected: (data: HuyaPasswordLoginSelectedRequest) =>
|
||||
api.post<HuyaPasswordLoginBatchResult, HuyaPasswordLoginBatchResult>('/huya/accounts/app-password-login/selected', data),
|
||||
smsCode: (data: HuyaSmsCodeRequest) =>
|
||||
api.post<HuyaSmsCodeResult, HuyaSmsCodeResult>('/huya/accounts/sms-code', data, { timeout: 120000 }),
|
||||
smsLogin: (data: HuyaSmsLoginRequest) =>
|
||||
|
||||
@@ -478,6 +478,13 @@ export interface HuyaPasswordLoginRequest {
|
||||
cookie?: string;
|
||||
}
|
||||
|
||||
export interface HuyaAppPasswordLoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
tag?: string;
|
||||
force_new_device?: boolean;
|
||||
}
|
||||
|
||||
export interface HuyaPasswordLoginResult extends MessageResponse {
|
||||
account: HuyaAccountItem;
|
||||
sdid: string;
|
||||
@@ -615,6 +622,7 @@ export interface HuyaPasswordLoginSelectedRequest {
|
||||
tag?: string;
|
||||
assigned_only?: boolean;
|
||||
has_cookie?: boolean;
|
||||
force_new_device?: boolean;
|
||||
}
|
||||
|
||||
export interface HuyaCookieItem {
|
||||
|
||||
@@ -56,6 +56,8 @@ export default function HuyaAccountsPage() {
|
||||
const [passwordImporting, setPasswordImporting] = useState(false);
|
||||
const [passwordLoginResultOpen, setPasswordLoginResultOpen] = useState(false);
|
||||
const [passwordLogging, setPasswordLogging] = useState(false);
|
||||
const [appPasswordLogging, setAppPasswordLogging] = useState(false);
|
||||
const [loginModalTitle, setLoginModalTitle] = useState('虎牙 App 协议登录结果 (推荐)');
|
||||
const [passwordLoginResults, setPasswordLoginResults] = useState<HuyaPasswordLoginBatchItem[]>([]);
|
||||
const [smsLoginOpen, setSmsLoginOpen] = useState(false);
|
||||
const [smsPhone, setSmsPhone] = useState('');
|
||||
@@ -291,7 +293,35 @@ export default function HuyaAccountsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginSelected = async () => {
|
||||
const handleAppLoginSelected = async () => {
|
||||
if (selectedCount === 0) {
|
||||
message.warning('请先选择虎牙账号');
|
||||
return;
|
||||
}
|
||||
setAppPasswordLogging(true);
|
||||
try {
|
||||
const result = await huyaApi.appPasswordLoginSelected({
|
||||
...selectionPayload,
|
||||
});
|
||||
setPasswordLoginResults(result.results || []);
|
||||
setLoginModalTitle('虎牙 App 协议登录结果 (推荐)');
|
||||
setPasswordLoginResultOpen(true);
|
||||
if (result.failed > 0) {
|
||||
message.warning(result.message);
|
||||
} else {
|
||||
message.success(result.message || 'App 登录成功,Cookie 已保存');
|
||||
}
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
loadSummary();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setAppPasswordLogging(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWebLoginSelected = async () => {
|
||||
if (selectedCount === 0) {
|
||||
message.warning('请先选择虎牙账号');
|
||||
return;
|
||||
@@ -302,11 +332,12 @@ export default function HuyaAccountsPage() {
|
||||
...selectionPayload,
|
||||
});
|
||||
setPasswordLoginResults(result.results || []);
|
||||
setLoginModalTitle('虎牙 Web 密码登录结果 (旧版)');
|
||||
setPasswordLoginResultOpen(true);
|
||||
if (result.failed > 0) {
|
||||
message.warning(result.message);
|
||||
} else {
|
||||
message.success(result.message || '登录成功,Cookie 已保存');
|
||||
message.success(result.message || 'Web 登录成功,Cookie 已保存');
|
||||
}
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
@@ -574,14 +605,25 @@ export default function HuyaAccountsPage() {
|
||||
批量打标签
|
||||
</Button>
|
||||
)}
|
||||
{canImport && (
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={selectedCount === 0}
|
||||
icon={<MobileOutlined />}
|
||||
loading={appPasswordLogging}
|
||||
onClick={handleAppLoginSelected}
|
||||
>
|
||||
App 登录选中 ({selectedCount})
|
||||
</Button>
|
||||
)}
|
||||
{canImport && (
|
||||
<Button
|
||||
disabled={selectedCount === 0}
|
||||
icon={<LoginOutlined />}
|
||||
loading={passwordLogging}
|
||||
onClick={handleLoginSelected}
|
||||
onClick={handleWebLoginSelected}
|
||||
>
|
||||
登录选中
|
||||
Web 登录 (旧版)
|
||||
</Button>
|
||||
)}
|
||||
{canDelete && selectedCount > 0 && (
|
||||
@@ -861,7 +903,7 @@ export default function HuyaAccountsPage() {
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="虎牙登录结果"
|
||||
title={loginModalTitle}
|
||||
open={passwordLoginResultOpen}
|
||||
onCancel={() => setPasswordLoginResultOpen(false)}
|
||||
footer={[
|
||||
@@ -871,6 +913,13 @@ export default function HuyaAccountsPage() {
|
||||
]}
|
||||
width={860}
|
||||
>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
message="登录说明"
|
||||
description="使用 App 协议登录时,系统已自动识别并通过 safe_auth 乱序滑块;若个别新账号提示“要求扫码验证 (qr_auth)”,则需在手机虎牙 App 上正常登录一次以建立设备信任。"
|
||||
/>
|
||||
{passwordLoginResults.length > 0 && (
|
||||
<Table
|
||||
columns={passwordLoginResultColumns}
|
||||
|
||||
Reference in New Issue
Block a user