From e5709e00e1a54a62ef0c0e8191d44fec06740e45 Mon Sep 17 00:00:00 2001 From: sunlei Date: Sat, 13 Jun 2026 12:18:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=8F=90=E5=89=8D=E6=8D=95=E8=8E=B7NapC?= =?UTF-8?q?at=E9=AA=8C=E8=AF=81=E7=A0=81=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/qqbot-napcat-login.service.ts | 29 +++-- .../qqbot-napcat-login.service.spec.ts | 103 +++++++++++++++++- 2 files changed, 121 insertions(+), 11 deletions(-) diff --git a/src/qqbot/account/qqbot-napcat-login.service.ts b/src/qqbot/account/qqbot-napcat-login.service.ts index a70782c..3649ce2 100644 --- a/src/qqbot/account/qqbot-napcat-login.service.ts +++ b/src/qqbot/account/qqbot-napcat-login.service.ts @@ -1760,26 +1760,39 @@ export class QqbotNapcatLoginService { } throw err; } + const qrcodeChallenge = this.isPasswordQrcodeChallenge(latestStatus); + const captchaRequired = this.toolsService.isNapcatCaptchaRequiredMessage( + latestStatus.loginError, + ); + const hasRestartTimestamp = + typeof sinceMs === 'number' && Number.isFinite(sinceMs); if ( - latestStatus.isLogin || - this.isPasswordQrcodeChallenge(latestStatus) || - this.toolsService.isNapcatCaptchaRequiredMessage( - latestStatus.loginError, - ) + !latestStatus.isLogin && + !qrcodeChallenge && + (hasRestartTimestamp || captchaRequired) ) { + latestStatus.captchaUrl = + this.getCaptchaUrlFromStatus(latestStatus) || + (await this.detectPasswordCaptchaUrl( + container, + sinceMs, + captchaRequired, + )); + } + if (latestStatus.isLogin || qrcodeChallenge || captchaRequired) { if ( !this.getCaptchaUrlFromStatus(latestStatus) && - this.toolsService.isNapcatCaptchaRequiredMessage( - latestStatus.loginError, - ) + captchaRequired ) { latestStatus.captchaUrl = await this.detectPasswordCaptchaUrl( container, sinceMs, + true, ); } return latestStatus; } + if (latestStatus.captchaUrl) return latestStatus; } return latestStatus; } diff --git a/test/qqbot/account/qqbot-napcat-login.service.spec.ts b/test/qqbot/account/qqbot-napcat-login.service.spec.ts index 81aed5a..5bbefd9 100644 --- a/test/qqbot/account/qqbot-napcat-login.service.spec.ts +++ b/test/qqbot/account/qqbot-napcat-login.service.spec.ts @@ -759,8 +759,9 @@ describe('QqbotNapcatLoginService', () => { const containerService = { detectRuntimeCaptchaUrl: jest .fn() - .mockResolvedValueOnce(null) - .mockResolvedValueOnce(staleCaptchaUrl), + .mockImplementation(async (_runtime: unknown, sinceMs?: number) => + typeof sinceMs === 'number' ? null : staleCaptchaUrl, + ), ensureRuntimeLoginEnv: jest .fn() .mockResolvedValue({ changed: true, ok: true }), @@ -815,10 +816,52 @@ describe('QqbotNapcatLoginService', () => { ); expect(session.captchaUrl).toBeUndefined(); - expect(containerService.detectRuntimeCaptchaUrl).toHaveBeenCalledTimes(1); + expect(containerService.detectRuntimeCaptchaUrl).toHaveBeenCalledTimes(2); + expect(containerService.detectRuntimeCaptchaUrl).not.toHaveBeenCalledWith( + container, + ); expect(session.qrcode).toBe('fallback-qrcode'); }); + it('does not read tail captcha logs for processing status without restart timestamp', async () => { + const staleCaptchaUrl = + 'https://ti.qq.com/safe/tools/captcha/sms-verify-login?uin=10001'; + const containerService = { + detectRuntimeCaptchaUrl: jest.fn().mockResolvedValue(staleCaptchaUrl), + }; + const refreshService = new QqbotNapcatLoginService( + { + get: jest.fn((key: string) => { + const values: Record = { + QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS: '1', + QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS: '1', + }; + return values[key] || ''; + }), + } as unknown as ConfigService, + {} as QqbotAccountService, + containerService as unknown as QqbotNapcatContainerService, + new ToolsService(), + ); + jest + .spyOn((refreshService as any).toolsService, 'sleep') + .mockResolvedValue(undefined); + jest.spyOn(refreshService as any, 'getLoginStatus').mockResolvedValue({ + isLogin: false, + loginError: '密码登录处理中', + }); + + const status = await (refreshService as any).waitForPasswordLoginStatus({ + baseUrl: 'http://127.0.0.1:6103/', + id: 'container-stale-tail-no-since', + name: 'napcat-10001', + webuiToken: 'token', + }); + + expect(status.captchaUrl).toBeUndefined(); + expect(containerService.detectRuntimeCaptchaUrl).not.toHaveBeenCalled(); + }); + it('keeps password captcha pending before cleanup when status check throws captcha error', async () => { const captchaUrl = 'https://ti.qq.com/safe/tools/captcha/sms-verify-login?uin=10001'; @@ -1329,6 +1372,60 @@ describe('QqbotNapcatLoginService', () => { expect(sleep).not.toHaveBeenCalled(); }); + it('returns captcha status during password wait when processing logs already contain captcha url', async () => { + const restartedAt = Date.now() - 1500; + const captchaUrl = + 'https://ti.qq.com/safe/tools/captcha/sms-verify-login?uin=10001'; + const containerService = { + detectRuntimeCaptchaUrl: jest.fn().mockResolvedValue(captchaUrl), + }; + const refreshService = new QqbotNapcatLoginService( + { + get: jest.fn((key: string) => { + const values: Record = { + QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS: '1000', + QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS: '3000', + }; + return values[key] || ''; + }), + } as unknown as ConfigService, + {} as QqbotAccountService, + containerService as unknown as QqbotNapcatContainerService, + new ToolsService(), + ); + const getLoginStatus = jest + .spyOn(refreshService as any, 'getLoginStatus') + .mockResolvedValue({ + isLogin: false, + loginError: '密码登录处理中', + }); + const sleep = jest + .spyOn((refreshService as any).toolsService, 'sleep') + .mockResolvedValue(undefined); + + const status = await (refreshService as any).waitForPasswordLoginStatus( + { + baseUrl: 'http://127.0.0.1:6103/', + id: 'container-captcha-log-processing', + name: 'napcat-10001', + webuiToken: 'token', + }, + restartedAt, + ); + + expect(status).toMatchObject({ + captchaUrl, + isLogin: false, + loginError: '密码登录处理中', + }); + expect(containerService.detectRuntimeCaptchaUrl).toHaveBeenCalledWith( + expect.objectContaining({ name: 'napcat-10001' }), + restartedAt, + ); + expect(getLoginStatus).toHaveBeenCalledTimes(1); + expect(sleep).not.toHaveBeenCalled(); + }); + it('returns qrcode status immediately without waiting full password window', async () => { const refreshService = new QqbotNapcatLoginService( {