Files
NapCatQQ/packages/napcat-webui-frontend/src/components/new_device_verify.tsx
手瓜一十雪 f9764c9559 Improve new-device QR handling and bypass init
Refactor new-device QR flow and streamline bypass init:

- napcat-shell: stop verbose logging and removed check of enableAllBypasses return value; just invoke native enableAllBypasses when not disabled by env.
- backend (QQLogin): simplify extraction of tokens from jumpUrl (use sig and uin-token), return an error if missing, and send oidbRequest directly (removed nested try/catch and regex fallback).
- frontend (new_device_verify): accept result.str_url without requiring bytes_token and pass an empty string to polling when bytes_token is absent.
- frontend (password_login): change render order to show captcha modal before new-device verification UI.
- frontend (qq_manager): normalize GetNewDeviceQRCode response — derive bytes_token from str_url's str_url query param (base64) when bytes_token is missing, and preserve extra status/error fields in the returned object.

These changes improve robustness when OIDB responses omit bytes_token, reduce noisy logs, and ensure the UI and polling still function.
2026-02-21 13:24:56 +08:00

159 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState, useCallback } from 'react';
import { Button } from '@heroui/button';
import { Spinner } from '@heroui/spinner';
import { QRCodeSVG } from 'qrcode.react';
import QQManager from '@/controllers/qq_manager';
interface NewDeviceVerifyProps {
/** jumpUrl from loginErrorInfo */
jumpUrl: string;
/** QQ uin for OIDB requests */
uin: string;
/** Called when QR verification is confirmed, passes str_nt_succ_token */
onVerified: (token: string) => void;
/** Called when user cancels */
onCancel?: () => void;
}
type QRStatus = 'loading' | 'waiting' | 'scanned' | 'confirmed' | 'error';
const NewDeviceVerify: React.FC<NewDeviceVerifyProps> = ({
jumpUrl,
uin,
onVerified,
onCancel,
}) => {
const [qrUrl, setQrUrl] = useState<string>('');
const [status, setStatus] = useState<QRStatus>('loading');
const [errorMsg, setErrorMsg] = useState<string>('');
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const mountedRef = useRef(true);
const stopPolling = useCallback(() => {
if (pollTimerRef.current) {
clearInterval(pollTimerRef.current);
pollTimerRef.current = null;
}
}, []);
const startPolling = useCallback((token: string) => {
stopPolling();
pollTimerRef.current = setInterval(async () => {
if (!mountedRef.current) return;
try {
const result = await QQManager.pollNewDeviceQR(uin, token);
if (!mountedRef.current) return;
const s = result?.uint32_guarantee_status;
if (s === 3) {
setStatus('scanned');
} else if (s === 1) {
stopPolling();
setStatus('confirmed');
const ntToken = result?.str_nt_succ_token || '';
onVerified(ntToken);
}
// s === 0 means still waiting, do nothing
} catch {
// Ignore poll errors, keep polling
}
}, 2500);
}, [uin, onVerified, stopPolling]);
const fetchQRCode = useCallback(async () => {
setStatus('loading');
setErrorMsg('');
try {
const result = await QQManager.getNewDeviceQRCode(uin, jumpUrl);
if (!mountedRef.current) return;
if (result?.str_url) {
setQrUrl(result.str_url);
setStatus('waiting');
// bytes_token 用于轮询,如果 OIDB 未返回则用空字符串
startPolling(result.bytes_token || '');
} else {
setStatus('error');
setErrorMsg('获取二维码失败,请重试');
}
} catch (e) {
if (!mountedRef.current) return;
setStatus('error');
setErrorMsg((e as Error).message || '获取二维码失败');
}
}, [uin, jumpUrl, startPolling]);
useEffect(() => {
mountedRef.current = true;
fetchQRCode();
return () => {
mountedRef.current = false;
stopPolling();
};
}, [fetchQRCode, stopPolling]);
const statusText: Record<QRStatus, string> = {
loading: '正在获取二维码...',
waiting: '请使用手机QQ扫描二维码完成验证',
scanned: '已扫描,请在手机上确认',
confirmed: '验证成功,正在登录...',
error: errorMsg || '获取二维码失败',
};
const statusColor: Record<QRStatus, string> = {
loading: 'text-default-500',
waiting: 'text-warning',
scanned: 'text-primary',
confirmed: 'text-success',
error: 'text-danger',
};
return (
<div className='flex flex-col gap-4 items-center'>
<p className='text-warning text-sm'>
使QQ扫描下方二维码完成验证
</p>
<div className='flex flex-col items-center gap-3' style={{ minHeight: 280 }}>
{status === 'loading' ? (
<div className='flex items-center justify-center' style={{ height: 240 }}>
<Spinner size='lg' />
</div>
) : status === 'error' ? (
<div className='flex flex-col items-center justify-center gap-3' style={{ height: 240 }}>
<p className='text-danger text-sm'>{errorMsg}</p>
<Button color='primary' variant='flat' onPress={fetchQRCode}>
</Button>
</div>
) : (
<div className='p-3 bg-white rounded-lg'>
<QRCodeSVG value={qrUrl} size={220} />
</div>
)}
<p className={`text-sm ${statusColor[status]}`}>
{statusText[status]}
</p>
</div>
<div className='flex gap-3'>
{status === 'waiting' && (
<Button color='default' variant='flat' size='sm' onPress={fetchQRCode}>
</Button>
)}
<Button
variant='light'
color='danger'
size='sm'
onPress={onCancel}
>
</Button>
</div>
</div>
);
};
export default NewDeviceVerify;