From 4107e4d3783c6280e2b8f0a191d11f047686aa40 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Mon, 20 Apr 2026 15:08:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=EF=BC=8C=E5=A4=84=E7=90=86=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=B7=B2=E5=AD=98=E5=9C=A8=E9=94=99=E8=AF=AF=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=9B=B8=E5=85=B3=E9=80=BB=E8=BE=91=E5=92=8C=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + background.js | 12 ++ background/auto-run-controller.js | 40 ++++- background/logging-status.js | 6 + content/auth-page-recovery.js | 16 +- content/signup-page.js | 35 +++- tests/auth-page-recovery.test.js | 22 +++ tests/auto-run-add-phone-stop.test.js | 163 ++++++++++++++++++ tests/auto-run-step4-restart.test.js | 122 +++++++++++++ tests/signup-verification-state-guard.test.js | 2 + 项目完整链路说明.md | 1 + 11 files changed, 417 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7204332..d4adf4d 100644 --- a/README.md +++ b/README.md @@ -502,6 +502,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 进入邮箱轮询前,脚本会先确认认证页是否已经进入验证码页面;如果注册认证流程出现 `糟糕,出错了 / 操作超时(Operation timed out)`,或 `/email-verification` 上的 `405 / Route Error` 且带有 `重试` 按钮,会先通过共享恢复逻辑最多自动点击 5 次 `重试`,必要时回到密码页重新提交,再继续等待验证码页面。 在 `Auto` 模式下,如果 Step 4 当前轮失败,后台不会立刻丢弃这轮邮箱;而是沿用当前邮箱回到 Step 1 重新开始当前轮,避免刚拿到的邮箱被直接换掉。 +但如果 Step 4 的认证重试页正文里出现 `user_already_exists`,则会直接判定“当前用户已存在”,不会点击 `重试`,而是立即结束当前轮;开启自动重试时会直接继续下一轮。 支持: diff --git a/background.js b/background.js index cc44a84..b479a39 100644 --- a/background.js +++ b/background.js @@ -4018,6 +4018,14 @@ function isRestartCurrentAttemptError(error) { 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) { const message = String(typeof error === 'string' ? error : error?.message || ''); return /STEP9_OAUTH_RETRY::/i.test(message) @@ -5511,6 +5519,7 @@ const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoR hasSavedProgress, isAddPhoneAuthFailure, isRestartCurrentAttemptError, + isSignupUserAlreadyExistsFailure, isStopError, launchAutoRunTimerPlan, normalizeAutoRunFallbackThreadIntervalMinutes, @@ -5818,6 +5827,9 @@ async function runAutoSequenceFromStep(startStep, context = {}) { } if (step === 4) { + if (isSignupUserAlreadyExistsFailure(err)) { + throw err; + } step4RestartCount += 1; const preservedState = await getState(); const preservedEmail = String(preservedState.email || '').trim(); diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index 36cc6d9..1f4d58c 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -23,6 +23,7 @@ hasSavedProgress, isAddPhoneAuthFailure, isRestartCurrentAttemptError, + isSignupUserAlreadyExistsFailure, isStopError, launchAutoRunTimerPlan, normalizeAutoRunFallbackThreadIntervalMinutes, @@ -458,7 +459,9 @@ const reason = getErrorMessage(err); roundSummary.failureReasons.push(reason); 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({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), @@ -499,6 +502,41 @@ 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) { const retryIndex = attemptRun; if (isRestartCurrentAttemptError(err)) { diff --git a/background/logging-status.js b/background/logging-status.js index f685fd3..dc6659c 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -91,6 +91,11 @@ return /当前邮箱已存在,需要重新开始新一轮/.test(message); } + function isSignupUserAlreadyExistsFailure(error) { + const message = getErrorMessage(error); + return /SIGNUP_USER_ALREADY_EXISTS::|user_already_exists/i.test(message); + } + function isStep9RecoverableAuthError(error) { const message = String(typeof error === 'string' ? error : error?.message || ''); return /STEP9_OAUTH_RETRY::/i.test(message) @@ -162,6 +167,7 @@ hasSavedProgress, isLegacyStep9RecoverableAuthError, isRestartCurrentAttemptError, + isSignupUserAlreadyExistsFailure, isStep9RecoverableAuthError, isStepDoneStatus, isVerificationMailPollingError, diff --git a/content/auth-page-recovery.js b/content/auth-page-recovery.js index 70b192e..63417f8 100644 --- a/content/auth-page-recovery.js +++ b/content/auth-page-recovery.js @@ -70,8 +70,9 @@ ? routeErrorPattern.test(text) : false; 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; } @@ -84,6 +85,7 @@ detailMatched, routeErrorMatched, 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) { idlePollCount = 0; clickCount += 1; @@ -204,6 +212,12 @@ ); } + if (finalRetryState.userAlreadyExistsBlocked) { + throw new Error( + 'SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。' + ); + } + throw new Error( `${logLabel || `步骤 ${step || '?'}:重试页恢复`}失败:已连续点击“重试” ${maxClickAttempts} 次,页面仍未恢复。URL: ${location.href}` ); diff --git a/content/signup-page.js b/content/signup-page.js index 5cb30a0..f536b92 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -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_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 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 authPageRecovery = self.MultiPageAuthPageRecovery?.createAuthPageRecovery?.({ @@ -663,6 +664,12 @@ function getVerificationErrorText() { 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() { return Boolean( 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 routeErrorMatched = AUTH_ROUTE_ERROR_PATTERN.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; } @@ -995,6 +1003,7 @@ function getAuthTimeoutErrorPageState(options = {}) { detailMatched, routeErrorMatched, maxCheckAttemptsBlocked, + userAlreadyExistsBlocked, }; } @@ -1073,6 +1082,9 @@ async function recoverCurrentAuthRetryPage(payload = {}) { if (retryState.maxCheckAttemptsBlocked) { throw new Error('CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器'); } + if (retryState.userAlreadyExistsBlocked) { + throw createSignupUserAlreadyExistsError(); + } if (retryState.retryButton && retryState.retryEnabled) { idlePollCount = 0; clickCount += 1; @@ -1113,6 +1125,9 @@ async function recoverCurrentAuthRetryPage(payload = {}) { if (finalRetryState.maxCheckAttemptsBlocked) { throw new Error('CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器'); } + if (finalRetryState.userAlreadyExistsBlocked) { + throw createSignupUserAlreadyExistsError(); + } throw new Error(`${logLabel || `步骤 ${step || '?'}:重试页恢复`}失败:已连续点击“重试” ${maxClickAttempts} 次,页面仍未恢复。URL: ${location.href}`); } @@ -1430,6 +1445,7 @@ function inspectSignupVerificationState() { return { state: 'error', retryButton: timeoutPage?.retryButton || null, + userAlreadyExistsBlocked: Boolean(timeoutPage?.userAlreadyExistsBlocked), }; } @@ -1503,6 +1519,9 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { recoveryRound += 1; if (snapshot.state === 'error') { + if (snapshot.userAlreadyExistsBlocked) { + throw createSignupUserAlreadyExistsError(); + } await recoverCurrentAuthRetryPage({ flow: 'signup', logLabel: `${prepareLogLabel}:检测到注册认证重试页,正在点击“重试”恢复(第 ${recoveryRound}/${maxRecoveryRounds} 次)`, @@ -1549,6 +1568,13 @@ async function waitForVerificationSubmitOutcome(step, timeout) { while (Date.now() - start < resolvedTimeout) { throwIfStopped(); + if (step === 4) { + const signupRetryState = getCurrentAuthRetryPageState('signup'); + if (signupRetryState?.userAlreadyExistsBlocked) { + throw createSignupUserAlreadyExistsError(); + } + } + const errorText = getVerificationErrorText(); if (errorText) { return { invalidCode: true, errorText }; @@ -1569,6 +1595,13 @@ async function waitForVerificationSubmitOutcome(step, timeout) { await sleep(150); } + if (step === 4) { + const signupRetryState = getCurrentAuthRetryPageState('signup'); + if (signupRetryState?.userAlreadyExistsBlocked) { + throw createSignupUserAlreadyExistsError(); + } + } + if (isVerificationPageStillVisible()) { return { invalidCode: true, diff --git a/tests/auth-page-recovery.test.js b/tests/auth-page-recovery.test.js index 9271eab..b48287d 100644 --- a/tests/auth-page-recovery.test.js +++ b/tests/auth-page-recovery.test.js @@ -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); +}); + diff --git a/tests/auto-run-add-phone-stop.test.js b/tests/auto-run-add-phone-stop.test.js index 0ca3f7e..509a4c0 100644 --- a/tests/auto-run-add-phone-stop.test.js +++ b/tests/auto-run-add-phone-stop.test.js @@ -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.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); +}); diff --git a/tests/auto-run-step4-restart.test.js b/tests/auto-run-step4-restart.test.js index 6cd4c94..2d9b683 100644 --- a/tests/auto-run-step4-restart.test.js +++ b/tests/auto-run-step4-restart.test.js @@ -55,6 +55,7 @@ function extractFunction(name) { const bundle = [ extractFunction('isAddPhoneAuthUrl'), extractFunction('isAddPhoneAuthState'), + extractFunction('isSignupUserAlreadyExistsFailure'), extractFunction('getPostStep6AutoRestartDecision'), extractFunction('runAutoSequenceFromStep'), ].join('\n'); @@ -207,3 +208,124 @@ return { assert.equal(currentState.password, 'Secret123!'); 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); +}); diff --git a/tests/signup-verification-state-guard.test.js b/tests/signup-verification-state-guard.test.js index f36efe2..4414636 100644 --- a/tests/signup-verification-state-guard.test.js +++ b/tests/signup-verification-state-guard.test.js @@ -138,6 +138,7 @@ return { assert.deepStrictEqual(api.run(), { state: 'error', retryButton: { textContent: 'Try again' }, + userAlreadyExistsBlocked: false, }); }); @@ -188,5 +189,6 @@ return { assert.deepStrictEqual(api.run(), { state: 'error', retryButton: { textContent: 'Try again' }, + userAlreadyExistsBlocked: false, }); }); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index c99d8a2..2568ca7 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -584,6 +584,7 @@ - 新增共享恢复层:`content/auth-page-recovery.js`。 - Step 4 在等待注册验证码页时,如果命中认证页 `Try again / 重试` 页,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续回到密码页重提和验证码页确认流程。 +- 但如果 Step 4 的认证重试页正文中出现 `user_already_exists`,则会直接视为“当前用户已存在”:不点击 `重试`,不再回到步骤 1 重开当前轮,而是立即结束当前轮;开启自动重试时直接进入下一轮。 - Step 7 在识别到登录超时报错页时,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复当前页面;若仍未恢复,则按原有可恢复失败逻辑重跑 Step 7。 - Step 8 如果发现认证页已经进入登录超时报错/重试页,会直接报错并回到 Step 7 重新开始,而不是在 Step 8 内部点击 `重试`。 - 任意认证页重试页如果正文中出现 `max_check_attempts`,会被视为 Cloudflare 风控触发:后台立刻完全停止流程,侧边栏会复用现有确认弹窗提示等待 15~30 分钟后再试,避免继续刷新或反复重试加重风控,确认按钮显示为“我知道了”。