fix(api): 修复QQBot被踢下线扫码更新
This commit is contained in:
parent
8a03e8c8b9
commit
0ddf785b8b
@ -39,6 +39,7 @@ NAPCAT_WEBUI_TOKEN=
|
|||||||
NAPCAT_WEBUI_TIMEOUT_MS=8000
|
NAPCAT_WEBUI_TIMEOUT_MS=8000
|
||||||
NAPCAT_LOGIN_QR_EXPIRE_MS=120000
|
NAPCAT_LOGIN_QR_EXPIRE_MS=120000
|
||||||
NAPCAT_WEBUI_READY_RETRIES=10
|
NAPCAT_WEBUI_READY_RETRIES=10
|
||||||
|
NAPCAT_WEBUI_RESTART_DELAY_MS=3000
|
||||||
QQBOT_NAPCAT_CONTAINER_MODE=
|
QQBOT_NAPCAT_CONTAINER_MODE=
|
||||||
QQBOT_NAPCAT_SSH_TARGET=nas
|
QQBOT_NAPCAT_SSH_TARGET=nas
|
||||||
QQBOT_NAPCAT_SSH_PORT=
|
QQBOT_NAPCAT_SSH_PORT=
|
||||||
|
|||||||
@ -42,6 +42,9 @@ describe('QqbotNapcatLoginService', () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(service as any, 'getSessionContainer')
|
.spyOn(service as any, 'getSessionContainer')
|
||||||
.mockResolvedValue({ id: 'container-1' });
|
.mockResolvedValue({ id: 'container-1' });
|
||||||
|
jest.spyOn(service as any, 'getLoginStatus').mockResolvedValue({
|
||||||
|
isLogin: false,
|
||||||
|
});
|
||||||
jest
|
jest
|
||||||
.spyOn(service as any, 'callRefreshQrcode')
|
.spyOn(service as any, 'callRefreshQrcode')
|
||||||
.mockResolvedValue('new-qrcode-image');
|
.mockResolvedValue('new-qrcode-image');
|
||||||
@ -108,6 +111,10 @@ describe('QqbotNapcatLoginService', () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(service as any, 'getSessionContainer')
|
.spyOn(service as any, 'getSessionContainer')
|
||||||
.mockResolvedValue(container);
|
.mockResolvedValue(container);
|
||||||
|
jest.spyOn(service as any, 'getLoginStatus').mockResolvedValue({
|
||||||
|
isLogin: false,
|
||||||
|
qrcodeurl: 'old-qrcode',
|
||||||
|
});
|
||||||
jest.spyOn(service as any, 'callRefreshQrcode').mockResolvedValue('');
|
jest.spyOn(service as any, 'callRefreshQrcode').mockResolvedValue('');
|
||||||
jest.spyOn(service as any, 'getQrcode').mockResolvedValue('new-qrcode');
|
jest.spyOn(service as any, 'getQrcode').mockResolvedValue('new-qrcode');
|
||||||
|
|
||||||
@ -120,6 +127,59 @@ describe('QqbotNapcatLoginService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('restarts the existing NapCat container before qrcode refresh when account is kicked offline', async () => {
|
||||||
|
const container = {
|
||||||
|
baseUrl: 'http://127.0.0.1:6103/',
|
||||||
|
id: 'container-3',
|
||||||
|
name: 'napcat-10001',
|
||||||
|
};
|
||||||
|
const containerService = {
|
||||||
|
restartRuntimeContainer: jest.fn().mockResolvedValue(true),
|
||||||
|
};
|
||||||
|
const refreshService = new QqbotNapcatLoginService(
|
||||||
|
{ get: jest.fn() } as unknown as ConfigService,
|
||||||
|
{} as QqbotAccountService,
|
||||||
|
containerService as unknown as QqbotNapcatContainerService,
|
||||||
|
);
|
||||||
|
jest.spyOn(refreshService as any, 'sleep').mockResolvedValue(undefined);
|
||||||
|
jest.spyOn(refreshService as any, 'getLoginStatus').mockResolvedValue({
|
||||||
|
isLogin: false,
|
||||||
|
qrcodeurl: 'new-status-qrcode',
|
||||||
|
});
|
||||||
|
jest
|
||||||
|
.spyOn(refreshService as any, 'callRefreshQrcode')
|
||||||
|
.mockResolvedValue('');
|
||||||
|
jest
|
||||||
|
.spyOn(refreshService as any, 'getQrcode')
|
||||||
|
.mockResolvedValue('new-qrcode');
|
||||||
|
|
||||||
|
const result = await (refreshService as any).refreshOrGetQrcode(
|
||||||
|
container,
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
fallbackStatus: {
|
||||||
|
isLogin: false,
|
||||||
|
isOffline: true,
|
||||||
|
loginError: '您的账号已在另一台终端登录',
|
||||||
|
qrcodeurl: 'old-qrcode',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe('new-qrcode');
|
||||||
|
expect(containerService.restartRuntimeContainer).toHaveBeenCalledWith(
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
expect((refreshService as any).getQrcode).toHaveBeenCalledWith(
|
||||||
|
container,
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
requireFresh: true,
|
||||||
|
staleQrcode: 'old-qrcode',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('retries while NapCat still exposes the stale qrcode', async () => {
|
it('retries while NapCat still exposes the stale qrcode', async () => {
|
||||||
jest.spyOn(service as any, 'sleep').mockResolvedValue(undefined);
|
jest.spyOn(service as any, 'sleep').mockResolvedValue(undefined);
|
||||||
jest
|
jest
|
||||||
|
|||||||
@ -125,9 +125,11 @@ export class QqbotNapcatLoginService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const container = await this.getSessionContainer(session);
|
const container = await this.getSessionContainer(session);
|
||||||
|
const loginStatus = await this.getLoginStatus(container);
|
||||||
session.qrcode = await this.refreshOrGetQrcode(container, true, {
|
session.qrcode = await this.refreshOrGetQrcode(container, true, {
|
||||||
|
fallbackStatus: loginStatus,
|
||||||
requireFresh: true,
|
requireFresh: true,
|
||||||
staleQrcode: session.qrcode,
|
staleQrcode: session.qrcode || loginStatus.qrcodeurl,
|
||||||
});
|
});
|
||||||
session.expiresAt = Date.now() + this.getSessionTtlMs();
|
session.expiresAt = Date.now() + this.getSessionTtlMs();
|
||||||
session.errorMessage = undefined;
|
session.errorMessage = undefined;
|
||||||
@ -436,10 +438,15 @@ export class QqbotNapcatLoginService {
|
|||||||
retry = false,
|
retry = false,
|
||||||
options: QrcodeRefreshOptions = {},
|
options: QrcodeRefreshOptions = {},
|
||||||
) {
|
) {
|
||||||
|
let fallbackStatus = options.fallbackStatus;
|
||||||
const lookupOptions: QrcodeLookupOptions = {
|
const lookupOptions: QrcodeLookupOptions = {
|
||||||
requireFresh: options.requireFresh,
|
requireFresh: options.requireFresh || fallbackStatus?.isOffline,
|
||||||
staleQrcode: options.staleQrcode,
|
staleQrcode: options.staleQrcode || fallbackStatus?.qrcodeurl,
|
||||||
};
|
};
|
||||||
|
if (fallbackStatus?.isOffline) {
|
||||||
|
await this.restartNapcatForLogin(container);
|
||||||
|
fallbackStatus = undefined;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const refreshedQrcode = await this.callRefreshQrcode(container, retry);
|
const refreshedQrcode = await this.callRefreshQrcode(container, retry);
|
||||||
if (refreshedQrcode) {
|
if (refreshedQrcode) {
|
||||||
@ -449,10 +456,10 @@ export class QqbotNapcatLoginService {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (
|
if (
|
||||||
!lookupOptions.requireFresh &&
|
!lookupOptions.requireFresh &&
|
||||||
options.fallbackStatus?.qrcodeurl &&
|
fallbackStatus?.qrcodeurl &&
|
||||||
!this.isExpiredQrcodeStatus(options.fallbackStatus)
|
!this.isExpiredQrcodeStatus(fallbackStatus)
|
||||||
) {
|
) {
|
||||||
return options.fallbackStatus.qrcodeurl;
|
return fallbackStatus.qrcodeurl;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
@ -505,8 +512,31 @@ export class QqbotNapcatLoginService {
|
|||||||
return this.requestNapcat<T>(container, path, body, credential);
|
return this.requestNapcat<T>(container, path, body, credential);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async restartNapcatForLogin(container: QqbotNapcatRuntime) {
|
||||||
|
const restartedByContainer =
|
||||||
|
await this.containerService.restartRuntimeContainer(container);
|
||||||
|
if (!restartedByContainer) {
|
||||||
|
try {
|
||||||
|
await this.postNapcat<Record<string, any> | null>(
|
||||||
|
container,
|
||||||
|
'/api/QQLogin/RestartNapCat',
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (!this.isTemporaryNapcatError(err)) throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.credentials.delete(this.getCredentialCacheKey(container));
|
||||||
|
await this.sleep(this.getRestartDelayMs());
|
||||||
|
await this.getLoginStatus(container, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCredentialCacheKey(container: QqbotNapcatRuntime) {
|
||||||
|
return container.id || container.baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
private async getCredential(container: QqbotNapcatRuntime) {
|
private async getCredential(container: QqbotNapcatRuntime) {
|
||||||
const cacheKey = container.id || container.baseUrl;
|
const cacheKey = this.getCredentialCacheKey(container);
|
||||||
const cached = this.credentials.get(cacheKey);
|
const cached = this.credentials.get(cacheKey);
|
||||||
if (cached && Date.now() < cached.expiresAt) {
|
if (cached && Date.now() < cached.expiresAt) {
|
||||||
return cached.credential;
|
return cached.credential;
|
||||||
@ -610,6 +640,12 @@ export class QqbotNapcatLoginService {
|
|||||||
return Number(this.configService.get('NAPCAT_WEBUI_TIMEOUT_MS') || 8000);
|
return Number(this.configService.get('NAPCAT_WEBUI_TIMEOUT_MS') || 8000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getRestartDelayMs() {
|
||||||
|
return Number(
|
||||||
|
this.configService.get('NAPCAT_WEBUI_RESTART_DELAY_MS') || 3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private getSelfId(info: NapcatLoginInfo) {
|
private getSelfId(info: NapcatLoginInfo) {
|
||||||
return `${info.uin || info.self_id || info.selfId || ''}`.trim();
|
return `${info.uin || info.self_id || info.selfId || ''}`.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -151,6 +151,27 @@ export class QqbotNapcatContainerService {
|
|||||||
return this.removeContainer(containerId);
|
return this.removeContainer(containerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async restartRuntimeContainer(runtime: QqbotNapcatRuntime) {
|
||||||
|
if (this.getManagedMode() !== 'ssh' || !runtime.id || !runtime.name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.runProcess(
|
||||||
|
'ssh',
|
||||||
|
[...this.getSshArgs(), 'docker', 'restart', runtime.name],
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
await this.containerRepository.update(
|
||||||
|
{ id: runtime.id },
|
||||||
|
{
|
||||||
|
lastError: null,
|
||||||
|
lastStartedAt: new Date(),
|
||||||
|
status: 'running',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private async removeContainer(containerId: string) {
|
private async removeContainer(containerId: string) {
|
||||||
const container = await this.containerRepository.findOne({
|
const container = await this.containerRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user