feat: 增强注册流程,处理用户已存在错误,更新相关逻辑和测试用例
This commit is contained in:
@@ -502,6 +502,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
|||||||
进入邮箱轮询前,脚本会先确认认证页是否已经进入验证码页面;如果注册认证流程出现 `糟糕,出错了 / 操作超时(Operation timed out)`,或 `/email-verification` 上的 `405 / Route Error` 且带有 `重试` 按钮,会先通过共享恢复逻辑最多自动点击 5 次 `重试`,必要时回到密码页重新提交,再继续等待验证码页面。
|
进入邮箱轮询前,脚本会先确认认证页是否已经进入验证码页面;如果注册认证流程出现 `糟糕,出错了 / 操作超时(Operation timed out)`,或 `/email-verification` 上的 `405 / Route Error` 且带有 `重试` 按钮,会先通过共享恢复逻辑最多自动点击 5 次 `重试`,必要时回到密码页重新提交,再继续等待验证码页面。
|
||||||
|
|
||||||
在 `Auto` 模式下,如果 Step 4 当前轮失败,后台不会立刻丢弃这轮邮箱;而是沿用当前邮箱回到 Step 1 重新开始当前轮,避免刚拿到的邮箱被直接换掉。
|
在 `Auto` 模式下,如果 Step 4 当前轮失败,后台不会立刻丢弃这轮邮箱;而是沿用当前邮箱回到 Step 1 重新开始当前轮,避免刚拿到的邮箱被直接换掉。
|
||||||
|
但如果 Step 4 的认证重试页正文里出现 `user_already_exists`,则会直接判定“当前用户已存在”,不会点击 `重试`,而是立即结束当前轮;开启自动重试时会直接继续下一轮。
|
||||||
|
|
||||||
支持:
|
支持:
|
||||||
|
|
||||||
|
|||||||
@@ -4018,6 +4018,14 @@ function isRestartCurrentAttemptError(error) {
|
|||||||
return /当前邮箱已存在,需要重新开始新一轮/.test(message);
|
return /当前邮箱已存在,需要重新开始新一轮/.test(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSignupUserAlreadyExistsFailure(error) {
|
||||||
|
if (typeof loggingStatus !== 'undefined' && loggingStatus?.isSignupUserAlreadyExistsFailure) {
|
||||||
|
return loggingStatus.isSignupUserAlreadyExistsFailure(error);
|
||||||
|
}
|
||||||
|
const message = getErrorMessage(error);
|
||||||
|
return /SIGNUP_USER_ALREADY_EXISTS::|user_already_exists/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
function isStep9RecoverableAuthError(error) {
|
function isStep9RecoverableAuthError(error) {
|
||||||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||||||
return /STEP9_OAUTH_RETRY::/i.test(message)
|
return /STEP9_OAUTH_RETRY::/i.test(message)
|
||||||
@@ -5511,6 +5519,7 @@ const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoR
|
|||||||
hasSavedProgress,
|
hasSavedProgress,
|
||||||
isAddPhoneAuthFailure,
|
isAddPhoneAuthFailure,
|
||||||
isRestartCurrentAttemptError,
|
isRestartCurrentAttemptError,
|
||||||
|
isSignupUserAlreadyExistsFailure,
|
||||||
isStopError,
|
isStopError,
|
||||||
launchAutoRunTimerPlan,
|
launchAutoRunTimerPlan,
|
||||||
normalizeAutoRunFallbackThreadIntervalMinutes,
|
normalizeAutoRunFallbackThreadIntervalMinutes,
|
||||||
@@ -5818,6 +5827,9 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (step === 4) {
|
if (step === 4) {
|
||||||
|
if (isSignupUserAlreadyExistsFailure(err)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
step4RestartCount += 1;
|
step4RestartCount += 1;
|
||||||
const preservedState = await getState();
|
const preservedState = await getState();
|
||||||
const preservedEmail = String(preservedState.email || '').trim();
|
const preservedEmail = String(preservedState.email || '').trim();
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
hasSavedProgress,
|
hasSavedProgress,
|
||||||
isAddPhoneAuthFailure,
|
isAddPhoneAuthFailure,
|
||||||
isRestartCurrentAttemptError,
|
isRestartCurrentAttemptError,
|
||||||
|
isSignupUserAlreadyExistsFailure,
|
||||||
isStopError,
|
isStopError,
|
||||||
launchAutoRunTimerPlan,
|
launchAutoRunTimerPlan,
|
||||||
normalizeAutoRunFallbackThreadIntervalMinutes,
|
normalizeAutoRunFallbackThreadIntervalMinutes,
|
||||||
@@ -458,7 +459,9 @@
|
|||||||
const reason = getErrorMessage(err);
|
const reason = getErrorMessage(err);
|
||||||
roundSummary.failureReasons.push(reason);
|
roundSummary.failureReasons.push(reason);
|
||||||
const blockedByAddPhone = typeof isAddPhoneAuthFailure === 'function' && isAddPhoneAuthFailure(err);
|
const blockedByAddPhone = typeof isAddPhoneAuthFailure === 'function' && isAddPhoneAuthFailure(err);
|
||||||
const canRetry = !blockedByAddPhone && autoRunSkipFailures && attemptRun < maxAttemptsForRound;
|
const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function'
|
||||||
|
&& isSignupUserAlreadyExistsFailure(err);
|
||||||
|
const canRetry = !blockedByAddPhone && !blockedBySignupUserAlreadyExists && autoRunSkipFailures && attemptRun < maxAttemptsForRound;
|
||||||
|
|
||||||
await setState({
|
await setState({
|
||||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||||
@@ -499,6 +502,41 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (blockedBySignupUserAlreadyExists) {
|
||||||
|
roundSummary.status = 'failed';
|
||||||
|
roundSummary.finalFailureReason = reason;
|
||||||
|
await setState({
|
||||||
|
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||||
|
});
|
||||||
|
await appendRoundRecordIfNeeded('failed', reason);
|
||||||
|
cancelPendingCommands('当前轮因 user_already_exists 已终止。');
|
||||||
|
await broadcastStopToContentScripts();
|
||||||
|
if (!autoRunSkipFailures) {
|
||||||
|
await addLog(
|
||||||
|
`第 ${targetRun}/${totalRuns} 轮触发 user_already_exists/用户已存在,自动重试未开启,当前自动运行将停止。`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
stoppedEarly = true;
|
||||||
|
await broadcastAutoRunStatus('stopped', {
|
||||||
|
currentRun: targetRun,
|
||||||
|
totalRuns,
|
||||||
|
attemptRun,
|
||||||
|
sessionId: 0,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog(`第 ${targetRun}/${totalRuns} 轮触发 user_already_exists/用户已存在,本轮将直接失败并跳过剩余重试。`, 'warn');
|
||||||
|
await addLog(
|
||||||
|
targetRun < totalRuns
|
||||||
|
? `第 ${targetRun}/${totalRuns} 轮因 user_already_exists/用户已存在提前结束,自动流程将继续下一轮。`
|
||||||
|
: `第 ${targetRun}/${totalRuns} 轮因 user_already_exists/用户已存在提前结束,已无后续轮次,本次自动运行结束。`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
forceFreshTabsNextRun = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (canRetry) {
|
if (canRetry) {
|
||||||
const retryIndex = attemptRun;
|
const retryIndex = attemptRun;
|
||||||
if (isRestartCurrentAttemptError(err)) {
|
if (isRestartCurrentAttemptError(err)) {
|
||||||
|
|||||||
@@ -91,6 +91,11 @@
|
|||||||
return /当前邮箱已存在,需要重新开始新一轮/.test(message);
|
return /当前邮箱已存在,需要重新开始新一轮/.test(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSignupUserAlreadyExistsFailure(error) {
|
||||||
|
const message = getErrorMessage(error);
|
||||||
|
return /SIGNUP_USER_ALREADY_EXISTS::|user_already_exists/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
function isStep9RecoverableAuthError(error) {
|
function isStep9RecoverableAuthError(error) {
|
||||||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||||||
return /STEP9_OAUTH_RETRY::/i.test(message)
|
return /STEP9_OAUTH_RETRY::/i.test(message)
|
||||||
@@ -162,6 +167,7 @@
|
|||||||
hasSavedProgress,
|
hasSavedProgress,
|
||||||
isLegacyStep9RecoverableAuthError,
|
isLegacyStep9RecoverableAuthError,
|
||||||
isRestartCurrentAttemptError,
|
isRestartCurrentAttemptError,
|
||||||
|
isSignupUserAlreadyExistsFailure,
|
||||||
isStep9RecoverableAuthError,
|
isStep9RecoverableAuthError,
|
||||||
isStepDoneStatus,
|
isStepDoneStatus,
|
||||||
isVerificationMailPollingError,
|
isVerificationMailPollingError,
|
||||||
|
|||||||
@@ -70,8 +70,9 @@
|
|||||||
? routeErrorPattern.test(text)
|
? routeErrorPattern.test(text)
|
||||||
: false;
|
: false;
|
||||||
const maxCheckAttemptsBlocked = /max_check_attempts/i.test(text);
|
const maxCheckAttemptsBlocked = /max_check_attempts/i.test(text);
|
||||||
|
const userAlreadyExistsBlocked = /user_already_exists/i.test(text);
|
||||||
|
|
||||||
if (!titleMatched && !detailMatched && !routeErrorMatched && !maxCheckAttemptsBlocked) {
|
if (!titleMatched && !detailMatched && !routeErrorMatched && !maxCheckAttemptsBlocked && !userAlreadyExistsBlocked) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +85,7 @@
|
|||||||
detailMatched,
|
detailMatched,
|
||||||
routeErrorMatched,
|
routeErrorMatched,
|
||||||
maxCheckAttemptsBlocked,
|
maxCheckAttemptsBlocked,
|
||||||
|
userAlreadyExistsBlocked,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +155,12 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (retryState.userAlreadyExistsBlocked) {
|
||||||
|
throw new Error(
|
||||||
|
'SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (retryState.retryButton && retryState.retryEnabled) {
|
if (retryState.retryButton && retryState.retryEnabled) {
|
||||||
idlePollCount = 0;
|
idlePollCount = 0;
|
||||||
clickCount += 1;
|
clickCount += 1;
|
||||||
@@ -204,6 +212,12 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (finalRetryState.userAlreadyExistsBlocked) {
|
||||||
|
throw new Error(
|
||||||
|
'SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${logLabel || `步骤 ${step || '?'}:重试页恢复`}失败:已连续点击“重试” ${maxClickAttempts} 次,页面仍未恢复。URL: ${location.href}`
|
`${logLabel || `步骤 ${step || '?'}:重试页恢复`}失败:已连续点击“重试” ${maxClickAttempts} 次,页面仍未恢复。URL: ${location.href}`
|
||||||
);
|
);
|
||||||
|
|||||||
+34
-1
@@ -612,6 +612,7 @@ const STEP5_SUBMIT_ERROR_PATTERN = /无法根据该信息创建帐户|请重试|
|
|||||||
const AUTH_TIMEOUT_ERROR_TITLE_PATTERN = /糟糕,出错了|something\s+went\s+wrong|oops/i;
|
const AUTH_TIMEOUT_ERROR_TITLE_PATTERN = /糟糕,出错了|something\s+went\s+wrong|oops/i;
|
||||||
const AUTH_TIMEOUT_ERROR_DETAIL_PATTERN = /operation\s+timed\s+out|timed\s+out|请求超时|操作超时/i;
|
const AUTH_TIMEOUT_ERROR_DETAIL_PATTERN = /operation\s+timed\s+out|timed\s+out|请求超时|操作超时/i;
|
||||||
const AUTH_ROUTE_ERROR_PATTERN = /405\s+method\s+not\s+allowed|route\s+error.*405/i;
|
const AUTH_ROUTE_ERROR_PATTERN = /405\s+method\s+not\s+allowed|route\s+error.*405/i;
|
||||||
|
const SIGNUP_USER_ALREADY_EXISTS_ERROR_PREFIX = 'SIGNUP_USER_ALREADY_EXISTS::';
|
||||||
const SIGNUP_EMAIL_EXISTS_PATTERN = /与此电子邮件地址相关联的帐户已存在|account\s+associated\s+with\s+this\s+email\s+address\s+already\s+exists|email\s+address.*already\s+exists/i;
|
const SIGNUP_EMAIL_EXISTS_PATTERN = /与此电子邮件地址相关联的帐户已存在|account\s+associated\s+with\s+this\s+email\s+address\s+already\s+exists|email\s+address.*already\s+exists/i;
|
||||||
|
|
||||||
const authPageRecovery = self.MultiPageAuthPageRecovery?.createAuthPageRecovery?.({
|
const authPageRecovery = self.MultiPageAuthPageRecovery?.createAuthPageRecovery?.({
|
||||||
@@ -663,6 +664,12 @@ function getVerificationErrorText() {
|
|||||||
return messages.find((text) => INVALID_VERIFICATION_CODE_PATTERN.test(text)) || '';
|
return messages.find((text) => INVALID_VERIFICATION_CODE_PATTERN.test(text)) || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSignupUserAlreadyExistsError() {
|
||||||
|
return new Error(
|
||||||
|
`${SIGNUP_USER_ALREADY_EXISTS_ERROR_PREFIX}步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isStep5Ready() {
|
function isStep5Ready() {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
document.querySelector('input[name="name"], input[autocomplete="name"], input[name="birthday"], input[name="age"], [role="spinbutton"][data-type="year"]')
|
document.querySelector('input[name="name"], input[autocomplete="name"], input[name="birthday"], input[name="age"], [role="spinbutton"][data-type="year"]')
|
||||||
@@ -981,8 +988,9 @@ function getAuthTimeoutErrorPageState(options = {}) {
|
|||||||
const detailMatched = AUTH_TIMEOUT_ERROR_DETAIL_PATTERN.test(text);
|
const detailMatched = AUTH_TIMEOUT_ERROR_DETAIL_PATTERN.test(text);
|
||||||
const routeErrorMatched = AUTH_ROUTE_ERROR_PATTERN.test(text);
|
const routeErrorMatched = AUTH_ROUTE_ERROR_PATTERN.test(text);
|
||||||
const maxCheckAttemptsBlocked = /max_check_attempts/i.test(text);
|
const maxCheckAttemptsBlocked = /max_check_attempts/i.test(text);
|
||||||
|
const userAlreadyExistsBlocked = /user_already_exists/i.test(text);
|
||||||
|
|
||||||
if (!titleMatched && !detailMatched && !routeErrorMatched && !maxCheckAttemptsBlocked) {
|
if (!titleMatched && !detailMatched && !routeErrorMatched && !maxCheckAttemptsBlocked && !userAlreadyExistsBlocked) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -995,6 +1003,7 @@ function getAuthTimeoutErrorPageState(options = {}) {
|
|||||||
detailMatched,
|
detailMatched,
|
||||||
routeErrorMatched,
|
routeErrorMatched,
|
||||||
maxCheckAttemptsBlocked,
|
maxCheckAttemptsBlocked,
|
||||||
|
userAlreadyExistsBlocked,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1073,6 +1082,9 @@ async function recoverCurrentAuthRetryPage(payload = {}) {
|
|||||||
if (retryState.maxCheckAttemptsBlocked) {
|
if (retryState.maxCheckAttemptsBlocked) {
|
||||||
throw new Error('CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器');
|
throw new Error('CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器');
|
||||||
}
|
}
|
||||||
|
if (retryState.userAlreadyExistsBlocked) {
|
||||||
|
throw createSignupUserAlreadyExistsError();
|
||||||
|
}
|
||||||
if (retryState.retryButton && retryState.retryEnabled) {
|
if (retryState.retryButton && retryState.retryEnabled) {
|
||||||
idlePollCount = 0;
|
idlePollCount = 0;
|
||||||
clickCount += 1;
|
clickCount += 1;
|
||||||
@@ -1113,6 +1125,9 @@ async function recoverCurrentAuthRetryPage(payload = {}) {
|
|||||||
if (finalRetryState.maxCheckAttemptsBlocked) {
|
if (finalRetryState.maxCheckAttemptsBlocked) {
|
||||||
throw new Error('CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器');
|
throw new Error('CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器');
|
||||||
}
|
}
|
||||||
|
if (finalRetryState.userAlreadyExistsBlocked) {
|
||||||
|
throw createSignupUserAlreadyExistsError();
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(`${logLabel || `步骤 ${step || '?'}:重试页恢复`}失败:已连续点击“重试” ${maxClickAttempts} 次,页面仍未恢复。URL: ${location.href}`);
|
throw new Error(`${logLabel || `步骤 ${step || '?'}:重试页恢复`}失败:已连续点击“重试” ${maxClickAttempts} 次,页面仍未恢复。URL: ${location.href}`);
|
||||||
}
|
}
|
||||||
@@ -1430,6 +1445,7 @@ function inspectSignupVerificationState() {
|
|||||||
return {
|
return {
|
||||||
state: 'error',
|
state: 'error',
|
||||||
retryButton: timeoutPage?.retryButton || null,
|
retryButton: timeoutPage?.retryButton || null,
|
||||||
|
userAlreadyExistsBlocked: Boolean(timeoutPage?.userAlreadyExistsBlocked),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1503,6 +1519,9 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
|
|||||||
recoveryRound += 1;
|
recoveryRound += 1;
|
||||||
|
|
||||||
if (snapshot.state === 'error') {
|
if (snapshot.state === 'error') {
|
||||||
|
if (snapshot.userAlreadyExistsBlocked) {
|
||||||
|
throw createSignupUserAlreadyExistsError();
|
||||||
|
}
|
||||||
await recoverCurrentAuthRetryPage({
|
await recoverCurrentAuthRetryPage({
|
||||||
flow: 'signup',
|
flow: 'signup',
|
||||||
logLabel: `${prepareLogLabel}:检测到注册认证重试页,正在点击“重试”恢复(第 ${recoveryRound}/${maxRecoveryRounds} 次)`,
|
logLabel: `${prepareLogLabel}:检测到注册认证重试页,正在点击“重试”恢复(第 ${recoveryRound}/${maxRecoveryRounds} 次)`,
|
||||||
@@ -1549,6 +1568,13 @@ async function waitForVerificationSubmitOutcome(step, timeout) {
|
|||||||
while (Date.now() - start < resolvedTimeout) {
|
while (Date.now() - start < resolvedTimeout) {
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
|
|
||||||
|
if (step === 4) {
|
||||||
|
const signupRetryState = getCurrentAuthRetryPageState('signup');
|
||||||
|
if (signupRetryState?.userAlreadyExistsBlocked) {
|
||||||
|
throw createSignupUserAlreadyExistsError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const errorText = getVerificationErrorText();
|
const errorText = getVerificationErrorText();
|
||||||
if (errorText) {
|
if (errorText) {
|
||||||
return { invalidCode: true, errorText };
|
return { invalidCode: true, errorText };
|
||||||
@@ -1569,6 +1595,13 @@ async function waitForVerificationSubmitOutcome(step, timeout) {
|
|||||||
await sleep(150);
|
await sleep(150);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (step === 4) {
|
||||||
|
const signupRetryState = getCurrentAuthRetryPageState('signup');
|
||||||
|
if (signupRetryState?.userAlreadyExistsBlocked) {
|
||||||
|
throw createSignupUserAlreadyExistsError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isVerificationPageStillVisible()) {
|
if (isVerificationPageStillVisible()) {
|
||||||
return {
|
return {
|
||||||
invalidCode: true,
|
invalidCode: true,
|
||||||
|
|||||||
@@ -204,3 +204,25 @@ test('auth page recovery throws cloudflare security blocked error on max_check_a
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('auth page recovery throws signup user already exists error without clicking retry', async () => {
|
||||||
|
const state = {
|
||||||
|
clickCount: 0,
|
||||||
|
pageText: 'Something went wrong. user_already_exists.',
|
||||||
|
pathname: '/email-verification',
|
||||||
|
retryVisible: true,
|
||||||
|
};
|
||||||
|
const api = createRecoveryApi(state);
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => api.recoverAuthRetryPage({
|
||||||
|
logLabel: '步骤 4:检测到注册认证重试页,正在点击“重试”恢复',
|
||||||
|
pathPatterns: [/\/email-verification(?:[/?#]|$)/i],
|
||||||
|
step: 4,
|
||||||
|
timeoutMs: 1000,
|
||||||
|
}),
|
||||||
|
/SIGNUP_USER_ALREADY_EXISTS::/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(state.clickCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -168,3 +168,166 @@ test('auto-run controller skips add-phone failures to the next round instead of
|
|||||||
assert.equal(runtime.state.autoRunActive, false);
|
assert.equal(runtime.state.autoRunActive, false);
|
||||||
assert.equal(runtime.state.autoRunSessionId, 0);
|
assert.equal(runtime.state.autoRunSessionId, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('auto-run controller skips user_already_exists failures to the next round instead of retrying the same round', async () => {
|
||||||
|
const events = {
|
||||||
|
logs: [],
|
||||||
|
broadcasts: [],
|
||||||
|
accountRecords: [],
|
||||||
|
runCalls: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentState = {
|
||||||
|
stepStatuses: {},
|
||||||
|
vpsUrl: 'https://example.com/vps',
|
||||||
|
vpsPassword: 'secret',
|
||||||
|
customPassword: '',
|
||||||
|
autoRunSkipFailures: true,
|
||||||
|
autoRunFallbackThreadIntervalMinutes: 0,
|
||||||
|
autoRunDelayEnabled: false,
|
||||||
|
autoRunDelayMinutes: 30,
|
||||||
|
autoStepDelaySeconds: null,
|
||||||
|
mailProvider: '163',
|
||||||
|
emailGenerator: 'duck',
|
||||||
|
gmailBaseEmail: '',
|
||||||
|
mail2925BaseEmail: '',
|
||||||
|
emailPrefix: 'demo',
|
||||||
|
inbucketHost: '',
|
||||||
|
inbucketMailbox: '',
|
||||||
|
cloudflareDomain: '',
|
||||||
|
cloudflareDomains: [],
|
||||||
|
tabRegistry: {},
|
||||||
|
sourceLastUrls: {},
|
||||||
|
autoRunRoundSummaries: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const runtime = {
|
||||||
|
state: {
|
||||||
|
autoRunActive: false,
|
||||||
|
autoRunCurrentRun: 0,
|
||||||
|
autoRunTotalRuns: 1,
|
||||||
|
autoRunAttemptRun: 0,
|
||||||
|
autoRunSessionId: 0,
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
return { ...this.state };
|
||||||
|
},
|
||||||
|
set(updates = {}) {
|
||||||
|
this.state = { ...this.state, ...updates };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let sessionSeed = 0;
|
||||||
|
|
||||||
|
const controller = api.createAutoRunController({
|
||||||
|
addLog: async (message, level = 'info') => {
|
||||||
|
events.logs.push({ message, level });
|
||||||
|
},
|
||||||
|
appendAccountRunRecord: async (status, _state, reason) => {
|
||||||
|
events.accountRecords.push({ status, reason });
|
||||||
|
return { status, reason };
|
||||||
|
},
|
||||||
|
AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
|
||||||
|
AUTO_RUN_RETRY_DELAY_MS: 3000,
|
||||||
|
AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
|
||||||
|
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
|
||||||
|
broadcastAutoRunStatus: async (phase, payload = {}) => {
|
||||||
|
events.broadcasts.push({ phase, ...payload });
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
|
||||||
|
autoRunPhase: phase,
|
||||||
|
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
|
||||||
|
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
|
||||||
|
autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
|
||||||
|
autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
broadcastStopToContentScripts: async () => {},
|
||||||
|
cancelPendingCommands: () => {},
|
||||||
|
clearStopRequest: () => {},
|
||||||
|
createAutoRunSessionId: () => {
|
||||||
|
sessionSeed += 1;
|
||||||
|
return sessionSeed;
|
||||||
|
},
|
||||||
|
getAutoRunStatusPayload: (phase, payload = {}) => ({
|
||||||
|
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
|
||||||
|
autoRunPhase: phase,
|
||||||
|
autoRunCurrentRun: payload.currentRun ?? 0,
|
||||||
|
autoRunTotalRuns: payload.totalRuns ?? 1,
|
||||||
|
autoRunAttemptRun: payload.attemptRun ?? 0,
|
||||||
|
autoRunSessionId: payload.sessionId ?? 0,
|
||||||
|
}),
|
||||||
|
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||||
|
getFirstUnfinishedStep: () => 1,
|
||||||
|
getPendingAutoRunTimerPlan: () => null,
|
||||||
|
getRunningSteps: () => [],
|
||||||
|
getState: async () => ({
|
||||||
|
...currentState,
|
||||||
|
stepStatuses: { ...(currentState.stepStatuses || {}) },
|
||||||
|
tabRegistry: { ...(currentState.tabRegistry || {}) },
|
||||||
|
sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
|
||||||
|
}),
|
||||||
|
getStopRequested: () => false,
|
||||||
|
hasSavedProgress: () => false,
|
||||||
|
isAddPhoneAuthFailure: () => false,
|
||||||
|
isRestartCurrentAttemptError: () => false,
|
||||||
|
isSignupUserAlreadyExistsFailure: (error) => /SIGNUP_USER_ALREADY_EXISTS::|user_already_exists/i.test(error?.message || String(error || '')),
|
||||||
|
isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
|
||||||
|
launchAutoRunTimerPlan: async () => false,
|
||||||
|
normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
|
||||||
|
persistAutoRunTimerPlan: async () => ({}),
|
||||||
|
resetState: async () => {
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
stepStatuses: {},
|
||||||
|
tabRegistry: {},
|
||||||
|
sourceLastUrls: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
runAutoSequenceFromStep: async () => {
|
||||||
|
events.runCalls += 1;
|
||||||
|
if (events.runCalls === 1) {
|
||||||
|
throw new Error('SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
runtime,
|
||||||
|
setState: async (updates = {}) => {
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
...updates,
|
||||||
|
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
|
||||||
|
tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
|
||||||
|
sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfAutoRunSessionStopped: (sessionId) => {
|
||||||
|
if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
|
||||||
|
throw new Error('流程已被用户停止。');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
waitForRunningStepsToFinish: async () => currentState,
|
||||||
|
chrome: {
|
||||||
|
runtime: {
|
||||||
|
sendMessage() {
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await controller.autoRunLoop(2, {
|
||||||
|
autoRunSkipFailures: true,
|
||||||
|
mode: 'restart',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(events.runCalls, 2, 'user_already_exists failure should skip the current round and continue with the next round');
|
||||||
|
assert.equal(events.broadcasts.some(({ phase }) => phase === 'retrying'), false, 'user_already_exists failure should not enter same-round retrying');
|
||||||
|
assert.equal(events.accountRecords.length, 1, 'user_already_exists should still persist a failed round record');
|
||||||
|
assert.equal(events.accountRecords[0].status, 'failed');
|
||||||
|
assert.match(events.accountRecords[0].reason, /SIGNUP_USER_ALREADY_EXISTS::/);
|
||||||
|
assert.ok(events.logs.some(({ message }) => /继续下一轮/.test(message)));
|
||||||
|
assert.equal(runtime.state.autoRunActive, false);
|
||||||
|
assert.equal(runtime.state.autoRunSessionId, 0);
|
||||||
|
});
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ function extractFunction(name) {
|
|||||||
const bundle = [
|
const bundle = [
|
||||||
extractFunction('isAddPhoneAuthUrl'),
|
extractFunction('isAddPhoneAuthUrl'),
|
||||||
extractFunction('isAddPhoneAuthState'),
|
extractFunction('isAddPhoneAuthState'),
|
||||||
|
extractFunction('isSignupUserAlreadyExistsFailure'),
|
||||||
extractFunction('getPostStep6AutoRestartDecision'),
|
extractFunction('getPostStep6AutoRestartDecision'),
|
||||||
extractFunction('runAutoSequenceFromStep'),
|
extractFunction('runAutoSequenceFromStep'),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
@@ -207,3 +208,124 @@ return {
|
|||||||
assert.equal(currentState.password, 'Secret123!');
|
assert.equal(currentState.password, 'Secret123!');
|
||||||
assert.equal(events.logs.some(({ message }) => /沿用当前邮箱回到步骤 1 重新开始/.test(message)), true);
|
assert.equal(events.logs.some(({ message }) => /沿用当前邮箱回到步骤 1 重新开始/.test(message)), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('auto-run does not restart step 4 current attempt when user_already_exists is detected', async () => {
|
||||||
|
const api = new Function(`
|
||||||
|
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
|
||||||
|
const LAST_STEP_ID = 10;
|
||||||
|
const FINAL_OAUTH_CHAIN_START_STEP = 7;
|
||||||
|
const chrome = {
|
||||||
|
tabs: {
|
||||||
|
update: async () => {},
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
sendMessage: async () => {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentState = {
|
||||||
|
email: 'existing@example.com',
|
||||||
|
password: 'Secret123!',
|
||||||
|
mailProvider: '163',
|
||||||
|
stepStatuses: {
|
||||||
|
1: 'pending',
|
||||||
|
2: 'pending',
|
||||||
|
3: 'pending',
|
||||||
|
4: 'pending',
|
||||||
|
5: 'pending',
|
||||||
|
6: 'pending',
|
||||||
|
7: 'pending',
|
||||||
|
8: 'pending',
|
||||||
|
9: 'pending',
|
||||||
|
10: 'pending',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const events = {
|
||||||
|
steps: [],
|
||||||
|
invalidations: [],
|
||||||
|
logs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
async function addLog(message, level = 'info') {
|
||||||
|
events.logs.push({ message, level });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureAutoEmailReady() {
|
||||||
|
return currentState.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function broadcastAutoRunStatus() {}
|
||||||
|
|
||||||
|
async function getState() {
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setState(updates) {
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
...updates,
|
||||||
|
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStopError(error) {
|
||||||
|
return (error?.message || String(error || '')) === '流程已被用户停止。';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStepDoneStatus(status) {
|
||||||
|
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeStepAndWait(step) {
|
||||||
|
events.steps.push(step);
|
||||||
|
if (step === 4) {
|
||||||
|
throw new Error('SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTabId() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invalidateDownstreamAfterStepRestart(step, options = {}) {
|
||||||
|
events.invalidations.push({ step, options });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLoginAuthStateLabel(state) {
|
||||||
|
return state || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorMessage(error) {
|
||||||
|
return error?.message || String(error || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLoginAuthStateFromContent() {
|
||||||
|
return { state: 'password_page', url: 'https://auth.openai.com/log-in' };
|
||||||
|
}
|
||||||
|
|
||||||
|
${bundle}
|
||||||
|
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
try {
|
||||||
|
await runAutoSequenceFromStep(1, {
|
||||||
|
targetRun: 1,
|
||||||
|
totalRuns: 1,
|
||||||
|
attemptRuns: 1,
|
||||||
|
continued: false,
|
||||||
|
});
|
||||||
|
return { events, currentState, error: null };
|
||||||
|
} catch (error) {
|
||||||
|
return { events, currentState, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const result = await api.run();
|
||||||
|
|
||||||
|
assert.match(result.error, /SIGNUP_USER_ALREADY_EXISTS::/);
|
||||||
|
assert.deepStrictEqual(result.events.invalidations, []);
|
||||||
|
assert.deepStrictEqual(result.events.steps, [1, 2, 3, 4]);
|
||||||
|
assert.equal(result.events.logs.some(({ message }) => /沿用当前邮箱回到步骤 1 重新开始/.test(message)), false);
|
||||||
|
});
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ return {
|
|||||||
assert.deepStrictEqual(api.run(), {
|
assert.deepStrictEqual(api.run(), {
|
||||||
state: 'error',
|
state: 'error',
|
||||||
retryButton: { textContent: 'Try again' },
|
retryButton: { textContent: 'Try again' },
|
||||||
|
userAlreadyExistsBlocked: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,5 +189,6 @@ return {
|
|||||||
assert.deepStrictEqual(api.run(), {
|
assert.deepStrictEqual(api.run(), {
|
||||||
state: 'error',
|
state: 'error',
|
||||||
retryButton: { textContent: 'Try again' },
|
retryButton: { textContent: 'Try again' },
|
||||||
|
userAlreadyExistsBlocked: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -584,6 +584,7 @@
|
|||||||
|
|
||||||
- 新增共享恢复层:`content/auth-page-recovery.js`。
|
- 新增共享恢复层:`content/auth-page-recovery.js`。
|
||||||
- Step 4 在等待注册验证码页时,如果命中认证页 `Try again / 重试` 页,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续回到密码页重提和验证码页确认流程。
|
- Step 4 在等待注册验证码页时,如果命中认证页 `Try again / 重试` 页,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续回到密码页重提和验证码页确认流程。
|
||||||
|
- 但如果 Step 4 的认证重试页正文中出现 `user_already_exists`,则会直接视为“当前用户已存在”:不点击 `重试`,不再回到步骤 1 重开当前轮,而是立即结束当前轮;开启自动重试时直接进入下一轮。
|
||||||
- Step 7 在识别到登录超时报错页时,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复当前页面;若仍未恢复,则按原有可恢复失败逻辑重跑 Step 7。
|
- Step 7 在识别到登录超时报错页时,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复当前页面;若仍未恢复,则按原有可恢复失败逻辑重跑 Step 7。
|
||||||
- Step 8 如果发现认证页已经进入登录超时报错/重试页,会直接报错并回到 Step 7 重新开始,而不是在 Step 8 内部点击 `重试`。
|
- Step 8 如果发现认证页已经进入登录超时报错/重试页,会直接报错并回到 Step 7 重新开始,而不是在 Step 8 内部点击 `重试`。
|
||||||
- 任意认证页重试页如果正文中出现 `max_check_attempts`,会被视为 Cloudflare 风控触发:后台立刻完全停止流程,侧边栏会复用现有确认弹窗提示等待 15~30 分钟后再试,避免继续刷新或反复重试加重风控,确认按钮显示为“我知道了”。
|
- 任意认证页重试页如果正文中出现 `max_check_attempts`,会被视为 Cloudflare 风控触发:后台立刻完全停止流程,侧边栏会复用现有确认弹窗提示等待 15~30 分钟后再试,避免继续刷新或反复重试加重风控,确认按钮显示为“我知道了”。
|
||||||
|
|||||||
Reference in New Issue
Block a user