fix(api): 修复QQBot更新登录二维码刷新
This commit is contained in:
parent
3720819b96
commit
8a03e8c8b9
@ -53,6 +53,93 @@ describe('QqbotNapcatLoginService', () => {
|
|||||||
expect(getQrcode).not.toHaveBeenCalled();
|
expect(getQrcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reuses the existing account container when refreshing login', async () => {
|
||||||
|
const account = {
|
||||||
|
id: 'account-1',
|
||||||
|
selfId: '10001',
|
||||||
|
};
|
||||||
|
const existingContainer = {
|
||||||
|
id: 'container-current',
|
||||||
|
name: 'napcat-10001',
|
||||||
|
};
|
||||||
|
const accountService = {
|
||||||
|
findById: jest.fn().mockResolvedValue(account),
|
||||||
|
};
|
||||||
|
const containerService = {
|
||||||
|
prepareAccountContainer: jest.fn().mockResolvedValue(existingContainer),
|
||||||
|
};
|
||||||
|
const refreshService = new QqbotNapcatLoginService(
|
||||||
|
{ get: jest.fn() } as unknown as ConfigService,
|
||||||
|
accountService as unknown as QqbotAccountService,
|
||||||
|
containerService as unknown as QqbotNapcatContainerService,
|
||||||
|
);
|
||||||
|
const startScan = jest
|
||||||
|
.spyOn(refreshService as any, 'startScan')
|
||||||
|
.mockResolvedValue({ sessionId: 'session-refresh' });
|
||||||
|
|
||||||
|
await refreshService.startRefresh('account-1');
|
||||||
|
|
||||||
|
expect(containerService.prepareAccountContainer).toHaveBeenCalledWith(
|
||||||
|
account,
|
||||||
|
);
|
||||||
|
expect(startScan).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
accountId: 'account-1',
|
||||||
|
expectedSelfId: '10001',
|
||||||
|
mode: 'refresh',
|
||||||
|
},
|
||||||
|
existingContainer,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires a qrcode different from the current one when refreshing qrcode', async () => {
|
||||||
|
(service as any).sessions.set('session-refresh-qrcode', {
|
||||||
|
containerId: 'container-3',
|
||||||
|
containerName: 'napcat-3',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresAt: Date.now() + 60_000,
|
||||||
|
id: 'session-refresh-qrcode',
|
||||||
|
mode: 'refresh',
|
||||||
|
qrcode: 'old-qrcode',
|
||||||
|
status: 'pending',
|
||||||
|
webuiPort: 6103,
|
||||||
|
});
|
||||||
|
const container = { id: 'container-3' };
|
||||||
|
jest
|
||||||
|
.spyOn(service as any, 'getSessionContainer')
|
||||||
|
.mockResolvedValue(container);
|
||||||
|
jest.spyOn(service as any, 'callRefreshQrcode').mockResolvedValue('');
|
||||||
|
jest.spyOn(service as any, 'getQrcode').mockResolvedValue('new-qrcode');
|
||||||
|
|
||||||
|
const result = await service.refreshQrcode('session-refresh-qrcode');
|
||||||
|
|
||||||
|
expect(result.qrcode).toBe('new-qrcode');
|
||||||
|
expect((service as any).getQrcode).toHaveBeenCalledWith(container, true, {
|
||||||
|
requireFresh: true,
|
||||||
|
staleQrcode: 'old-qrcode',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retries while NapCat still exposes the stale qrcode', async () => {
|
||||||
|
jest.spyOn(service as any, 'sleep').mockResolvedValue(undefined);
|
||||||
|
jest
|
||||||
|
.spyOn(service as any, 'postNapcat')
|
||||||
|
.mockResolvedValueOnce({ qrcode: 'old-qrcode' })
|
||||||
|
.mockResolvedValueOnce({ qrcode: 'new-qrcode' });
|
||||||
|
|
||||||
|
const result = await (service as any).getQrcode(
|
||||||
|
{ id: 'container-4' },
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
requireFresh: true,
|
||||||
|
staleQrcode: 'old-qrcode',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe('new-qrcode');
|
||||||
|
expect((service as any).postNapcat).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
it('does not replace current qrcode with expired status qrcode', async () => {
|
it('does not replace current qrcode with expired status qrcode', async () => {
|
||||||
(service as any).sessions.set('session-2', {
|
(service as any).sessions.set('session-2', {
|
||||||
containerId: 'container-2',
|
containerId: 'container-2',
|
||||||
|
|||||||
@ -57,6 +57,15 @@ type QqbotLoginScanSession = {
|
|||||||
webuiPort?: null | number;
|
webuiPort?: null | number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type QrcodeLookupOptions = {
|
||||||
|
requireFresh?: boolean;
|
||||||
|
staleQrcode?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type QrcodeRefreshOptions = QrcodeLookupOptions & {
|
||||||
|
fallbackStatus?: NapcatLoginStatus;
|
||||||
|
};
|
||||||
|
|
||||||
export type QqbotLoginScanResult = {
|
export type QqbotLoginScanResult = {
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
containerId?: string;
|
containerId?: string;
|
||||||
@ -116,7 +125,10 @@ export class QqbotNapcatLoginService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const container = await this.getSessionContainer(session);
|
const container = await this.getSessionContainer(session);
|
||||||
session.qrcode = await this.refreshOrGetQrcode(container, true);
|
session.qrcode = await this.refreshOrGetQrcode(container, true, {
|
||||||
|
requireFresh: true,
|
||||||
|
staleQrcode: session.qrcode,
|
||||||
|
});
|
||||||
session.expiresAt = Date.now() + this.getSessionTtlMs();
|
session.expiresAt = Date.now() + this.getSessionTtlMs();
|
||||||
session.errorMessage = undefined;
|
session.errorMessage = undefined;
|
||||||
this.sessions.set(session.id, session);
|
this.sessions.set(session.id, session);
|
||||||
@ -174,11 +186,11 @@ export class QqbotNapcatLoginService {
|
|||||||
return this.completeLogin(session, container);
|
return this.completeLogin(session, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
const qrcode = await this.refreshOrGetQrcode(
|
const qrcode = await this.refreshOrGetQrcode(container, true, {
|
||||||
container,
|
fallbackStatus: loginStatus,
|
||||||
true,
|
requireFresh: this.isExpiredQrcodeStatus(loginStatus),
|
||||||
loginStatus,
|
staleQrcode: loginStatus.qrcodeurl,
|
||||||
);
|
});
|
||||||
const session = this.createSession({
|
const session = this.createSession({
|
||||||
...options,
|
...options,
|
||||||
container,
|
container,
|
||||||
@ -390,7 +402,11 @@ export class QqbotNapcatLoginService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getQrcode(container: QqbotNapcatRuntime, retry = false) {
|
private async getQrcode(
|
||||||
|
container: QqbotNapcatRuntime,
|
||||||
|
retry = false,
|
||||||
|
options: QrcodeLookupOptions = {},
|
||||||
|
) {
|
||||||
return this.executeNapcatRequest(retry, async () => {
|
return this.executeNapcatRequest(retry, async () => {
|
||||||
try {
|
try {
|
||||||
const data = await this.postNapcat<NapcatQrcode>(
|
const data = await this.postNapcat<NapcatQrcode>(
|
||||||
@ -399,16 +415,16 @@ export class QqbotNapcatLoginService {
|
|||||||
);
|
);
|
||||||
const qrcode = this.pickQrcode(data);
|
const qrcode = this.pickQrcode(data);
|
||||||
if (!qrcode) {
|
if (!qrcode) {
|
||||||
return this.getQrcodeFromStatus(container);
|
return this.getQrcodeFromStatus(container, options);
|
||||||
}
|
}
|
||||||
return qrcode;
|
return this.ensureFreshQrcode(qrcode, options);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (this.isAlreadyLoggedIn(err)) {
|
if (this.isAlreadyLoggedIn(err)) {
|
||||||
const status = await this.getLoginStatus(container);
|
const status = await this.getLoginStatus(container);
|
||||||
return status.qrcodeurl || '';
|
return this.ensureFreshQrcode(status.qrcodeurl || '', options);
|
||||||
}
|
}
|
||||||
if (this.isQrcodePending(err)) {
|
if (this.isQrcodePending(err)) {
|
||||||
return this.getQrcodeFromStatus(container);
|
return this.getQrcodeFromStatus(container, options);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
@ -418,31 +434,63 @@ export class QqbotNapcatLoginService {
|
|||||||
private async refreshOrGetQrcode(
|
private async refreshOrGetQrcode(
|
||||||
container: QqbotNapcatRuntime,
|
container: QqbotNapcatRuntime,
|
||||||
retry = false,
|
retry = false,
|
||||||
fallbackStatus?: NapcatLoginStatus,
|
options: QrcodeRefreshOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const lookupOptions: QrcodeLookupOptions = {
|
||||||
|
requireFresh: options.requireFresh,
|
||||||
|
staleQrcode: options.staleQrcode,
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const refreshedQrcode = await this.callRefreshQrcode(container, retry);
|
const refreshedQrcode = await this.callRefreshQrcode(container, retry);
|
||||||
if (refreshedQrcode) return refreshedQrcode;
|
if (refreshedQrcode) {
|
||||||
return await this.getQrcode(container, retry);
|
return this.ensureFreshQrcode(refreshedQrcode, lookupOptions);
|
||||||
|
}
|
||||||
|
return await this.getQrcode(container, retry, lookupOptions);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (
|
if (
|
||||||
fallbackStatus?.qrcodeurl &&
|
!lookupOptions.requireFresh &&
|
||||||
!this.isExpiredQrcodeStatus(fallbackStatus)
|
options.fallbackStatus?.qrcodeurl &&
|
||||||
|
!this.isExpiredQrcodeStatus(options.fallbackStatus)
|
||||||
) {
|
) {
|
||||||
return fallbackStatus.qrcodeurl;
|
return options.fallbackStatus.qrcodeurl;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getQrcodeFromStatus(container: QqbotNapcatRuntime) {
|
private async getQrcodeFromStatus(
|
||||||
|
container: QqbotNapcatRuntime,
|
||||||
|
options: QrcodeLookupOptions = {},
|
||||||
|
) {
|
||||||
const status = await this.getLoginStatus(container);
|
const status = await this.getLoginStatus(container);
|
||||||
if (status.qrcodeurl && !this.isExpiredQrcodeStatus(status)) {
|
if (status.qrcodeurl && !this.isExpiredQrcodeStatus(status)) {
|
||||||
return status.qrcodeurl;
|
return this.ensureFreshQrcode(status.qrcodeurl, options);
|
||||||
|
}
|
||||||
|
if (options.requireFresh && status.qrcodeurl) {
|
||||||
|
throw new Error('NapCat 二维码仍未刷新');
|
||||||
}
|
}
|
||||||
throwVbenError('NapCat 未返回登录二维码');
|
throwVbenError('NapCat 未返回登录二维码');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ensureFreshQrcode(qrcode: string, options: QrcodeLookupOptions = {}) {
|
||||||
|
const normalized = `${qrcode || ''}`.trim();
|
||||||
|
if (options.requireFresh && !normalized) {
|
||||||
|
throw new Error('NapCat 二维码仍未刷新');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
normalized &&
|
||||||
|
options.requireFresh &&
|
||||||
|
this.isSameQrcode(normalized, options.staleQrcode)
|
||||||
|
) {
|
||||||
|
throw new Error('NapCat 二维码仍未刷新');
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSameQrcode(left: string, right?: string) {
|
||||||
|
return !!right && left.trim() === right.trim();
|
||||||
|
}
|
||||||
|
|
||||||
private pickQrcode(data?: NapcatQrcode | null) {
|
private pickQrcode(data?: NapcatQrcode | null) {
|
||||||
if (!data) return '';
|
if (!data) return '';
|
||||||
return `${data.qrcode || data.qrcodeurl || data.url || ''}`.trim();
|
return `${data.qrcode || data.qrcodeurl || data.url || ''}`.trim();
|
||||||
@ -582,6 +630,7 @@ export class QqbotNapcatLoginService {
|
|||||||
'ETIMEDOUT',
|
'ETIMEDOUT',
|
||||||
'NapCat 请求超时',
|
'NapCat 请求超时',
|
||||||
'NapCat 未返回登录二维码',
|
'NapCat 未返回登录二维码',
|
||||||
|
'NapCat 二维码仍未刷新',
|
||||||
'QRCode Get Error',
|
'QRCode Get Error',
|
||||||
'socket hang up',
|
'socket hang up',
|
||||||
].some((keyword) => message.includes(keyword));
|
].some((keyword) => message.includes(keyword));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user