Revert "添加自动登录 | feat(login): automatically restore last logged-in account (#1980)"

This reverts commit 31c45bf18e4ea2cc23f6492304fef88afb8e8302.
This commit is contained in:
手瓜一十雪
2026-08-07 17:47:00 +08:00
parent 624ecf5b9a
commit 0fcd0e80e0
9 changed files with 119 additions and 203 deletions
+7 -21
View File
@@ -87,13 +87,11 @@ export async function NCoreInitFramework (
// 提前启动 WebUI,使其在登录前可用,支持通过 WebUI 操控登录过程
WebUiDataRuntime.setWorkingEnv(NapCatCoreWorkingEnv.Framework);
await InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription)
.catch(e => logger.logError(e));
InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e));
// 直到登录成功后,执行下一步
const selfInfo = await new Promise<SelfInfo>((resolve) => {
const loginContext = { isLogined: false };
let autoLoginTriggered = false;
const loginListener = new NodeIKernelLoginListener();
@@ -107,11 +105,6 @@ export async function NCoreInitFramework (
loginContext.isLogined = true;
WebUiDataRuntime.setQQLoginStatus(true);
WebUiDataRuntime.setQQLoginError('');
try {
await WebUiConfig.UpdateLastLoginAccount(loginResult.uin);
} catch (error) {
logger.logWarn('[NapCat] [Framework] 保存最近登录账号失败:', error);
}
await new Promise<void>(resolve => {
registerInitCallback(() => resolve());
});
@@ -147,19 +140,12 @@ export async function NCoreInitFramework (
logger.log('[NapCat] [Framework] 登录服务已连接');
// 注册 WebUI 登录操作回调
registerWebUiLoginCallbacks(loginService, loginContext, logger);
if (autoLoginTriggered) return;
autoLoginTriggered = true;
(async () => {
try {
const res = await loginService.getLoginList();
const list = res.LocalLoginInfoList.filter((item) => item.isQuickLogin);
WebUiDataRuntime.setQQQuickLoginList(list.map((item) => item.uin.toString()));
WebUiDataRuntime.setQQNewLoginList(list);
} catch (error) {
logger.logError('[NapCat] [Framework] 获取登录列表失败:', error);
}
await WebUiDataRuntime.runWebUiConfigQuickFunction();
})().catch(error => logger.logError('[NapCat] [Framework] 自动登录执行失败:', error));
// 获取历史登录列表供 WebUI 使用
loginService.getLoginList().then((res) => {
const list = res.LocalLoginInfoList.filter((item) => item.isQuickLogin);
WebUiDataRuntime.setQQQuickLoginList(list.map((item) => item.uin.toString()));
WebUiDataRuntime.setQQNewLoginList(list);
}).catch(e => logger.logError('[NapCat] [Framework] 获取登录列表失败:', e));
};
loginListener.onQRCodeSessionUserScaned = () => {
@@ -28,7 +28,7 @@ echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LO
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %*
REM Optional: pass the QQ number as the first argument for quick login; omit it for QR code login
REM Example: "%~f0" 123456
REM Optional: -q <QQ_NUMBER> for quick login, omit for QR code login
REM Example: "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" -q 123456
pause
@@ -37,5 +37,5 @@ echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LO
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %*
REM Optional: pass the QQ number as the first argument for quick login; omit it for QR code login
REM Example: "%~f0" 123456
REM Optional: -q <QQ_NUMBER> for quick login, omit for QR code login
REM Example: "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" -q 123456
@@ -1,6 +1,6 @@
@echo off
REM 快速登录示例脚本
REM QQ 号参数是可选的,作为启动脚本的第一个参数传入;不传则使用二维码登录
REM -q 参数是可选的,不传则使用二维码登录
REM
REM 使用方法(删掉对应系统那行的 REM):
REM ./launcher-user.bat 123456
+14 -10
View File
@@ -24,7 +24,7 @@ import { createHash } from 'node:crypto';
import { LoginListItem, NodeIKernelLoginService } from 'napcat-core/services';
import qrcode from 'napcat-qrcode/lib/main';
import { NapCatAdapterManager } from 'napcat-adapter';
import { InitWebUi, WebUiConfig } from 'napcat-webui-backend/index';
import { InitWebUi } from 'napcat-webui-backend/index';
import { WebUiDataRuntime } from 'napcat-webui-backend/src/helper/Data';
import { napCatVersion } from 'napcat-common/src/version';
import { NodeIO3MiscListener } from 'napcat-core/listeners/NodeIO3MiscListener';
@@ -142,11 +142,6 @@ async function handleLogin (
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
context.isLogined = true;
WebUiDataRuntime.setQQLoginStatus(true);
try {
await WebUiConfig.UpdateLastLoginAccount(loginResult.uin);
} catch (error) {
logger.logWarn('[NapCat] [Login] 保存最近登录账号失败:', error);
}
inner_resolve({
uid: loginResult.uid,
uin: loginResult.uin,
@@ -486,18 +481,28 @@ async function handleLoginInner (context: { isLogined: boolean; }, logger: LogWr
if (!success && !attempted && !context.isLogined) loginService.getQRCodePicture();
}
} else {
logger.log('没有 -q 指令指定快速登录,将尝试配置账号或最近登录账号');
logger.log('没有 -q 指令指定快速登录,将使用二维码登录方式');
if (historyLoginList.length > 0) {
logger.log(`可用于快速登录 of QQ:\n${historyLoginList
.map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`)
.join('\n')
}`);
}
let hasAttemptedFallback = false;
try {
// 检查是否配置了自动登录账号,并尝试 WebUi 快速/密码回退登录。
// 这可以避免在已经有自动登录配置的情况下过早输出二维码。
const quickLoginList = WebUiDataRuntime.getQQQuickLoginList() || [];
const hasAutoLogin = process.env['NAPCAT_QUICK_ACCOUNT'] || (quickLoginList.length > 0); // 假如有历史登录列表
if (hasAutoLogin || process.env['NAPCAT_QUICK_PASSWORD'] || process.env['NAPCAT_QUICK_PASSWORD_MD5']) {
hasAttemptedFallback = true;
}
await WebUiDataRuntime.runWebUiConfigQuickFunction();
} catch (error) {
logger.logError('WebUi 快速登录失败 执行失败', error);
if (!context.isLogined) loginService.getQRCodePicture();
}
if (!hasAttemptedFallback && !context.isLogined) {
loginService.getQRCodePicture();
}
}
@@ -643,8 +648,7 @@ export async function NCoreInitShell () {
logger.log('[NapCat] [Core] NapCat.Core Version: ' + napCatVersion);
WebUiDataRuntime.setWorkingEnv(NapCatCoreWorkingEnv.Shell);
await InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription)
.catch(e => logger.logError(e));
InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e));
const engine = wrapper.NodeIQQNTWrapperEngine.get();
const loginService = wrapper.NodeIKernelLoginService.get();
let session: NodeIQQNTWrapperSession;
-51
View File
@@ -1,51 +0,0 @@
import { Value } from '@sinclair/typebox/value';
import { describe, expect, test } from 'vitest';
import {
resolveAutoLoginAccount,
WebUiConfigSchema,
type WebUiConfigType,
} from '../napcat-webui-backend/src/helper/config-schema';
describe('automatic login account selection', () => {
test('uses explicit sources before the last successful account', () => {
expect(resolveAutoLoginAccount('10001', '10002', '10003')).toBe('10001');
expect(resolveAutoLoginAccount(undefined, '10002', '10003')).toBe('10002');
expect(resolveAutoLoginAccount(undefined, undefined, '10003')).toBe('10003');
});
test('ignores empty account values', () => {
expect(resolveAutoLoginAccount(' ', '', ' 10003 ')).toBe('10003');
expect(resolveAutoLoginAccount(undefined, '', ' ')).toBeUndefined();
});
});
describe('WebUI login configuration migration', () => {
test('adds an empty last account to an existing configuration', () => {
const config = Value.Parse(WebUiConfigSchema, {
host: '127.0.0.1',
autoLoginAccount: '10002',
}) as WebUiConfigType;
expect(config.host).toBe('127.0.0.1');
expect(config.autoLoginAccount).toBe('10002');
expect(config.lastLoginAccount).toBe('');
});
test('preserves existing settings when the last account is updated', () => {
const current = Value.Parse(WebUiConfigSchema, {
host: '127.0.0.1',
port: 7000,
autoLoginAccount: '10002',
}) as WebUiConfigType;
const updated = Value.Parse(WebUiConfigSchema, {
...current,
lastLoginAccount: '10003',
}) as WebUiConfigType;
expect(updated.host).toBe('127.0.0.1');
expect(updated.port).toBe(7000);
expect(updated.autoLoginAccount).toBe('10002');
expect(updated.lastLoginAccount).toBe('10003');
});
});
+60 -69
View File
@@ -8,7 +8,7 @@ import { createServer } from 'http';
import { createHash, randomUUID } from 'node:crypto';
import { createServer as createHttpsServer } from 'https';
import { NapCatPathWrapper } from 'napcat-common/src/path';
import { resolveAutoLoginAccount, WebUiConfigWrapper } from '@/napcat-webui-backend/src/helper/config';
import { WebUiConfigWrapper } from '@/napcat-webui-backend/src/helper/config';
import { ALLRouter } from '@/napcat-webui-backend/src/router';
import { cors } from '@/napcat-webui-backend/src/middleware/cors';
import { createUrl, getRandomToken } from '@/napcat-webui-backend/src/utils/url';
@@ -118,74 +118,6 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra
WebUiConfig = new WebUiConfigWrapper();
let config = await WebUiConfig.GetWebUIConfig();
// 自动登录与 WebUI 服务是否启用无关,需要在所有提前返回之前注册。
WebUiDataRuntime.setWebUiConfigQuickFunction(
async () => {
const autoLoginAccount = resolveAutoLoginAccount(
process.env['NAPCAT_QUICK_ACCOUNT'],
WebUiConfig.getAutoLoginAccount(),
WebUiConfig.getLastLoginAccount()
);
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] 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) {
await WebUiDataRuntime.refreshQRCode();
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 作为备用),保持二维码登录兜底`);
await WebUiDataRuntime.refreshQRCode();
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);
}
await WebUiDataRuntime.refreshQRCode();
});
// 检查是否禁用WebUI(若禁用则不进行密码检测)
if (config.disableWebUI) {
logger.log('[NapCat] [WebUi] WebUI is disabled by configuration.');
@@ -220,6 +152,65 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra
logger.log('[NapCat] [WebUi] Current WebUi is not run.');
return;
}
WebUiDataRuntime.setWebUiConfigQuickFunction(
async () => {
const autoLoginAccount = process.env['NAPCAT_QUICK_ACCOUNT'] || WebUiConfig.getAutoLoginAccount();
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] 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);
}
});
// ------------注册中间件------------
// 使用express的json中间件
app.use(express.json());
@@ -1,37 +0,0 @@
import { Static, Type } from '@sinclair/typebox';
import { themeType } from '../types/theme';
import { getRandomToken } from '../utils/url';
export const WebUiConfigSchema = Type.Object({
host: Type.String({ default: '::' }),
port: Type.Number({ default: 6099 }),
token: Type.String({ default: getRandomToken(12) }),
loginRate: Type.Number({ default: 10 }),
autoLoginAccount: Type.String({ default: '' }),
lastLoginAccount: Type.String({ default: '' }),
theme: themeType,
disableWebUI: Type.Boolean({ default: false }),
accessControlMode: Type.Union([
Type.Literal('none'),
Type.Literal('whitelist'),
Type.Literal('blacklist'),
], { default: 'none' }),
ipWhitelist: Type.Array(Type.String(), { default: [] }),
ipBlacklist: Type.Array(Type.String(), { default: [] }),
enableXForwardedFor: Type.Boolean({ default: false }),
enable2FA: Type.Boolean({ default: false }),
totpSecret: Type.String({ default: '' }),
});
export type WebUiConfigType = Static<typeof WebUiConfigSchema>;
export function resolveAutoLoginAccount (
environmentAccount: string | undefined,
configuredAccount: string | undefined,
lastLoginAccount: string | undefined
): string | undefined {
return [environmentAccount, configuredAccount, lastLoginAccount]
.map(account => account?.trim())
.find((account): account is string => Boolean(account));
}
@@ -1,4 +1,5 @@
import { webUiPathWrapper, getInitialWebUiToken } from '@/napcat-webui-backend/index';
import { Type, Static } from '@sinclair/typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';
import { Value } from '@sinclair/typebox/value';
import fs, { constants } from 'node:fs/promises';
@@ -6,9 +7,39 @@ import fs, { constants } from 'node:fs/promises';
import { resolve } from 'node:path';
import { deepMerge } from '../utils/object';
import { WebUiConfigSchema, type WebUiConfigType } from './config-schema';
import { themeType } from '../types/theme';
import { getRandomToken } from '../utils/url';
export { resolveAutoLoginAccount, WebUiConfigSchema, type WebUiConfigType } from './config-schema';
// 限制尝试端口的次数,避免死循环
// 定义配置的类型
const WebUiConfigSchema = Type.Object({
host: Type.String({ default: '::' }),
port: Type.Number({ default: 6099 }),
token: Type.String({ default: getRandomToken(12) }),
loginRate: Type.Number({ default: 10 }),
autoLoginAccount: Type.String({ default: '' }),
theme: themeType,
// 是否关闭WebUI
disableWebUI: Type.Boolean({ default: false }),
// 网络访问控制模式: 'none' | 'whitelist' | 'blacklist'
accessControlMode: Type.Union([
Type.Literal('none'),
Type.Literal('whitelist'),
Type.Literal('blacklist'),
], { default: 'none' }),
// IP白名单列表
ipWhitelist: Type.Array(Type.String(), { default: [] }),
// IP黑名单列表
ipBlacklist: Type.Array(Type.String(), { default: [] }),
// 是否启用 X-Forwarded-For 获取真实IP
enableXForwardedFor: Type.Boolean({ default: false }),
// 是否启用双因素认证(2FA)
enable2FA: Type.Boolean({ default: false }),
// TOTP 密钥
totpSecret: Type.String({ default: '' }),
});
export type WebUiConfigType = Static<typeof WebUiConfigSchema>;
// 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件
export class WebUiConfigWrapper {
@@ -211,14 +242,6 @@ export class WebUiConfigWrapper {
await this.UpdateWebUIConfig({ autoLoginAccount: uin });
}
getLastLoginAccount (): string | undefined {
return this.WebUiConfigData?.lastLoginAccount;
}
async UpdateLastLoginAccount (uin: string): Promise<void> {
await this.UpdateWebUIConfig({ lastLoginAccount: uin });
}
// 获取主题内容
async GetTheme (): Promise<WebUiConfigType['theme']> {
const config = await this.GetWebUIConfig();