fix: 修复NapCat扫码状态回填
This commit is contained in:
parent
2e960f94d3
commit
0497233798
@ -28,6 +28,7 @@ import type {
|
|||||||
QqbotAccountAbilityType,
|
QqbotAccountAbilityType,
|
||||||
QqbotAccountListItem,
|
QqbotAccountListItem,
|
||||||
QqbotConnectionRole,
|
QqbotConnectionRole,
|
||||||
|
QqbotNapcatRuntimeLoginStatus,
|
||||||
QqbotNapcatWebuiStatus,
|
QqbotNapcatWebuiStatus,
|
||||||
QqbotRuntimeContainerStatus,
|
QqbotRuntimeContainerStatus,
|
||||||
} from '../../contract/qqbot.types';
|
} from '../../contract/qqbot.types';
|
||||||
@ -546,6 +547,28 @@ export class QqbotAccountService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists the QQ-login-only status reported by NapCat WebUI without changing OneBot or container connectivity.
|
||||||
|
* @param selfId - QQ account number whose NapCat login state was just probed.
|
||||||
|
* @param qqLoginStatus - WebUI login state such as qrcode pending, offline, or online.
|
||||||
|
* @param lastError - Optional human-facing reason; null clears stale risk/offline text when the state is actionable.
|
||||||
|
*/
|
||||||
|
async markQqLoginStatus(
|
||||||
|
selfId: string,
|
||||||
|
qqLoginStatus: QqbotNapcatRuntimeLoginStatus,
|
||||||
|
lastError?: null | string,
|
||||||
|
) {
|
||||||
|
const payload: Partial<QqbotAccount> = {
|
||||||
|
qqLoginStatus,
|
||||||
|
};
|
||||||
|
if (lastError !== undefined) {
|
||||||
|
payload.lastError = lastError
|
||||||
|
? this.toolsService.toColumnText(lastError, 500)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
await this.accountRepository.update({ selfId }, payload);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询 QQBot 核心数据。
|
* 查询 QQBot 核心数据。
|
||||||
* @param account - account 输入;驱动 `toolsService.toTrimmedString()` 的 QQBot步骤。
|
* @param account - account 输入;驱动 `toolsService.toTrimmedString()` 的 QQBot步骤。
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import type {
|
|||||||
QqbotLoginScanResult,
|
QqbotLoginScanResult,
|
||||||
QqbotLoginScanSession,
|
QqbotLoginScanSession,
|
||||||
QqbotLoginScanStatus,
|
QqbotLoginScanStatus,
|
||||||
|
QqbotNapcatRuntimeLoginStatus,
|
||||||
QqbotNapcatRuntime,
|
QqbotNapcatRuntime,
|
||||||
QrcodeLookupOptions,
|
QrcodeLookupOptions,
|
||||||
QrcodeRefreshOptions,
|
QrcodeRefreshOptions,
|
||||||
@ -242,6 +243,9 @@ export class QqbotNapcatLoginService {
|
|||||||
return this.toResult(session);
|
return this.toResult(session);
|
||||||
}
|
}
|
||||||
if (session.preparingRelogin) {
|
if (session.preparingRelogin) {
|
||||||
|
if (this.recoverStaleReloginPreparation(session)) {
|
||||||
|
return this.refreshQrcode(sessionId);
|
||||||
|
}
|
||||||
return this.keepSessionPending(
|
return this.keepSessionPending(
|
||||||
session,
|
session,
|
||||||
session.errorMessage || 'NapCat 正在尝试快速登录,请稍后',
|
session.errorMessage || 'NapCat 正在尝试快速登录,请稍后',
|
||||||
@ -272,6 +276,9 @@ export class QqbotNapcatLoginService {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (!loginStatus.isLogin) {
|
||||||
|
await this.syncSessionQqLoginStatus(session, loginStatus);
|
||||||
|
}
|
||||||
|
|
||||||
if (loginStatus.isOffline && session.mode !== 'refresh') {
|
if (loginStatus.isOffline && session.mode !== 'refresh') {
|
||||||
await this.restartNapcatForLogin(container, { waitForReady: false });
|
await this.restartNapcatForLogin(container, { waitForReady: false });
|
||||||
@ -354,6 +361,7 @@ export class QqbotNapcatLoginService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!status.isLogin) {
|
if (!status.isLogin) {
|
||||||
|
await this.syncSessionQqLoginStatus(session, status);
|
||||||
const captchaUrl = this.getCaptchaUrlFromStatus(status);
|
const captchaUrl = this.getCaptchaUrlFromStatus(status);
|
||||||
if (captchaUrl) {
|
if (captchaUrl) {
|
||||||
return this.keepPasswordCaptchaPending(
|
return this.keepPasswordCaptchaPending(
|
||||||
@ -912,6 +920,83 @@ export class QqbotNapcatLoginService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists the QQ-login-only status observed during a scan session without altering container or OneBot state.
|
||||||
|
* @param session - Login session that carries the target QQ number for refresh-login flows.
|
||||||
|
* @param status - Latest NapCat WebUI CheckLoginStatus response used as the QQ login source of truth.
|
||||||
|
*/
|
||||||
|
private async syncSessionQqLoginStatus(
|
||||||
|
session: QqbotLoginScanSession,
|
||||||
|
status: NapcatLoginStatus,
|
||||||
|
) {
|
||||||
|
const selfId = this.toolsService.toTrimmedString(session.expectedSelfId);
|
||||||
|
if (!selfId) return;
|
||||||
|
|
||||||
|
const marker = (
|
||||||
|
this.accountService as unknown as {
|
||||||
|
markQqLoginStatus?: (
|
||||||
|
selfId: string,
|
||||||
|
qqLoginStatus: QqbotNapcatRuntimeLoginStatus,
|
||||||
|
lastError?: null | string,
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
|
).markQqLoginStatus;
|
||||||
|
if (!marker) return;
|
||||||
|
|
||||||
|
const qqLoginStatus = this.toSessionQqLoginStatus(status);
|
||||||
|
const lastError = this.toSessionQqLoginError(status, qqLoginStatus);
|
||||||
|
await marker.call(this.accountService, selfId, qqLoginStatus, lastError);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a NapCat WebUI login probe into the account-table QQ login status vocabulary.
|
||||||
|
* @param status - Raw WebUI CheckLoginStatus payload returned by the current container.
|
||||||
|
* @returns Persistable QQ-login-only state.
|
||||||
|
*/
|
||||||
|
private toSessionQqLoginStatus(
|
||||||
|
status: NapcatLoginStatus,
|
||||||
|
): QqbotNapcatRuntimeLoginStatus {
|
||||||
|
const message = this.toolsService.toTrimmedString(status.loginError);
|
||||||
|
if (status.isLogin) return 'online';
|
||||||
|
if (
|
||||||
|
this.toolsService.isNapcatExpiredQrcodeStatus(status) ||
|
||||||
|
message.includes('二维码已过期')
|
||||||
|
) {
|
||||||
|
return 'qrcode_expired';
|
||||||
|
}
|
||||||
|
if (status.qrcodeurl) return 'qrcode_pending';
|
||||||
|
if (
|
||||||
|
status.isOffline ||
|
||||||
|
this.toolsService.isNapcatOfflineLoginMessage(message)
|
||||||
|
) {
|
||||||
|
return 'offline';
|
||||||
|
}
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the account error text that should accompany a QQ-login-only status update.
|
||||||
|
* @param status - Raw WebUI status containing the optional NapCat login error text.
|
||||||
|
* @param qqLoginStatus - Normalized state that decides whether stale error text should be cleared.
|
||||||
|
* @returns Null to clear stale errors, a reason string, or undefined when the account row should be left unchanged.
|
||||||
|
*/
|
||||||
|
private toSessionQqLoginError(
|
||||||
|
status: NapcatLoginStatus,
|
||||||
|
qqLoginStatus: QqbotNapcatRuntimeLoginStatus,
|
||||||
|
) {
|
||||||
|
const message = this.toolsService.toTrimmedString(status.loginError);
|
||||||
|
if (qqLoginStatus === 'online' || qqLoginStatus === 'qrcode_pending') {
|
||||||
|
return message || null;
|
||||||
|
}
|
||||||
|
if (qqLoginStatus === 'offline') {
|
||||||
|
return message || 'NapCat 账号已离线,请重新扫码登录';
|
||||||
|
}
|
||||||
|
if (qqLoginStatus === 'qrcode_expired') {
|
||||||
|
return message || 'NapCat 登录二维码已过期';
|
||||||
|
}
|
||||||
|
return message || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动New Device Verification。
|
* 启动New Device Verification。
|
||||||
* @param session - session 输入;使用 `status`、`captchaUrl`、`qrcode`、`deviceVerifyUrl` 字段生成结果。
|
* @param session - session 输入;使用 `status`、`captchaUrl`、`qrcode`、`deviceVerifyUrl` 字段生成结果。
|
||||||
@ -2178,6 +2263,10 @@ export class QqbotNapcatLoginService {
|
|||||||
'processing',
|
'processing',
|
||||||
'等待扫码确认',
|
'等待扫码确认',
|
||||||
);
|
);
|
||||||
|
await this.syncSessionQqLoginStatus(session, {
|
||||||
|
isLogin: false,
|
||||||
|
qrcodeurl: session.qrcode,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = this.toolsService.getErrorMessage(err);
|
const message = this.toolsService.getErrorMessage(err);
|
||||||
if (this.toolsService.isNapcatTemporaryError(err)) {
|
if (this.toolsService.isNapcatTemporaryError(err)) {
|
||||||
|
|||||||
@ -251,6 +251,11 @@ export class NapcatLoginStateStoreService {
|
|||||||
*/
|
*/
|
||||||
private async markCompleted(sessionId: string) {
|
private async markCompleted(sessionId: string) {
|
||||||
if (!this.loginSessionRepository) return;
|
if (!this.loginSessionRepository) return;
|
||||||
|
const current = await this.loginSessionRepository.findOne({
|
||||||
|
where: { sessionKey: sessionId },
|
||||||
|
});
|
||||||
|
if (!current || current.status === 'pending') return;
|
||||||
|
|
||||||
await this.loginSessionRepository.update(
|
await this.loginSessionRepository.update(
|
||||||
{ sessionKey: sessionId },
|
{ sessionKey: sessionId },
|
||||||
{
|
{
|
||||||
|
|||||||
@ -264,6 +264,37 @@ describe('NapCat persistent login state contract', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not mark a still-pending scan session completed when it is removed from the runtime cache', async () => {
|
||||||
|
const loginSessionRepository = createRepository<NapcatLoginSession>();
|
||||||
|
const store = new NapcatLoginStateStoreService(
|
||||||
|
loginSessionRepository as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
store.set({
|
||||||
|
containerId: 'container-pending-delete',
|
||||||
|
containerName: 'kt-qqbot-napcat-pending-delete',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresAt: Date.now() + 60_000,
|
||||||
|
id: 'pending-delete-session',
|
||||||
|
mode: 'refresh',
|
||||||
|
qrcode: 'https://txz.qq.com/p?k=fresh',
|
||||||
|
status: 'pending',
|
||||||
|
webuiPort: 6099,
|
||||||
|
});
|
||||||
|
await store.flushSessionWrites('pending-delete-session');
|
||||||
|
|
||||||
|
store.delete('pending-delete-session');
|
||||||
|
await store.flushSessionWrites('pending-delete-session');
|
||||||
|
|
||||||
|
expect(loginSessionRepository.rows[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
completedAt: null,
|
||||||
|
sessionKey: 'pending-delete-session',
|
||||||
|
status: 'pending',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('recovers captcha, new-device, and cleanup blockers after cache miss', async () => {
|
it('recovers captcha, new-device, and cleanup blockers after cache miss', async () => {
|
||||||
const loginSessionRepository = createRepository<NapcatLoginSession>();
|
const loginSessionRepository = createRepository<NapcatLoginSession>();
|
||||||
const loginChallengeRepository =
|
const loginChallengeRepository =
|
||||||
|
|||||||
@ -3107,6 +3107,87 @@ describe('QqbotNapcatLoginService', () => {
|
|||||||
expect(getLoginStatus).not.toHaveBeenCalled();
|
expect(getLoginStatus).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('refreshes stale relogin qrcode from NapCat status instead of staying in quick login pending', async () => {
|
||||||
|
(service as any).sessions.set('session-stale-relogin-qrcode', {
|
||||||
|
accountId: 'account-1',
|
||||||
|
containerId: 'container-stale-relogin',
|
||||||
|
containerName: 'napcat-stale-relogin',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
errorMessage: 'NapCat 正在尝试快速登录,请稍后',
|
||||||
|
expectedSelfId: '10001',
|
||||||
|
expiresAt: Date.now() + 60_000,
|
||||||
|
id: 'session-stale-relogin-qrcode',
|
||||||
|
lastRestartedAt: 1,
|
||||||
|
mode: 'refresh',
|
||||||
|
preparingRelogin: true,
|
||||||
|
status: 'pending',
|
||||||
|
webuiPort: 6110,
|
||||||
|
});
|
||||||
|
const container = { id: 'container-stale-relogin' };
|
||||||
|
jest
|
||||||
|
.spyOn(service as any, 'getSessionContainer')
|
||||||
|
.mockResolvedValue(container);
|
||||||
|
jest.spyOn(service as any, 'getLoginStatus').mockResolvedValue({
|
||||||
|
isLogin: false,
|
||||||
|
qrcodeurl: 'status-qrcode',
|
||||||
|
});
|
||||||
|
jest
|
||||||
|
.spyOn(service as any, 'callRefreshQrcode')
|
||||||
|
.mockResolvedValue('fresh-qrcode');
|
||||||
|
|
||||||
|
const result = await service.refreshQrcode('session-stale-relogin-qrcode');
|
||||||
|
|
||||||
|
expect(result.status).toBe('pending');
|
||||||
|
expect(result.qrcode).toBe('fresh-qrcode');
|
||||||
|
expect(result.errorMessage).toBeUndefined();
|
||||||
|
expect((service as any).sessions.get('session-stale-relogin-qrcode')).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
preparingRelogin: false,
|
||||||
|
qrcode: 'fresh-qrcode',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs account qrcode status when scan status reads a NapCat qrcode', async () => {
|
||||||
|
const accountService = {
|
||||||
|
markQqLoginStatus: jest.fn(),
|
||||||
|
};
|
||||||
|
const syncService = new QqbotNapcatLoginService(
|
||||||
|
{ get: jest.fn() } as unknown as ConfigService,
|
||||||
|
accountService as unknown as QqbotAccountService,
|
||||||
|
{} as QqbotNapcatContainerService,
|
||||||
|
new ToolsService(),
|
||||||
|
);
|
||||||
|
(syncService as any).sessions.set('session-sync-qrcode-status', {
|
||||||
|
accountId: 'account-1',
|
||||||
|
containerId: 'container-sync-status',
|
||||||
|
containerName: 'napcat-sync-status',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expectedSelfId: '10001',
|
||||||
|
expiresAt: Date.now() + 60_000,
|
||||||
|
id: 'session-sync-qrcode-status',
|
||||||
|
mode: 'refresh',
|
||||||
|
status: 'pending',
|
||||||
|
webuiPort: 6111,
|
||||||
|
});
|
||||||
|
jest
|
||||||
|
.spyOn(syncService as any, 'getSessionContainer')
|
||||||
|
.mockResolvedValue({ id: 'container-sync-status' });
|
||||||
|
jest.spyOn(syncService as any, 'getLoginStatus').mockResolvedValue({
|
||||||
|
isLogin: false,
|
||||||
|
qrcodeurl: 'status-qrcode',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await syncService.status('session-sync-qrcode-status');
|
||||||
|
|
||||||
|
expect(result.qrcode).toBe('status-qrcode');
|
||||||
|
expect(accountService.markQqLoginStatus).toHaveBeenCalledWith(
|
||||||
|
'10001',
|
||||||
|
'qrcode_pending',
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('replays scan progress events to late SSE subscribers', () => {
|
it('replays scan progress events to late SSE subscribers', () => {
|
||||||
const session = {
|
const session = {
|
||||||
containerId: 'container-events',
|
containerId: 'container-events',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user