fix: 修复NapCat更新登录二维码兜底
This commit is contained in:
parent
0a46e44097
commit
d03e7c09a4
@ -1285,6 +1285,10 @@ export class QqbotNapcatLoginService {
|
||||
}
|
||||
|
||||
if (!loginStatus.isLogin) {
|
||||
if (this.isPasswordQrcodeChallenge(loginStatus)) {
|
||||
await this.keepPasswordQrcodePending(session, container, loginStatus);
|
||||
return true;
|
||||
}
|
||||
const captchaUrl = await this.resolvePasswordCaptchaUrl(
|
||||
container,
|
||||
loginStatus,
|
||||
@ -1363,6 +1367,45 @@ export class QqbotNapcatLoginService {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async keepPasswordQrcodePending(
|
||||
session: QqbotLoginScanSession,
|
||||
container: QqbotNapcatRuntime,
|
||||
loginStatus: NapcatLoginStatus,
|
||||
) {
|
||||
await this.clearRuntimeLoginPasswordAfterFailedPassword(
|
||||
session,
|
||||
container,
|
||||
session.expectedSelfId,
|
||||
);
|
||||
this.publishScanResultEvent(
|
||||
session,
|
||||
'password-login-qrcode',
|
||||
'processing',
|
||||
'密码登录未完成,已切换到扫码确认',
|
||||
);
|
||||
session.qrcode = await this.refreshOrGetQrcode(container, true, {
|
||||
fallbackStatus: loginStatus,
|
||||
requireFresh: this.toolsService.isNapcatExpiredQrcodeStatus(loginStatus),
|
||||
staleQrcode: loginStatus.qrcodeurl,
|
||||
});
|
||||
session.captchaUrl = undefined;
|
||||
session.errorMessage = undefined;
|
||||
session.expiresAt = Date.now() + this.getSessionTtlMs();
|
||||
this.sessions.set(session.id, session);
|
||||
this.publishScanResultEvent(
|
||||
session,
|
||||
'qrcode-ready',
|
||||
'success',
|
||||
'登录二维码已生成',
|
||||
);
|
||||
this.publishScanResultEvent(
|
||||
session,
|
||||
'waiting-scan',
|
||||
'processing',
|
||||
'等待扫码确认',
|
||||
);
|
||||
}
|
||||
|
||||
private async resolvePasswordCaptchaUrl(
|
||||
container: QqbotNapcatRuntime,
|
||||
loginStatus: NapcatLoginStatus,
|
||||
@ -1385,6 +1428,13 @@ export class QqbotNapcatLoginService {
|
||||
return this.toolsService.extractNapcatCaptchaUrl(status.loginError);
|
||||
}
|
||||
|
||||
private isPasswordQrcodeChallenge(status: NapcatLoginStatus) {
|
||||
return (
|
||||
!!this.toolsService.toTrimmedString(status.qrcodeurl) ||
|
||||
this.toolsService.isNapcatExpiredQrcodeStatus(status)
|
||||
);
|
||||
}
|
||||
|
||||
private async clearRuntimeLoginPasswordBeforeQuick(
|
||||
session: QqbotLoginScanSession,
|
||||
container: QqbotNapcatRuntime,
|
||||
@ -1666,6 +1716,7 @@ export class QqbotNapcatLoginService {
|
||||
latestStatus = await this.getLoginStatus(container, true);
|
||||
if (
|
||||
latestStatus.isLogin ||
|
||||
this.isPasswordQrcodeChallenge(latestStatus) ||
|
||||
this.toolsService.isNapcatCaptchaRequiredMessage(
|
||||
latestStatus.loginError,
|
||||
)
|
||||
|
||||
@ -132,15 +132,19 @@ export class QqbotNapcatContainerService {
|
||||
skipPull: true,
|
||||
token: container.webuiToken,
|
||||
});
|
||||
const verified = await this.runtimeMatchesLoginEnv(container.name, {
|
||||
...options,
|
||||
selfId: account,
|
||||
});
|
||||
await this.containerRepository.update(
|
||||
{ id: container.id },
|
||||
{
|
||||
lastError: null,
|
||||
lastError: verified ? null : 'NapCat 运行态登录环境校验失败',
|
||||
lastStartedAt: new Date(),
|
||||
status: 'running',
|
||||
},
|
||||
);
|
||||
return { changed: true, ok: true };
|
||||
return { changed: true, ok: verified };
|
||||
} catch {
|
||||
return { changed: false, ok: false };
|
||||
}
|
||||
|
||||
@ -748,6 +748,162 @@ describe('QqbotNapcatLoginService', () => {
|
||||
expect(session.captchaUrl).toBe(captchaUrl);
|
||||
});
|
||||
|
||||
it('keeps qrcode pending when password login falls into NapCat qrcode status', async () => {
|
||||
const container = {
|
||||
baseUrl: 'http://127.0.0.1:6103/',
|
||||
id: 'container-password-qrcode',
|
||||
name: 'napcat-10001',
|
||||
};
|
||||
const containerService = {
|
||||
ensureRuntimeLoginEnv: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ changed: false, ok: true })
|
||||
.mockResolvedValueOnce({ changed: true, ok: true })
|
||||
.mockResolvedValueOnce({ changed: true, ok: true }),
|
||||
resetRuntimeLoginState: jest.fn().mockResolvedValue(true),
|
||||
restartRuntimeContainer: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
const refreshService = new QqbotNapcatLoginService(
|
||||
{
|
||||
get: jest.fn((key: string) => {
|
||||
const values: Record<string, string> = {
|
||||
QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS: '1',
|
||||
QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS: '10',
|
||||
QQBOT_NAPCAT_QUICK_LOGIN_WAIT_MS: '1',
|
||||
};
|
||||
return values[key] || '';
|
||||
}),
|
||||
} as unknown as ConfigService,
|
||||
{} as QqbotAccountService,
|
||||
containerService as unknown as QqbotNapcatContainerService,
|
||||
new ToolsService(),
|
||||
);
|
||||
const session = (refreshService as any).createSession({
|
||||
accountId: 'account-1',
|
||||
container,
|
||||
expectedSelfId: '10001',
|
||||
mode: 'refresh',
|
||||
preparingRelogin: true,
|
||||
status: 'pending',
|
||||
});
|
||||
(refreshService as any).sessions.set(session.id, session);
|
||||
jest
|
||||
.spyOn((refreshService as any).toolsService, 'sleep')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(refreshService as any, 'getLoginStatus')
|
||||
.mockResolvedValueOnce({
|
||||
isLogin: false,
|
||||
loginError: '快速登录未找到历史会话',
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
isLogin: false,
|
||||
loginError: '二维码已过期,请刷新',
|
||||
qrcodeurl: 'expired-qrcode',
|
||||
});
|
||||
const refreshQrcode = jest
|
||||
.spyOn(refreshService as any, 'refreshOrGetQrcode')
|
||||
.mockResolvedValue('fresh-qrcode');
|
||||
|
||||
await (refreshService as any).prepareReloginQrcode(
|
||||
session,
|
||||
container,
|
||||
'qq-password',
|
||||
);
|
||||
|
||||
expect(containerService.resetRuntimeLoginState).not.toHaveBeenCalled();
|
||||
expect(refreshQrcode).toHaveBeenCalledWith(
|
||||
container,
|
||||
true,
|
||||
expect.objectContaining({
|
||||
requireFresh: true,
|
||||
staleQrcode: 'expired-qrcode',
|
||||
}),
|
||||
);
|
||||
expect(session.status).toBe('pending');
|
||||
expect(session.captchaUrl).toBeUndefined();
|
||||
expect(session.passwordMd5).toBeUndefined();
|
||||
expect(session.qrcode).toBe('fresh-qrcode');
|
||||
const steps = (
|
||||
(refreshService as any).sessionEventLogs.get(session.id) ?? []
|
||||
).map((event: { step: string }) => event.step);
|
||||
expect(steps).toContain('password-login-qrcode');
|
||||
expect(steps).not.toContain('relogin-reset-start');
|
||||
});
|
||||
|
||||
it('prioritizes current qrcode status over stale captcha URL in runtime logs', async () => {
|
||||
const container = {
|
||||
baseUrl: 'http://127.0.0.1:6103/',
|
||||
id: 'container-password-qrcode-over-captcha-log',
|
||||
name: 'napcat-10001',
|
||||
};
|
||||
const containerService = {
|
||||
detectRuntimeCaptchaUrl: jest
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
'https://ti.qq.com/safe/tools/captcha/sms-verify-login?uin=10001',
|
||||
),
|
||||
ensureRuntimeLoginEnv: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ changed: false, ok: true })
|
||||
.mockResolvedValueOnce({ changed: true, ok: true })
|
||||
.mockResolvedValueOnce({ changed: true, ok: true }),
|
||||
resetRuntimeLoginState: jest.fn().mockResolvedValue(true),
|
||||
restartRuntimeContainer: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
const refreshService = new QqbotNapcatLoginService(
|
||||
{
|
||||
get: jest.fn((key: string) => {
|
||||
const values: Record<string, string> = {
|
||||
QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS: '1',
|
||||
QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS: '10',
|
||||
};
|
||||
return values[key] || '';
|
||||
}),
|
||||
} as unknown as ConfigService,
|
||||
{} as QqbotAccountService,
|
||||
containerService as unknown as QqbotNapcatContainerService,
|
||||
new ToolsService(),
|
||||
);
|
||||
const session = (refreshService as any).createSession({
|
||||
accountId: 'account-1',
|
||||
container,
|
||||
expectedSelfId: '10001',
|
||||
mode: 'refresh',
|
||||
preparingRelogin: true,
|
||||
status: 'pending',
|
||||
});
|
||||
(refreshService as any).sessions.set(session.id, session);
|
||||
jest
|
||||
.spyOn((refreshService as any).toolsService, 'sleep')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(refreshService as any, 'getLoginStatus')
|
||||
.mockResolvedValueOnce({
|
||||
isLogin: false,
|
||||
loginError: '快速登录未找到历史会话',
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
isLogin: false,
|
||||
loginError: '二维码已过期,请刷新',
|
||||
qrcodeurl: 'expired-qrcode',
|
||||
});
|
||||
jest
|
||||
.spyOn(refreshService as any, 'refreshOrGetQrcode')
|
||||
.mockResolvedValue('fresh-qrcode');
|
||||
|
||||
await (refreshService as any).prepareReloginQrcode(
|
||||
session,
|
||||
container,
|
||||
'qq-password',
|
||||
);
|
||||
|
||||
expect(containerService.detectRuntimeCaptchaUrl).not.toHaveBeenCalled();
|
||||
expect(session.status).toBe('pending');
|
||||
expect(session.captchaUrl).toBeUndefined();
|
||||
expect(session.qrcode).toBe('fresh-qrcode');
|
||||
});
|
||||
|
||||
it('submits captcha result back to NapCat and completes password login', async () => {
|
||||
const accountService = {
|
||||
ensureScannedAccount: jest.fn().mockResolvedValue('account-1'),
|
||||
@ -964,6 +1120,42 @@ describe('QqbotNapcatLoginService', () => {
|
||||
expect(sleep).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns qrcode status immediately without waiting full password window', async () => {
|
||||
const refreshService = new QqbotNapcatLoginService(
|
||||
{
|
||||
get: jest.fn((key: string) => {
|
||||
const values: Record<string, string> = {
|
||||
QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS: '1000',
|
||||
QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS: '3000',
|
||||
};
|
||||
return values[key] || '';
|
||||
}),
|
||||
} as unknown as ConfigService,
|
||||
{} as QqbotAccountService,
|
||||
{} as QqbotNapcatContainerService,
|
||||
new ToolsService(),
|
||||
);
|
||||
const getLoginStatus = jest
|
||||
.spyOn(refreshService as any, 'getLoginStatus')
|
||||
.mockResolvedValue({
|
||||
isLogin: false,
|
||||
loginError: '二维码已过期,请刷新',
|
||||
qrcodeurl: 'expired-qrcode',
|
||||
});
|
||||
const sleep = jest.spyOn((refreshService as any).toolsService, 'sleep');
|
||||
|
||||
const status = await (refreshService as any).waitForPasswordLoginStatus({
|
||||
baseUrl: 'http://127.0.0.1:6103/',
|
||||
id: 'container-qrcode-early',
|
||||
name: 'napcat-10001',
|
||||
webuiToken: 'token',
|
||||
});
|
||||
|
||||
expect(status.qrcodeurl).toBe('expired-qrcode');
|
||||
expect(getLoginStatus).toHaveBeenCalledTimes(1);
|
||||
expect(sleep).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not keep stale captcha pending after captcha submit returns non-captcha failure', async () => {
|
||||
const container = {
|
||||
baseUrl: 'http://127.0.0.1:6103/',
|
||||
|
||||
@ -418,7 +418,11 @@ describe('QqbotNapcatContainerService', () => {
|
||||
service.runProcess = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('inspect failed'))
|
||||
.mockResolvedValueOnce({ stderr: '', stdout: '' });
|
||||
.mockResolvedValueOnce({ stderr: '', stdout: '' })
|
||||
.mockResolvedValueOnce({
|
||||
stderr: '',
|
||||
stdout: 'ACCOUNT=2354598417\nNAPCAT_QUICK_PASSWORD=qq-password\n',
|
||||
});
|
||||
|
||||
const recreated = await service.ensureRuntimeLoginEnv(
|
||||
{ id: 'container-1', name: 'kt-qqbot-napcat-x' },
|
||||
@ -429,7 +433,7 @@ describe('QqbotNapcatContainerService', () => {
|
||||
);
|
||||
|
||||
expect(recreated).toEqual({ changed: true, ok: true });
|
||||
expect(service.runProcess).toHaveBeenCalledTimes(2);
|
||||
expect(service.runProcess).toHaveBeenCalledTimes(3);
|
||||
expect(service.runProcess.mock.calls[1][2]).toContain(
|
||||
"NAPCAT_QUICK_PASSWORD='qq-password'",
|
||||
);
|
||||
@ -466,7 +470,11 @@ describe('QqbotNapcatContainerService', () => {
|
||||
service.runProcess = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ stderr: '', stdout: 'ACCOUNT=2354598417\n' })
|
||||
.mockResolvedValueOnce({ stderr: '', stdout: '' });
|
||||
.mockResolvedValueOnce({ stderr: '', stdout: '' })
|
||||
.mockResolvedValueOnce({
|
||||
stderr: '',
|
||||
stdout: 'ACCOUNT=2354598417\nNAPCAT_QUICK_PASSWORD=qq-password\n',
|
||||
});
|
||||
|
||||
const recreated = await service.ensureRuntimeLoginEnv(
|
||||
{ id: 'container-1', name: 'kt-qqbot-napcat-x' },
|
||||
@ -477,7 +485,7 @@ describe('QqbotNapcatContainerService', () => {
|
||||
);
|
||||
|
||||
expect(recreated).toEqual({ changed: true, ok: true });
|
||||
expect(service.runProcess).toHaveBeenCalledTimes(2);
|
||||
expect(service.runProcess).toHaveBeenCalledTimes(3);
|
||||
expect(service.runProcess.mock.calls[1][2]).toContain(
|
||||
"NAPCAT_QUICK_PASSWORD='qq-password'",
|
||||
);
|
||||
@ -490,6 +498,59 @@ describe('QqbotNapcatContainerService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('fails login env preparation when recreated container still misses password env', async () => {
|
||||
const containerRepository = {
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
getOne: jest.fn().mockResolvedValue({
|
||||
dataDir: '/vol1/docker/kt-qqbot/napcat-instances/kt-qqbot-napcat-x',
|
||||
id: 'container-1',
|
||||
image: 'mlikiowa/napcat-docker:latest',
|
||||
name: 'kt-qqbot-napcat-x',
|
||||
reverseWsUrl: 'ws://127.0.0.1:48085/qqbot/onebot/reverse',
|
||||
webuiPort: 6100,
|
||||
webuiToken: 'token-x',
|
||||
}),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
})),
|
||||
update: jest.fn(),
|
||||
};
|
||||
const service = new QqbotNapcatContainerService(
|
||||
{
|
||||
get: jest.fn((key: string) =>
|
||||
key === 'QQBOT_NAPCAT_CONTAINER_MODE' ? 'ssh' : '',
|
||||
),
|
||||
} as any,
|
||||
containerRepository as any,
|
||||
{} as any,
|
||||
new ToolsService(),
|
||||
) as any;
|
||||
service.runProcess = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ stderr: '', stdout: 'ACCOUNT=2354598417\n' })
|
||||
.mockResolvedValueOnce({ stderr: '', stdout: '' })
|
||||
.mockResolvedValueOnce({ stderr: '', stdout: 'ACCOUNT=2354598417\n' });
|
||||
|
||||
const recreated = await service.ensureRuntimeLoginEnv(
|
||||
{ id: 'container-1', name: 'kt-qqbot-napcat-x' },
|
||||
{
|
||||
loginPassword: 'qq-password',
|
||||
selfId: '2354598417',
|
||||
},
|
||||
);
|
||||
|
||||
expect(recreated).toEqual({ changed: true, ok: false });
|
||||
expect(service.runProcess).toHaveBeenCalledTimes(3);
|
||||
expect(containerRepository.update).toHaveBeenCalledWith(
|
||||
{ id: 'container-1' },
|
||||
expect.objectContaining({
|
||||
lastError: 'NapCat 运行态登录环境校验失败',
|
||||
status: 'running',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('removes password env for a quick-login-only runtime stage', async () => {
|
||||
const containerRepository = {
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
@ -524,7 +585,8 @@ describe('QqbotNapcatContainerService', () => {
|
||||
stderr: '',
|
||||
stdout: 'ACCOUNT=2354598417\nNAPCAT_QUICK_PASSWORD=old-password\n',
|
||||
})
|
||||
.mockResolvedValueOnce({ stderr: '', stdout: '' });
|
||||
.mockResolvedValueOnce({ stderr: '', stdout: '' })
|
||||
.mockResolvedValueOnce({ stderr: '', stdout: 'ACCOUNT=2354598417\n' });
|
||||
|
||||
const recreated = await service.ensureRuntimeLoginEnv(
|
||||
{ id: 'container-1', name: 'kt-qqbot-napcat-x' },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user