fix: 阻止NapCat更新登录复用旧二维码

This commit is contained in:
sunlei 2026-06-23 10:27:41 +08:00
parent 2eea7b254b
commit 4942cfd010
2 changed files with 101 additions and 4 deletions

View File

@ -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 正在重新生成二维码,请稍后';
}

View File

@ -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',