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_LOGIN_QR_EXPIRE_MS=120000
|
||||
NAPCAT_WEBUI_READY_RETRIES=10
|
||||
NAPCAT_WEBUI_RESTART_DELAY_MS=3000
|
||||
QQBOT_NAPCAT_CONTAINER_MODE=
|
||||
QQBOT_NAPCAT_SSH_TARGET=nas
|
||||
QQBOT_NAPCAT_SSH_PORT=
|
||||
|
||||
@ -42,6 +42,9 @@ describe('QqbotNapcatLoginService', () => {
|
||||
jest
|
||||
.spyOn(service as any, 'getSessionContainer')
|
||||
.mockResolvedValue({ id: 'container-1' });
|
||||
jest.spyOn(service as any, 'getLoginStatus').mockResolvedValue({
|
||||
isLogin: false,
|
||||
});
|
||||
jest
|
||||
.spyOn(service as any, 'callRefreshQrcode')
|
||||
.mockResolvedValue('new-qrcode-image');
|
||||
@ -108,6 +111,10 @@ describe('QqbotNapcatLoginService', () => {
|
||||
jest
|
||||
.spyOn(service as any, 'getSessionContainer')
|
||||
.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, '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 () => {
|
||||
jest.spyOn(service as any, 'sleep').mockResolvedValue(undefined);
|
||||
jest
|
||||
|
||||
@ -125,9 +125,11 @@ export class QqbotNapcatLoginService {
|
||||
}
|
||||
|
||||
const container = await this.getSessionContainer(session);
|
||||
const loginStatus = await this.getLoginStatus(container);
|
||||
session.qrcode = await this.refreshOrGetQrcode(container, true, {
|
||||
fallbackStatus: loginStatus,
|
||||
requireFresh: true,
|
||||
staleQrcode: session.qrcode,
|
||||
staleQrcode: session.qrcode || loginStatus.qrcodeurl,
|
||||
});
|
||||
session.expiresAt = Date.now() + this.getSessionTtlMs();
|
||||
session.errorMessage = undefined;
|
||||
@ -436,10 +438,15 @@ export class QqbotNapcatLoginService {
|
||||
retry = false,
|
||||
options: QrcodeRefreshOptions = {},
|
||||
) {
|
||||
let fallbackStatus = options.fallbackStatus;
|
||||
const lookupOptions: QrcodeLookupOptions = {
|
||||
requireFresh: options.requireFresh,
|
||||
staleQrcode: options.staleQrcode,
|
||||
requireFresh: options.requireFresh || fallbackStatus?.isOffline,
|
||||
staleQrcode: options.staleQrcode || fallbackStatus?.qrcodeurl,
|
||||
};
|
||||
if (fallbackStatus?.isOffline) {
|
||||
await this.restartNapcatForLogin(container);
|
||||
fallbackStatus = undefined;
|
||||
}
|
||||
try {
|
||||
const refreshedQrcode = await this.callRefreshQrcode(container, retry);
|
||||
if (refreshedQrcode) {
|
||||
@ -449,10 +456,10 @@ export class QqbotNapcatLoginService {
|
||||
} catch (err) {
|
||||
if (
|
||||
!lookupOptions.requireFresh &&
|
||||
options.fallbackStatus?.qrcodeurl &&
|
||||
!this.isExpiredQrcodeStatus(options.fallbackStatus)
|
||||
fallbackStatus?.qrcodeurl &&
|
||||
!this.isExpiredQrcodeStatus(fallbackStatus)
|
||||
) {
|
||||
return options.fallbackStatus.qrcodeurl;
|
||||
return fallbackStatus.qrcodeurl;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
@ -505,8 +512,31 @@ export class QqbotNapcatLoginService {
|
||||
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) {
|
||||
const cacheKey = container.id || container.baseUrl;
|
||||
const cacheKey = this.getCredentialCacheKey(container);
|
||||
const cached = this.credentials.get(cacheKey);
|
||||
if (cached && Date.now() < cached.expiresAt) {
|
||||
return cached.credential;
|
||||
@ -610,6 +640,12 @@ export class QqbotNapcatLoginService {
|
||||
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) {
|
||||
return `${info.uin || info.self_id || info.selfId || ''}`.trim();
|
||||
}
|
||||
|
||||
@ -151,6 +151,27 @@ export class QqbotNapcatContainerService {
|
||||
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) {
|
||||
const container = await this.containerRepository.findOne({
|
||||
where: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user