From eb07cdb715a43724b7736ab1719de64e48fb888e Mon Sep 17 00:00:00 2001 From: Eric-Terminal Date: Sat, 21 Feb 2026 14:18:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E5=90=8E=E5=9B=9E=E9=80=80=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E5=B9=B6=E8=A1=A5=E5=85=85=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=20(#1638)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 自动登录失败后回退密码登录并补充独立配置 改动文件: - packages/napcat-webui-backend/src/helper/config.ts - packages/napcat-webui-backend/src/utils/auto_login.ts - packages/napcat-webui-backend/src/utils/auto_login_config.ts - packages/napcat-webui-backend/index.ts - packages/napcat-webui-backend/src/api/QQLogin.ts - packages/napcat-webui-backend/src/router/QQLogin.ts - packages/napcat-webui-frontend/src/controllers/qq_manager.ts - packages/napcat-webui-frontend/src/pages/dashboard/config/login.tsx - packages/napcat-test/autoPasswordFallback.test.ts 目的: - 在启动阶段将自动登录流程从“仅快速登录”扩展为“快速登录失败后自动回退密码登录”,并保持二维码兜底。 - 在 WebUI 登录配置页新增独立的自动回退账号/密码配置,密码仅提交与存储 MD5,不回显明文。 效果: - 后端配置新增 autoPasswordLoginAccount 与 autoPasswordLoginPasswordMd5 字段,并提供读取、更新(空密码不覆盖)和清空能力。 - 新增 QQLogin API:GetAutoPasswordLoginConfig / SetAutoPasswordLoginConfig / ClearAutoPasswordLoginConfig。 - WebUI 登录配置页新增自动回退密码登录区块,支持保存、刷新、清空及“留空不修改密码”交互。 - 新增自动登录回退逻辑单测与配置补丁构造单测,覆盖快速成功、回退成功、回退失败、无密码兜底等场景。 * feat: 精简为环境变量驱动的快速登录失败密码回退 改动目的: - 按维护者建议将方案收敛为后端环境变量驱动,不新增 WebUI 配置与路由 - 保留“快速登录失败 -> 密码回退 -> 二维码兜底”核心能力 - 兼容快速启动参数场景,降低评审复杂度 主要改动文件: - packages/napcat-webui-backend/index.ts - packages/napcat-shell/base.ts - packages/napcat-webui-backend/src/api/QQLogin.ts - packages/napcat-webui-backend/src/helper/config.ts - packages/napcat-webui-backend/src/router/QQLogin.ts - packages/napcat-webui-frontend/src/controllers/qq_manager.ts - packages/napcat-webui-frontend/src/pages/dashboard/config/login.tsx - 删除:packages/napcat-webui-backend/src/utils/auto_login.ts - 删除:packages/napcat-webui-backend/src/utils/auto_login_config.ts - 删除:packages/napcat-test/autoPasswordFallback.test.ts 实现细节: 1. WebUI 启动自动登录链路 - 保留 NAPCAT_QUICK_ACCOUNT 优先逻辑 - 快速登录失败后触发密码回退 - 回退密码来源优先级: a) NAPCAT_QUICK_PASSWORD_MD5(32 位 MD5) b) NAPCAT_QUICK_PASSWORD(运行时自动计算 MD5) - 未配置回退密码时保持二维码兜底,并输出带 QQ 号的引导日志 2. Shell 快速登录链路 - quickLoginWithUin 失败判定统一基于 result 码 + errMsg - 覆盖历史账号不存在、凭证失效、快速登录异常等场景 - 失败后统一进入同一密码回退逻辑,再兜底二维码 3. 文案与可运维性 - 日志明确推荐优先使用 ACCOUNT + NAPCAT_QUICK_PASSWORD - NAPCAT_QUICK_PASSWORD_MD5 作为备用方式 效果: - 满足自动回退登录需求,且改动面显著缩小 - 不修改 napcat-docker 仓库代码,直接兼容现有容器启动参数 - 便于上游快速审阅与合并 * fix: 修复 napcat-framework 未使用变量导致的 CI typecheck 失败 改动文件: - packages/napcat-framework/napcat.ts 问题背景: - 上游代码中声明了变量 bypassEnabled,但后续未使用 - 在 CI 的全量 TypeScript 检查中触发 TS6133(声明但未读取) - 导致 PR Build 机器人评论显示构建失败(Type check failed) 具体修复: - 将以下语句从“赋值后未使用”改为“直接调用” - 原:const bypassEnabled = napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions); - 现:napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions); 影响与效果: - 不改变运行时行为(仍会执行 enableAllBypasses) - 消除 TS6133 报错,恢复 typecheck 可通过 本地验证: - pnpm run typecheck:通过 - pnpm run build:framework:通过 - pnpm run build:shell:通过 --------- Co-authored-by: 手瓜一十雪 --- packages/napcat-shell/base.ts | 84 ++++++++++++++++++++++---- packages/napcat-webui-backend/index.ts | 62 ++++++++++++++++--- 2 files changed, 126 insertions(+), 20 deletions(-) diff --git a/packages/napcat-shell/base.ts b/packages/napcat-shell/base.ts index c813a92b..e8909d5e 100644 --- a/packages/napcat-shell/base.ts +++ b/packages/napcat-shell/base.ts @@ -20,6 +20,7 @@ import { hostname, systemVersion } from 'napcat-common/src/system'; import path from 'path'; import fs from 'fs'; import os from 'os'; +import { createHash } from 'node:crypto'; import { LoginListItem, NodeIKernelLoginService } from 'napcat-core/services'; import qrcode from 'napcat-qrcode/lib/main'; import { NapCatAdapterManager } from 'napcat-adapter'; @@ -194,6 +195,24 @@ async function handleLogin ( return await selfInfo; } async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWrapper, loginService: NodeIKernelLoginService, quickLoginUin: string | undefined, historyLoginList: LoginListItem[]) { + const resolveQuickPasswordMd5 = (): string | undefined => { + const quickPasswordMd5 = process.env['NAPCAT_QUICK_PASSWORD_MD5']?.trim(); + if (quickPasswordMd5) { + if (/^[a-fA-F0-9]{32}$/.test(quickPasswordMd5)) { + return quickPasswordMd5.toLowerCase(); + } + logger.logError('NAPCAT_QUICK_PASSWORD_MD5 格式无效(需为 32 位 MD5)'); + } + + const quickPassword = process.env['NAPCAT_QUICK_PASSWORD']; + if (typeof quickPassword === 'string' && quickPassword.length > 0) { + logger.log('检测到 NAPCAT_QUICK_PASSWORD,已在内存中计算 MD5 用于回退登录'); + return createHash('md5').update(quickPassword, 'utf8').digest('hex'); + } + + return undefined; + }; + // 注册刷新二维码回调 WebUiDataRuntime.setRefreshQRCodeCallback(async () => { loginService.getQRCodePicture(); @@ -204,10 +223,12 @@ async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWr if (uin) { logger.log('正在快速登录 ', uin); loginService.quickLoginWithUin(uin).then(res => { - if (res.loginErrorInfo.errMsg) { - WebUiDataRuntime.setQQLoginError(res.loginErrorInfo.errMsg); + const quickLoginSuccess = res.result === '0' && !res.loginErrorInfo?.errMsg; + if (!quickLoginSuccess) { + const errMsg = res.loginErrorInfo?.errMsg || `快速登录失败,错误码: ${res.result}`; + WebUiDataRuntime.setQQLoginError(errMsg); loginService.getQRCodePicture(); - resolve({ result: false, message: res.loginErrorInfo.errMsg }); + resolve({ result: false, message: errMsg }); } else { WebUiDataRuntime.setQQLoginStatus(true); WebUiDataRuntime.setQQLoginError(''); @@ -292,6 +313,38 @@ async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWr } }); }); + const tryPasswordFallbackLogin = async (uin: string): Promise<{ success: boolean, attempted: boolean; }> => { + const quickPasswordMd5 = resolveQuickPasswordMd5(); + if (!quickPasswordMd5) { + logger.log(`QQ ${uin} 未配置回退密码环境变量,建议优先使用 ACCOUNT + NAPCAT_QUICK_PASSWORD(NAPCAT_QUICK_PASSWORD_MD5 作为备用),将使用二维码登录方式`); + return { success: false, attempted: false }; + } + + logger.log('正在尝试密码回退登录 ', uin); + const fallbackResult = await WebUiDataRuntime.requestPasswordLogin(uin, quickPasswordMd5); + if (fallbackResult.result) { + logger.log('密码回退登录成功 ', uin); + return { success: true, attempted: true }; + } + if (fallbackResult.needCaptcha) { + const captchaTip = fallbackResult.proofWaterUrl + ? `密码回退需要验证码,请在 WebUi 中继续完成验证:${fallbackResult.proofWaterUrl}` + : '密码回退需要验证码,请在 WebUi 中继续完成验证'; + logger.logWarn(captchaTip); + WebUiDataRuntime.setQQLoginError('密码回退需要验证码,请在 WebUi 中继续完成验证'); + return { success: false, attempted: true }; + } + if (fallbackResult.needNewDevice) { + const newDeviceTip = fallbackResult.jumpUrl + ? `密码回退需要新设备验证,请在 WebUi 中继续完成验证:${fallbackResult.jumpUrl}` + : '密码回退需要新设备验证,请在 WebUi 中继续完成验证'; + logger.logWarn(newDeviceTip); + WebUiDataRuntime.setQQLoginError('密码回退需要新设备验证,请在 WebUi 中继续完成验证'); + return { success: false, attempted: true }; + } + logger.logError('密码回退登录失败:', fallbackResult.message); + return { success: false, attempted: true }; + }; // 注册验证码登录回调(密码登录需要验证码时的第二步) WebUiDataRuntime.setCaptchaLoginCall(async (uin: string, passwordMd5: string, ticket: string, randstr: string, sid: string) => { @@ -404,17 +457,26 @@ async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWr if (historyLoginList.some(u => u.uin === quickLoginUin)) { logger.log('正在快速登录 ', quickLoginUin); loginService.quickLoginWithUin(quickLoginUin) - .then(result => { - if (result.loginErrorInfo.errMsg) { - logger.logError('快速登录错误:', result.loginErrorInfo.errMsg); - WebUiDataRuntime.setQQLoginError(result.loginErrorInfo.errMsg); - if (!context.isLogined) loginService.getQRCodePicture(); + .then(async result => { + const quickLoginSuccess = result.result === '0' && !result.loginErrorInfo?.errMsg; + if (!quickLoginSuccess) { + const errMsg = result.loginErrorInfo?.errMsg || `快速登录失败,错误码: ${result.result}`; + logger.logError('快速登录错误:', errMsg); + WebUiDataRuntime.setQQLoginError(errMsg); + const { success, attempted } = await tryPasswordFallbackLogin(quickLoginUin); + if (!success && !attempted && !context.isLogined) loginService.getQRCodePicture(); } }) - .catch(); + .catch(async (error) => { + logger.logError('快速登录异常:', error); + WebUiDataRuntime.setQQLoginError('快速登录发生错误'); + const { success, attempted } = await tryPasswordFallbackLogin(quickLoginUin); + if (!success && !attempted && !context.isLogined) loginService.getQRCodePicture(); + }); } else { - logger.logError('快速登录失败,未找到该 QQ 历史登录记录,将使用二维码登录方式'); - if (!context.isLogined) loginService.getQRCodePicture(); + logger.logError('快速登录失败,未找到该 QQ 历史登录记录,将尝试密码回退登录'); + const { success, attempted } = await tryPasswordFallbackLogin(quickLoginUin); + if (!success && !attempted && !context.isLogined) loginService.getQRCodePicture(); } } else { logger.log('没有 -q 指令指定快速登录,将使用二维码登录方式'); diff --git a/packages/napcat-webui-backend/index.ts b/packages/napcat-webui-backend/index.ts index 87481d6b..ddff5d8c 100644 --- a/packages/napcat-webui-backend/index.ts +++ b/packages/napcat-webui-backend/index.ts @@ -5,7 +5,7 @@ import express from 'express'; import type { WebUiConfigType } from './src/types'; import { createServer } from 'http'; -import { randomUUID } from 'node:crypto'; +import { createHash, randomUUID } from 'node:crypto'; import { createServer as createHttpsServer } from 'https'; import { NapCatPathWrapper } from 'napcat-common/src/path'; import { WebUiConfigWrapper } from '@/napcat-webui-backend/src/helper/config'; @@ -156,16 +156,60 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra WebUiDataRuntime.setWebUiConfigQuickFunction( async () => { const autoLoginAccount = process.env['NAPCAT_QUICK_ACCOUNT'] || WebUiConfig.getAutoLoginAccount(); - if (autoLoginAccount) { - try { - const { result, message } = await WebUiDataRuntime.requestQuickLogin(autoLoginAccount); - if (!result) { - throw new Error(message); + const resolveQuickPasswordMd5 = (): string | undefined => { + const quickPasswordMd5FromEnv = process.env['NAPCAT_QUICK_PASSWORD_MD5']?.trim(); + if (quickPasswordMd5FromEnv) { + if (/^[a-fA-F0-9]{32}$/.test(quickPasswordMd5FromEnv)) { + return quickPasswordMd5FromEnv.toLowerCase(); } - console.log(`[NapCat] [WebUi] Auto login account: ${autoLoginAccount}`); - } catch (error) { - console.log('[NapCat] [WebUi] Auto login account failed.' + error); + console.log('[NapCat] [WebUi] NAPCAT_QUICK_PASSWORD_MD5 格式无效(需为 32 位 MD5)'); } + + const quickPassword = process.env['NAPCAT_QUICK_PASSWORD']; + if (typeof quickPassword === 'string' && quickPassword.length > 0) { + console.log('[NapCat] [WebUi] 检测到 NAPCAT_QUICK_PASSWORD,已在内存中计算 MD5 用于回退登录'); + return createHash('md5').update(quickPassword, 'utf8').digest('hex'); + } + return undefined; + }; + if (!autoLoginAccount) { + return; + } + const quickPasswordMd5 = resolveQuickPasswordMd5(); + + try { + const { result, message } = await WebUiDataRuntime.requestQuickLogin(autoLoginAccount); + if (result) { + console.log(`[NapCat] [WebUi] 自动快速登录成功: ${autoLoginAccount}`); + return; + } + console.log(`[NapCat] [WebUi] 自动快速登录失败: ${message || '未知错误'}`); + } catch (error) { + console.log('[NapCat] [WebUi] 自动快速登录异常:' + error); + } + + if (!quickPasswordMd5) { + console.log(`[NapCat] [WebUi] QQ ${autoLoginAccount} 未配置回退密码环境变量,建议优先使用 ACCOUNT + NAPCAT_QUICK_PASSWORD(NAPCAT_QUICK_PASSWORD_MD5 作为备用),保持二维码登录兜底`); + return; + } + + try { + const { result, message, needCaptcha, needNewDevice } = await WebUiDataRuntime.requestPasswordLogin(autoLoginAccount, quickPasswordMd5); + if (result) { + console.log(`[NapCat] [WebUi] 自动密码回退登录成功: ${autoLoginAccount}`); + return; + } + if (needCaptcha) { + console.log(`[NapCat] [WebUi] 自动密码回退登录需要验证码,请在登录页面继续完成: ${autoLoginAccount}`); + return; + } + if (needNewDevice) { + console.log(`[NapCat] [WebUi] 自动密码回退登录需要新设备验证,请在登录页面继续完成: ${autoLoginAccount}`); + return; + } + console.log(`[NapCat] [WebUi] 自动密码回退登录失败: ${message || '未知错误'}`); + } catch (error) { + console.log('[NapCat] [WebUi] 自动密码回退登录异常:' + error); } }); // ------------注册中间件------------