diff --git a/src/modules/qqbot/napcat/application/login/qqbot-napcat-login.service.ts b/src/modules/qqbot/napcat/application/login/qqbot-napcat-login.service.ts index 634ac89..34b3d06 100644 --- a/src/modules/qqbot/napcat/application/login/qqbot-napcat-login.service.ts +++ b/src/modules/qqbot/napcat/application/login/qqbot-napcat-login.service.ts @@ -30,6 +30,11 @@ import { QqbotAccountService } from '@/modules/qqbot/core/application/account/qq import { NapcatLoginStateStoreService } from '../../infrastructure/persistence/napcat-login-state-store.service'; import { QqbotNapcatContainerService } from '../../infrastructure/integration/container/qqbot-napcat-container.service'; +type PendingQrcodeUpdateOptions = { + clearStaleQrcode?: boolean; + requireFresh?: boolean; +}; + @Injectable() export class QqbotNapcatLoginService { private readonly fallbackLoginSessionStore = @@ -155,7 +160,10 @@ export class QqbotNapcatLoginService { */ async startRefresh(accountId: string) { const activeSession = this.findActiveRefreshSession(accountId); - if (activeSession) return this.toResult(activeSession); + if (activeSession) { + if (activeSession.qrcode) return this.refreshQrcode(activeSession.id); + return this.toResult(activeSession); + } const runningTask = this.refreshStartTasks[accountId]; if (runningTask) return runningTask; @@ -413,6 +421,7 @@ export class QqbotNapcatLoginService { session.errorMessage = status.loginError || undefined; if ( status.qrcodeurl && + session.mode !== 'refresh' && !this.toolsService.isNapcatExpiredQrcodeStatus(status) ) { const qrcodeChanged = session.qrcode !== status.qrcodeurl; @@ -425,10 +434,13 @@ export class QqbotNapcatLoginService { '登录二维码已生成', ); } - } else if (status.isOffline) { + } else if (status.isOffline && session.mode !== 'refresh') { session.qrcode = undefined; } else if (!this.toolsService.isNapcatExpiredQrcodeStatus(status)) { - await this.tryUpdatePendingQrcode(container, session, status); + await this.tryUpdatePendingQrcode(container, session, status, { + clearStaleQrcode: session.mode === 'refresh', + requireFresh: session.mode === 'refresh', + }); } this.persistLoginSession(session); return this.toResult(session); @@ -1916,15 +1928,18 @@ export class QqbotNapcatLoginService { * @param container - container 输入;驱动 `this.getQrcode()` 的 NapCat步骤。 * @param session - session 输入;使用 `qrcode`、`errorMessage` 字段生成结果。 * @param status - NapCat列表;使用 `qrcodeurl`、`loginError` 字段生成结果。 + * @param options - refresh 会话要求二维码必须区别于已知旧码,失败时避免继续把旧码回传给 Admin。 */ private async tryUpdatePendingQrcode( container: QqbotNapcatRuntime, session: QqbotLoginScanSession, status: NapcatLoginStatus, + options: PendingQrcodeUpdateOptions = {}, ) { + const requireFresh = options.requireFresh ?? !!session.qrcode; try { const qrcode = await this.getQrcode(container, false, { - requireFresh: !!session.qrcode, + requireFresh, staleQrcode: session.qrcode || status.qrcodeurl, }); if (qrcode) { @@ -1942,6 +1957,9 @@ export class QqbotNapcatLoginService { } } catch (err) { if (!this.toolsService.isNapcatTemporaryError(err)) throw err; + if (options.clearStaleQrcode || requireFresh) { + session.qrcode = undefined; + } session.errorMessage = session.errorMessage || 'NapCat 正在重新生成二维码,请稍后'; } diff --git a/test/qqbot/account/qqbot-napcat-login.service.spec.ts b/test/qqbot/account/qqbot-napcat-login.service.spec.ts index c9beb8f..96f02ff 100644 --- a/test/qqbot/account/qqbot-napcat-login.service.spec.ts +++ b/test/qqbot/account/qqbot-napcat-login.service.spec.ts @@ -276,6 +276,43 @@ describe('QqbotNapcatLoginService', () => { expect(prepareReloginQrcode).toHaveBeenCalledTimes(1); }); + it('does not return a stale qrcode when reusing an active refresh session', async () => { + const refreshService = new QqbotNapcatLoginService( + { get: jest.fn() } as unknown as ConfigService, + {} as QqbotAccountService, + {} as QqbotNapcatContainerService, + new ToolsService(), + ); + (refreshService as any).sessions.set('session-active-stale-qrcode', { + accountId: 'account-1', + containerId: 'container-1', + containerName: 'napcat-1', + createdAt: Date.now(), + expiresAt: Date.now() + 60_000, + id: 'session-active-stale-qrcode', + mode: 'refresh', + qrcode: 'old-qrcode', + status: 'pending', + webuiPort: 6101, + }); + const refreshQrcode = jest + .spyOn(refreshService, 'refreshQrcode') + .mockResolvedValue({ + errorMessage: 'NapCat 正在重新生成二维码,请稍后刷新或等待自动更新', + mode: 'refresh', + qrcode: undefined, + sessionId: 'session-active-stale-qrcode', + status: 'pending', + }); + + const result = await refreshService.startRefresh('account-1'); + + expect(result.sessionId).toBe('session-active-stale-qrcode'); + expect(result.qrcode).toBeUndefined(); + expect(result.errorMessage).toContain('正在重新生成二维码'); + expect(refreshQrcode).toHaveBeenCalledWith('session-active-stale-qrcode'); + }); + it('shares an in-flight refresh task before the first session is created', async () => { const account = { id: 'account-1', @@ -453,6 +490,9 @@ describe('QqbotNapcatLoginService', () => { isLogin: false, qrcodeurl: 'fresh-qrcode-after-restart', }); + jest + .spyOn(refreshService as any, 'getQrcode') + .mockResolvedValue('fresh-qrcode-after-restart'); const result = await refreshService.status(session.id); @@ -3405,6 +3445,9 @@ describe('QqbotNapcatLoginService', () => { isLogin: false, qrcodeurl: 'status-qrcode', }); + jest + .spyOn(syncService as any, 'getQrcode') + .mockResolvedValue('status-qrcode'); const result = await syncService.status('session-sync-qrcode-status'); @@ -3701,6 +3744,42 @@ describe('QqbotNapcatLoginService', () => { ).rejects.toThrow('NapCat WebUI 登录态仍阻止生成新二维码'); }); + it('keeps refresh status pending instead of reusing stale WebUI qrcode', async () => { + (service as any).sessions.set('session-status-stale-qrcode', { + accountId: 'account-1', + containerId: 'container-status-stale', + containerName: 'napcat-status-stale', + createdAt: Date.now(), + expiresAt: Date.now() + 60_000, + id: 'session-status-stale-qrcode', + mode: 'refresh', + qrcode: 'old-qrcode', + status: 'pending', + webuiPort: 6101, + }); + const container = { id: 'container-status-stale' }; + jest + .spyOn(service as any, 'getSessionContainer') + .mockResolvedValue(container); + jest.spyOn(service as any, 'getLoginStatus').mockResolvedValue({ + isLogin: false, + qrcodeurl: 'old-qrcode', + }); + const getQrcode = jest + .spyOn(service as any, 'getQrcode') + .mockRejectedValue(new Error('NapCat 二维码仍未刷新')); + + const result = await service.status('session-status-stale-qrcode'); + + expect(result.status).toBe('pending'); + expect(result.qrcode).toBeUndefined(); + expect(result.errorMessage).toContain('正在重新生成二维码'); + expect(getQrcode).toHaveBeenCalledWith(container, false, { + requireFresh: true, + staleQrcode: 'old-qrcode', + }); + }); + it('does not replace current qrcode with expired status qrcode', async () => { (service as any).sessions.set('session-2', { containerId: 'container-2',