添加虎牙短信登录

This commit is contained in:
yml2213
2026-07-05 23:10:50 +08:00
parent 3928b43460
commit 3f50d962a0
7 changed files with 742 additions and 2 deletions
+80 -1
View File
@@ -11,7 +11,13 @@ from fastapi.responses import StreamingResponse
from sqlalchemy import func
from sqlalchemy.orm import Session, joinedload
from core.huya import HuyaCredentialError, HuyaLoginError, login_huya_password
from core.huya import (
HuyaCredentialError,
HuyaLoginError,
login_huya_password,
login_huya_sms,
send_huya_sms_code,
)
from core.huya.cookie_utils import normalize_huya_cookie
from ..database import SessionLocal, get_db
@@ -31,6 +37,8 @@ from ..schemas import (
HuyaPasswordLoginRequest,
HuyaPasswordLoginSelectedRequest,
HuyaRechargeGoodsOut,
HuyaSmsCodeRequest,
HuyaSmsLoginRequest,
HuyaTaskBatchRequest,
HuyaTaskOut,
)
@@ -258,6 +266,77 @@ def password_login_account(
}
@router.post("/accounts/sms-code")
def send_sms_code(
req: HuyaSmsCodeRequest,
current: User = Depends(get_current_user),
):
"""发送虎牙短信验证码,返回提交登录所需 state。"""
_require_huya_perm(current, "huya:import")
try:
result = send_huya_sms_code(
phone=req.phone.strip(),
cookie=req.cookie.strip() or None,
)
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
if not result.success or not result.state:
raise HTTPException(status_code=502, detail=result.message or "虎牙短信发码失败")
return {
"message": result.message or "短信已发送",
"success": True,
"state": result.state,
"sdid": result.sdid,
"context": result.context,
"request_id": result.request_id,
}
@router.post("/accounts/sms-login")
def sms_login_account(
req: HuyaSmsLoginRequest,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""提交虎牙短信验证码登录,成功后保存 Cookie。"""
_require_huya_perm(current, "huya:import")
try:
result = login_huya_sms(
authcode=req.authcode.strip(),
state=req.state.strip(),
phone=req.phone.strip(),
)
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
if not result.success or not result.cookie:
raise HTTPException(status_code=502, detail=result.message or "虎牙短信登录失败")
try:
account = upsert_huya_cookie(db, result.cookie, tag=req.tag, username_hint="")
phone = (req.phone or "").strip()
if phone:
account.game_phone = phone
account.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(account)
except ValueError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return {
"message": "登录成功,Cookie 已保存",
"success": True,
"account": _account_out(account),
"sdid": result.sdid,
}
@router.post("/accounts/password-login/selected")
def password_login_selected_accounts(
req: HuyaPasswordLoginSelectedRequest,
+14
View File
@@ -183,6 +183,20 @@ class HuyaPasswordLoginRequest(BaseModel):
cookie: str = ""
class HuyaSmsCodeRequest(BaseModel):
"""发送虎牙短信验证码。"""
phone: str = Field(..., min_length=5, max_length=32)
cookie: str = ""
class HuyaSmsLoginRequest(BaseModel):
"""提交虎牙短信验证码并保存 Cookie。"""
authcode: str = Field(..., min_length=4, max_length=8)
state: str = Field(..., min_length=1)
phone: str = ""
tag: str = ""
class HuyaPasswordAccountImport(BaseModel):
"""导入虎牙账号密码,稍后再选择登录。"""
text: str = Field(..., min_length=1)
+8
View File
@@ -11,6 +11,10 @@ import type {
HuyaPasswordLoginResult,
HuyaPasswordLoginSelectedRequest,
HuyaRechargeGoodsItem,
HuyaSmsCodeRequest,
HuyaSmsCodeResult,
HuyaSmsLoginRequest,
HuyaSmsLoginResult,
HuyaTaskBatchRequest,
HuyaTaskBatchResult,
HuyaTaskItem,
@@ -31,6 +35,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),
smsCode: (data: HuyaSmsCodeRequest) =>
api.post<HuyaSmsCodeResult, HuyaSmsCodeResult>('/huya/accounts/sms-code', data, { timeout: 120000 }),
smsLogin: (data: HuyaSmsLoginRequest) =>
api.post<HuyaSmsLoginResult, HuyaSmsLoginResult>('/huya/accounts/sms-login', data, { timeout: 120000 }),
assign: (id: number, assigned_to: number | null) =>
api.put<MessageResponse, MessageResponse>(`/huya/accounts/${id}/assign`, { assigned_to }),
batchAssign: (account_ids: number[], assigned_to: number | null) =>
+24
View File
@@ -149,6 +149,30 @@ export interface HuyaPasswordLoginResult extends MessageResponse {
sdid: string;
}
export interface HuyaSmsCodeRequest {
phone: string;
cookie?: string;
}
export interface HuyaSmsCodeResult extends MessageResponse {
state: string;
sdid: string;
context: string;
request_id: string;
}
export interface HuyaSmsLoginRequest {
authcode: string;
state: string;
phone?: string;
tag?: string;
}
export interface HuyaSmsLoginResult extends MessageResponse {
account: HuyaAccountItem;
sdid: string;
}
export interface HuyaPasswordLoginBatchItem {
line: number;
username: string;
+134 -1
View File
@@ -3,7 +3,7 @@ import {
Button, Card, Col, Input, message, Modal, Popconfirm, Row, Select, Space, Statistic, Table, Tag, Typography,
} from 'antd';
import type { TableProps } from 'antd';
import { DeleteOutlined, FilterOutlined, ImportOutlined, LoginOutlined, ReloadOutlined, SearchOutlined, TagOutlined } from '@ant-design/icons';
import { DeleteOutlined, FilterOutlined, ImportOutlined, LoginOutlined, MobileOutlined, ReloadOutlined, SearchOutlined, TagOutlined } from '@ant-design/icons';
import {
huyaApi,
type HuyaAccountItem,
@@ -53,6 +53,13 @@ export default function HuyaAccountsPage() {
const [passwordLoginResultOpen, setPasswordLoginResultOpen] = useState(false);
const [passwordLogging, setPasswordLogging] = useState(false);
const [passwordLoginResults, setPasswordLoginResults] = useState<HuyaPasswordLoginBatchItem[]>([]);
const [smsLoginOpen, setSmsLoginOpen] = useState(false);
const [smsPhone, setSmsPhone] = useState('');
const [smsCode, setSmsCode] = useState('');
const [smsTag, setSmsTag] = useState<string[]>([]);
const [smsState, setSmsState] = useState('');
const [smsSending, setSmsSending] = useState(false);
const [smsLogging, setSmsLogging] = useState(false);
const [searchText, setSearchText] = useState('');
const [tagFilter, setTagFilter] = useState('');
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
@@ -159,6 +166,67 @@ export default function HuyaAccountsPage() {
setPasswordImportOpen(true);
};
const openSmsLogin = () => {
setSmsPhone('');
setSmsCode('');
setSmsTag([]);
setSmsState('');
setSmsLoginOpen(true);
};
const handleSendSmsCode = async () => {
const phone = smsPhone.trim();
if (!phone) {
message.warning('请先输入手机号');
return;
}
setSmsSending(true);
try {
const result = await huyaApi.smsCode({ phone });
setSmsState(result.state);
message.success(result.message || '短信已发送');
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setSmsSending(false);
}
};
const handleSmsLogin = async () => {
const phone = smsPhone.trim();
const authcode = smsCode.trim();
if (!smsState) {
message.warning('请先发送短信验证码');
return;
}
if (!authcode) {
message.warning('请先输入短信验证码');
return;
}
setSmsLogging(true);
try {
const tag = smsTag.length > 0 ? smsTag[smsTag.length - 1].trim() : '';
const result = await huyaApi.smsLogin({
phone,
authcode,
state: smsState,
tag,
});
message.success(result.message || '登录成功,Cookie 已保存');
setSmsLoginOpen(false);
setSmsPhone('');
setSmsCode('');
setSmsTag([]);
setSmsState('');
loadAccounts();
loadTags();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setSmsLogging(false);
}
};
const handleImportPasswordAccounts = async () => {
if (!passwordImportText.trim()) {
message.warning('请先粘贴虎牙账号密码');
@@ -491,6 +559,15 @@ export default function HuyaAccountsPage() {
</Button>
</Popconfirm>
)}
{canImport && (
<Button
icon={<MobileOutlined />}
loading={smsSending || smsLogging}
onClick={openSmsLogin}
>
</Button>
)}
{canImport && (
<Button icon={<ImportOutlined />} onClick={openPasswordImport}>
@@ -656,6 +733,62 @@ export default function HuyaAccountsPage() {
</Space>
</Modal>
<Modal
title="虎牙短信登录"
open={smsLoginOpen}
onCancel={() => {
if (smsSending || smsLogging) return;
setSmsLoginOpen(false);
}}
footer={[
<Button key="cancel" disabled={smsSending || smsLogging} onClick={() => setSmsLoginOpen(false)}>
</Button>,
<Button key="send" icon={<MobileOutlined />} loading={smsSending} disabled={smsLogging} onClick={handleSendSmsCode}>
{smsState ? '重新发码' : '发送短信'}
</Button>,
<Button key="login" type="primary" icon={<LoginOutlined />} loading={smsLogging} disabled={!smsState || smsSending} onClick={handleSmsLogin}>
</Button>,
]}
maskClosable={!(smsSending || smsLogging)}
closable={!(smsSending || smsLogging)}
width={520}
>
<Space direction="vertical" style={{ width: '100%' }} size={12}>
<Input
value={smsPhone}
onChange={(e) => setSmsPhone(e.target.value)}
placeholder="手机号"
disabled={smsSending || smsLogging || Boolean(smsState)}
prefix={<MobileOutlined />}
/>
<Input
value={smsCode}
onChange={(e) => setSmsCode(e.target.value.replace(/\D/g, '').slice(0, 8))}
placeholder="短信验证码"
maxLength={8}
disabled={!smsState || smsSending || smsLogging}
onPressEnter={handleSmsLogin}
/>
<Select
mode="tags"
style={{ width: '100%' }}
placeholder="可选,保存到账号标签"
maxCount={1}
value={smsTag}
onChange={setSmsTag}
options={tags.map((tag) => ({ value: tag, label: tag }))}
disabled={smsSending || smsLogging}
/>
{smsState ? (
<Text type="secondary"></Text>
) : (
<Text type="secondary"></Text>
)}
</Space>
</Modal>
<Modal
title="虎牙登录结果"
open={passwordLoginResultOpen}