fix: 修复NapCat验证码日志捕获
This commit is contained in:
parent
492bf4b682
commit
3dec63b5d2
@ -1266,7 +1266,10 @@ export class QqbotNapcatLoginService {
|
|||||||
'processing',
|
'processing',
|
||||||
'等待 NapCat 密码登录结果',
|
'等待 NapCat 密码登录结果',
|
||||||
);
|
);
|
||||||
loginStatus = await this.waitForPasswordLoginStatus(container);
|
loginStatus = await this.waitForPasswordLoginStatus(
|
||||||
|
container,
|
||||||
|
session.lastRestartedAt,
|
||||||
|
);
|
||||||
|
|
||||||
if (loginStatus.isLogin) {
|
if (loginStatus.isLogin) {
|
||||||
loginInfo = await this.getLoginInfo(container);
|
loginInfo = await this.getLoginInfo(container);
|
||||||
@ -1413,6 +1416,32 @@ export class QqbotNapcatLoginService {
|
|||||||
) {
|
) {
|
||||||
const statusCaptchaUrl = this.getCaptchaUrlFromStatus(loginStatus);
|
const statusCaptchaUrl = this.getCaptchaUrlFromStatus(loginStatus);
|
||||||
if (statusCaptchaUrl) return statusCaptchaUrl;
|
if (statusCaptchaUrl) return statusCaptchaUrl;
|
||||||
|
const runtimeCaptchaUrl = await this.detectPasswordCaptchaUrl(
|
||||||
|
container,
|
||||||
|
sinceMs,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if (runtimeCaptchaUrl) return runtimeCaptchaUrl;
|
||||||
|
if (
|
||||||
|
!this.toolsService.isNapcatCaptchaRequiredMessage(loginStatus.loginError)
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return this.detectPasswordCaptchaUrl(container, sinceMs, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCaptchaUrlFromStatus(status: NapcatLoginStatus) {
|
||||||
|
return (
|
||||||
|
this.toolsService.toTrimmedString(status.captchaUrl) ||
|
||||||
|
this.toolsService.extractNapcatCaptchaUrl(status.loginError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectPasswordCaptchaUrl(
|
||||||
|
container: QqbotNapcatRuntime,
|
||||||
|
sinceMs?: number,
|
||||||
|
allowTailFallback = true,
|
||||||
|
) {
|
||||||
if (typeof this.containerService.detectRuntimeCaptchaUrl !== 'function') {
|
if (typeof this.containerService.detectRuntimeCaptchaUrl !== 'function') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -1421,20 +1450,12 @@ export class QqbotNapcatLoginService {
|
|||||||
sinceMs,
|
sinceMs,
|
||||||
);
|
);
|
||||||
if (recentCaptchaUrl) return recentCaptchaUrl;
|
if (recentCaptchaUrl) return recentCaptchaUrl;
|
||||||
if (
|
if (!allowTailFallback) return '';
|
||||||
!this.toolsService.isNapcatCaptchaRequiredMessage(loginStatus.loginError)
|
|
||||||
) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
(await this.containerService.detectRuntimeCaptchaUrl(container)) || ''
|
(await this.containerService.detectRuntimeCaptchaUrl(container)) || ''
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCaptchaUrlFromStatus(status: NapcatLoginStatus) {
|
|
||||||
return this.toolsService.extractNapcatCaptchaUrl(status.loginError);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isPasswordQrcodeChallenge(status: NapcatLoginStatus) {
|
private isPasswordQrcodeChallenge(status: NapcatLoginStatus) {
|
||||||
return (
|
return (
|
||||||
!!this.toolsService.toTrimmedString(status.qrcodeurl) ||
|
!!this.toolsService.toTrimmedString(status.qrcodeurl) ||
|
||||||
@ -1710,7 +1731,10 @@ export class QqbotNapcatLoginService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async waitForPasswordLoginStatus(container: QqbotNapcatRuntime) {
|
private async waitForPasswordLoginStatus(
|
||||||
|
container: QqbotNapcatRuntime,
|
||||||
|
sinceMs?: number,
|
||||||
|
) {
|
||||||
let latestStatus: NapcatLoginStatus = { isLogin: false };
|
let latestStatus: NapcatLoginStatus = { isLogin: false };
|
||||||
const attempts = this.getLoginPollAttempts(
|
const attempts = this.getLoginPollAttempts(
|
||||||
this.getPasswordLoginWaitMs(),
|
this.getPasswordLoginWaitMs(),
|
||||||
@ -1726,6 +1750,10 @@ export class QqbotNapcatLoginService {
|
|||||||
const errorMessage = this.toolsService.getErrorMessage(err);
|
const errorMessage = this.toolsService.getErrorMessage(err);
|
||||||
if (this.toolsService.isNapcatCaptchaRequiredMessage(errorMessage)) {
|
if (this.toolsService.isNapcatCaptchaRequiredMessage(errorMessage)) {
|
||||||
return {
|
return {
|
||||||
|
captchaUrl: await this.detectPasswordCaptchaUrl(
|
||||||
|
container,
|
||||||
|
sinceMs,
|
||||||
|
),
|
||||||
isLogin: false,
|
isLogin: false,
|
||||||
loginError: errorMessage,
|
loginError: errorMessage,
|
||||||
};
|
};
|
||||||
@ -1739,6 +1767,17 @@ export class QqbotNapcatLoginService {
|
|||||||
latestStatus.loginError,
|
latestStatus.loginError,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
if (
|
||||||
|
!this.getCaptchaUrlFromStatus(latestStatus) &&
|
||||||
|
this.toolsService.isNapcatCaptchaRequiredMessage(
|
||||||
|
latestStatus.loginError,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
latestStatus.captchaUrl = await this.detectPasswordCaptchaUrl(
|
||||||
|
container,
|
||||||
|
sinceMs,
|
||||||
|
);
|
||||||
|
}
|
||||||
return latestStatus;
|
return latestStatus;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export type NapcatLoginInfo = Record<string, any> & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type NapcatLoginStatus = {
|
export type NapcatLoginStatus = {
|
||||||
|
captchaUrl?: string;
|
||||||
isLogin?: boolean;
|
isLogin?: boolean;
|
||||||
isOffline?: boolean;
|
isOffline?: boolean;
|
||||||
loginError?: string;
|
loginError?: string;
|
||||||
|
|||||||
@ -748,6 +748,77 @@ describe('QqbotNapcatLoginService', () => {
|
|||||||
expect(session.captchaUrl).toBe(captchaUrl);
|
expect(session.captchaUrl).toBe(captchaUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not keep stale captcha from tail logs when current password status is processing', async () => {
|
||||||
|
const staleCaptchaUrl =
|
||||||
|
'https://ti.qq.com/safe/tools/captcha/sms-verify-login?uin=10001';
|
||||||
|
const container = {
|
||||||
|
baseUrl: 'http://127.0.0.1:6103/',
|
||||||
|
id: 'container-password-stale-captcha',
|
||||||
|
name: 'napcat-10001',
|
||||||
|
};
|
||||||
|
const containerService = {
|
||||||
|
detectRuntimeCaptchaUrl: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValueOnce(staleCaptchaUrl),
|
||||||
|
ensureRuntimeLoginEnv: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ changed: true, ok: true }),
|
||||||
|
resetRuntimeLoginState: jest.fn().mockResolvedValue(true),
|
||||||
|
restartRuntimeContainer: jest.fn().mockResolvedValue(true),
|
||||||
|
};
|
||||||
|
const refreshService = new QqbotNapcatLoginService(
|
||||||
|
{
|
||||||
|
get: jest.fn((key: string) => {
|
||||||
|
const values: Record<string, string> = {
|
||||||
|
QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS: '1',
|
||||||
|
QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS: '1',
|
||||||
|
QQBOT_NAPCAT_QUICK_LOGIN_WAIT_MS: '1',
|
||||||
|
};
|
||||||
|
return values[key] || '';
|
||||||
|
}),
|
||||||
|
} as unknown as ConfigService,
|
||||||
|
{} as QqbotAccountService,
|
||||||
|
containerService as unknown as QqbotNapcatContainerService,
|
||||||
|
new ToolsService(),
|
||||||
|
);
|
||||||
|
const session = (refreshService as any).createSession({
|
||||||
|
accountId: 'account-1',
|
||||||
|
container,
|
||||||
|
expectedSelfId: '10001',
|
||||||
|
mode: 'refresh',
|
||||||
|
preparingRelogin: true,
|
||||||
|
status: 'pending',
|
||||||
|
});
|
||||||
|
(refreshService as any).sessions.set(session.id, session);
|
||||||
|
jest
|
||||||
|
.spyOn((refreshService as any).toolsService, 'sleep')
|
||||||
|
.mockResolvedValue(undefined);
|
||||||
|
jest
|
||||||
|
.spyOn(refreshService as any, 'getLoginStatus')
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
isLogin: false,
|
||||||
|
loginError: '快速登录未找到历史会话',
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
isLogin: false,
|
||||||
|
loginError: '密码登录处理中',
|
||||||
|
});
|
||||||
|
jest
|
||||||
|
.spyOn(refreshService as any, 'refreshOrGetQrcode')
|
||||||
|
.mockResolvedValue('fallback-qrcode');
|
||||||
|
|
||||||
|
await (refreshService as any).prepareReloginQrcode(
|
||||||
|
session,
|
||||||
|
container,
|
||||||
|
'qq-password',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(session.captchaUrl).toBeUndefined();
|
||||||
|
expect(containerService.detectRuntimeCaptchaUrl).toHaveBeenCalledTimes(1);
|
||||||
|
expect(session.qrcode).toBe('fallback-qrcode');
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps password captcha pending before cleanup when status check throws captcha error', async () => {
|
it('keeps password captcha pending before cleanup when status check throws captcha error', async () => {
|
||||||
const captchaUrl =
|
const captchaUrl =
|
||||||
'https://ti.qq.com/safe/tools/captcha/sms-verify-login?uin=10001';
|
'https://ti.qq.com/safe/tools/captcha/sms-verify-login?uin=10001';
|
||||||
@ -1210,6 +1281,54 @@ describe('QqbotNapcatLoginService', () => {
|
|||||||
expect(sleep).not.toHaveBeenCalled();
|
expect(sleep).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures captcha url from runtime logs when status check throws captcha message without url', async () => {
|
||||||
|
const restartedAt = Date.now() - 1500;
|
||||||
|
const captchaUrl =
|
||||||
|
'https://ti.qq.com/safe/tools/captcha/sms-verify-login?uin=10001';
|
||||||
|
const containerService = {
|
||||||
|
detectRuntimeCaptchaUrl: jest.fn().mockResolvedValue(captchaUrl),
|
||||||
|
};
|
||||||
|
const refreshService = new QqbotNapcatLoginService(
|
||||||
|
{
|
||||||
|
get: jest.fn((key: string) => {
|
||||||
|
const values: Record<string, string> = {
|
||||||
|
QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS: '1000',
|
||||||
|
QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS: '3000',
|
||||||
|
};
|
||||||
|
return values[key] || '';
|
||||||
|
}),
|
||||||
|
} as unknown as ConfigService,
|
||||||
|
{} as QqbotAccountService,
|
||||||
|
containerService as unknown as QqbotNapcatContainerService,
|
||||||
|
new ToolsService(),
|
||||||
|
);
|
||||||
|
jest
|
||||||
|
.spyOn(refreshService as any, 'getLoginStatus')
|
||||||
|
.mockRejectedValueOnce(new Error('需要验证码'));
|
||||||
|
const sleep = jest.spyOn((refreshService as any).toolsService, 'sleep');
|
||||||
|
|
||||||
|
const status = await (refreshService as any).waitForPasswordLoginStatus(
|
||||||
|
{
|
||||||
|
baseUrl: 'http://127.0.0.1:6103/',
|
||||||
|
id: 'container-captcha-log',
|
||||||
|
name: 'napcat-10001',
|
||||||
|
webuiToken: 'token',
|
||||||
|
},
|
||||||
|
restartedAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(status).toMatchObject({
|
||||||
|
captchaUrl,
|
||||||
|
isLogin: false,
|
||||||
|
loginError: '需要验证码',
|
||||||
|
});
|
||||||
|
expect(containerService.detectRuntimeCaptchaUrl).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ name: 'napcat-10001' }),
|
||||||
|
restartedAt,
|
||||||
|
);
|
||||||
|
expect(sleep).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('returns qrcode status immediately without waiting full password window', async () => {
|
it('returns qrcode status immediately without waiting full password window', async () => {
|
||||||
const refreshService = new QqbotNapcatLoginService(
|
const refreshService = new QqbotNapcatLoginService(
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user