From f9764c955912f50eeae6543d99c5f9efa08999c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Sat, 21 Feb 2026 13:24:56 +0800 Subject: [PATCH] Improve new-device QR handling and bypass init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/napcat-shell/base.ts | 6 +-- .../napcat-webui-backend/src/api/QQLogin.ts | 49 ++++++++----------- .../src/components/new_device_verify.tsx | 5 +- .../src/components/password_login.tsx | 16 +++--- .../src/controllers/qq_manager.ts | 28 ++++++++++- 5 files changed, 59 insertions(+), 45 deletions(-) diff --git a/packages/napcat-shell/base.ts b/packages/napcat-shell/base.ts index 4e3e5bca..c813a92b 100644 --- a/packages/napcat-shell/base.ts +++ b/packages/napcat-shell/base.ts @@ -541,11 +541,7 @@ export async function NCoreInitShell () { // wrapper.node 加载后立刻启用 Bypass(可通过环境变量禁用) if (process.env['NAPCAT_DISABLE_BYPASS'] !== '1') { const bypassOptions = napcatConfig.bypass ?? {}; - logger.logDebug('[NapCat] Bypass 配置:', bypassOptions); - const bypassEnabled = napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions); - if (bypassEnabled) { - logger.log('[NapCat] Napi2NativeLoader: 已启用Bypass'); - } + napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions); } else { logger.log('[NapCat] Napi2NativeLoader: Bypass已通过环境变量禁用'); } diff --git a/packages/napcat-webui-backend/src/api/QQLogin.ts b/packages/napcat-webui-backend/src/api/QQLogin.ts index b0d5981a..92ff9add 100644 --- a/packages/napcat-webui-backend/src/api/QQLogin.ts +++ b/packages/napcat-webui-backend/src/api/QQLogin.ts @@ -509,37 +509,28 @@ export const QQGetNewDeviceQRCodeHandler: RequestHandler = async (req, res) => { return sendError(res, 'uin and jumpUrl are required'); } - try { - // 从 jumpUrl 中提取 str_url 参数作为 str_dev_auth_token - let strDevAuthToken = ''; - let strUinToken = ''; - try { - const url = new URL(jumpUrl); - strDevAuthToken = url.searchParams.get('str_url') || ''; - strUinToken = url.searchParams.get('str_uin_token') || ''; - } catch { - // 如果 URL 解析失败,尝试正则提取 - const strUrlMatch = jumpUrl.match(/str_url=([^&]*)/); - const uinTokenMatch = jumpUrl.match(/str_uin_token=([^&]*)/); - strDevAuthToken = strUrlMatch ? decodeURIComponent(strUrlMatch[1]) : ''; - strUinToken = uinTokenMatch ? decodeURIComponent(uinTokenMatch[1]) : ''; - } + // 从 jumpUrl 中提取参数 + // jumpUrl 格式: https://accounts.qq.com/safe/verify?...&uin-token=xxx&sig=yyy + // sig -> str_dev_auth_token, uin-token -> str_uin_token + const url = new URL(jumpUrl); + const strDevAuthToken = url.searchParams.get('sig') || ''; + const strUinToken = url.searchParams.get('uin-token') || ''; - const body = { - str_dev_auth_token: strDevAuthToken, - uint32_flag: 1, - uint32_url_type: 0, - str_uin_token: strUinToken, - str_dev_type: 'Windows', - str_dev_name: os.hostname() || 'DESKTOP-NAPCAT', - }; - - const result = await oidbRequest(uin, body); - // result 应包含 str_url (二维码内容) 和 bytes_token 等 - return sendSuccess(res, result); - } catch (e) { - return sendError(res, `Failed to get new device QR code: ${(e as Error).message}`); + if (!strDevAuthToken || !strUinToken) { + return sendError(res, 'Failed to get new device QR code: unable to extract sig/uin-token from jumpUrl'); } + + const body = { + str_dev_auth_token: strDevAuthToken, + uint32_flag: 1, + uint32_url_type: 0, + str_uin_token: strUinToken, + str_dev_type: 'Windows', + str_dev_name: os.hostname() || 'DESKTOP-NAPCAT', + }; + + const result = await oidbRequest(uin, body); + return sendSuccess(res, result); }; // 轮询新设备验证二维码状态 diff --git a/packages/napcat-webui-frontend/src/components/new_device_verify.tsx b/packages/napcat-webui-frontend/src/components/new_device_verify.tsx index c96a0020..18316ab4 100644 --- a/packages/napcat-webui-frontend/src/components/new_device_verify.tsx +++ b/packages/napcat-webui-frontend/src/components/new_device_verify.tsx @@ -66,10 +66,11 @@ const NewDeviceVerify: React.FC = ({ try { const result = await QQManager.getNewDeviceQRCode(uin, jumpUrl); if (!mountedRef.current) return; - if (result?.str_url && result?.bytes_token) { + if (result?.str_url) { setQrUrl(result.str_url); setStatus('waiting'); - startPolling(result.bytes_token); + // bytes_token 用于轮询,如果 OIDB 未返回则用空字符串 + startPolling(result.bytes_token || ''); } else { setStatus('error'); setErrorMsg('获取二维码失败,请重试'); diff --git a/packages/napcat-webui-frontend/src/components/password_login.tsx b/packages/napcat-webui-frontend/src/components/password_login.tsx index 545ac71f..1f7d8223 100644 --- a/packages/napcat-webui-frontend/src/components/password_login.tsx +++ b/packages/napcat-webui-frontend/src/components/password_login.tsx @@ -52,14 +52,7 @@ const PasswordLogin: React.FC = ({ onSubmit, onCaptchaSubmit return (
- {newDeviceState?.needNewDevice && newDeviceState.jumpUrl ? ( - onNewDeviceVerified?.(token)} - onCancel={onNewDeviceCancel} - /> - ) : captchaState?.needCaptcha && captchaState.proofWaterUrl ? ( + {captchaState?.needCaptcha && captchaState.proofWaterUrl ? (

登录需要安全验证,请完成验证码

= ({ onSubmit, onCaptchaSubmit 取消验证
+ ) : newDeviceState?.needNewDevice && newDeviceState.jumpUrl ? ( + onNewDeviceVerified?.(token)} + onCancel={onNewDeviceCancel} + /> ) : ( <>
diff --git a/packages/napcat-webui-frontend/src/controllers/qq_manager.ts b/packages/napcat-webui-frontend/src/controllers/qq_manager.ts index bbbcad63..eff73288 100644 --- a/packages/napcat-webui-frontend/src/controllers/qq_manager.ts +++ b/packages/napcat-webui-frontend/src/controllers/qq_manager.ts @@ -141,11 +141,37 @@ export default class QQManager { const data = await serverRequest.post>('/QQLogin/GetNewDeviceQRCode', { uin, jumpUrl, }); - return data.data.data; + const result = data.data.data; + if (result?.str_url) { + let bytesToken = result.bytes_token || ''; + if (!bytesToken && result.str_url) { + // 只对 str_url 参数值做 base64 编码 + try { + const urlObj = new URL(result.str_url); + const strUrlParam = urlObj.searchParams.get('str_url') || ''; + bytesToken = strUrlParam ? btoa(strUrlParam) : ''; + } catch { + bytesToken = ''; + } + } + return { + str_url: result.str_url, + bytes_token: bytesToken, + uint32_guarantee_status: result.uint32_guarantee_status, + ActionStatus: result.ActionStatus, + ErrorCode: result.ErrorCode, + ErrorInfo: result.ErrorInfo, + }; + } + return result; } public static async pollNewDeviceQR (uin: string, bytesToken: string) {