mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-28 07:40:27 +00:00
feat: 自动登录失败后回退密码登录并补充独立配置 (#1638)
* 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: 手瓜一十雪 <nanaeonn@outlook.com>
This commit is contained in:
@@ -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 指令指定快速登录,将使用二维码登录方式');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
// ------------注册中间件------------
|
||||
|
||||
Reference in New Issue
Block a user