diff --git a/background.js b/background.js index 54a2830..cb414ab 100644 --- a/background.js +++ b/background.js @@ -7382,7 +7382,78 @@ function isRestartCurrentAttemptError(error) { return loggingStatus.isRestartCurrentAttemptError(error); } const message = String(typeof error === 'string' ? error : error?.message || ''); - return /当前邮箱已存在,需要重新开始新一轮/.test(message); + return /当前邮箱已存在,需要重新开始新一轮|SIGNUP_PHONE_PASSWORD_MISMATCH::/i.test(message); +} + +function isSignupPhonePasswordMismatchFailure(error) { + const message = getErrorMessage(error); + return /SIGNUP_PHONE_PASSWORD_MISMATCH::/i.test(message); +} + +function getSignupPhonePasswordMismatchRestartPayload(preservedState = {}) { + const preservedEmail = String(preservedState.email || '').trim(); + const preservedPassword = String(preservedState.password || '').trim(); + const accountIdentifierType = String(preservedState.accountIdentifierType || '').trim().toLowerCase(); + const activeSignupPhoneNumber = String( + preservedState.signupPhoneNumber + || preservedState.signupPhoneActivation?.phoneNumber + || preservedState.signupPhoneCompletedActivation?.phoneNumber + || (accountIdentifierType === 'phone' ? preservedState.accountIdentifier : '') + || '' + ).trim(); + const shouldClearSignupPhoneRuntime = Boolean( + activeSignupPhoneNumber + || preservedState.signupPhoneActivation + || preservedState.signupPhoneCompletedActivation + || preservedState.signupPhoneVerificationRequestedAt + || preservedState.signupPhoneVerificationPurpose + || accountIdentifierType === 'phone' + ); + const restorePayload = {}; + if (preservedEmail) restorePayload.email = preservedEmail; + if (preservedPassword) restorePayload.password = preservedPassword; + if (shouldClearSignupPhoneRuntime) { + restorePayload.signupPhoneNumber = ''; + restorePayload.signupPhoneActivation = null; + restorePayload.signupPhoneCompletedActivation = null; + restorePayload.signupPhoneVerificationRequestedAt = null; + restorePayload.signupPhoneVerificationPurpose = ''; + if (accountIdentifierType === 'phone') { + restorePayload.accountIdentifierType = null; + restorePayload.accountIdentifier = ''; + } + } + return { + activeSignupPhoneNumber, + preservedEmail, + restorePayload, + shouldClearSignupPhoneRuntime, + }; +} + +async function restartSignupPhonePasswordMismatchAttemptFromStep(step, restartCount, error) { + const preservedState = await getState(); + const { + activeSignupPhoneNumber, + preservedEmail, + restorePayload, + shouldClearSignupPhoneRuntime, + } = getSignupPhonePasswordMismatchRestartPayload(preservedState); + const emailSuffix = preservedEmail ? `当前邮箱:${preservedEmail};` : ''; + const phoneSuffix = activeSignupPhoneNumber ? `当前手机号:${activeSignupPhoneNumber};` : ''; + await addLog( + `步骤 ${step}:检测到手机号/密码不匹配,准备丢弃当前注册手机号并回到步骤 1 重新开始(第 ${restartCount} 次重开)。${phoneSuffix}${emailSuffix}原因:${getErrorMessage(error)}`, + 'warn' + ); + await invalidateDownstreamAfterStepRestart(1, { + logLabel: `步骤 ${step} 检测到手机号/密码不匹配后准备回到步骤 1 重新获取手机号重试(第 ${restartCount} 次重开)`, + }); + if (shouldClearSignupPhoneRuntime) { + await addLog(`步骤 ${step}:已清空本轮注册手机号与接码订单,下一次重开将重新获取号码。`, 'warn'); + } + if (Object.keys(restorePayload).length) { + await setState(restorePayload); + } } function isSignupUserAlreadyExistsFailure(error) { @@ -10040,6 +10111,8 @@ async function runAutoSequenceFromStep(startStep, context = {}) { await executeStepAndWait(2, AUTO_STEP_DELAYS[2]); } + let restartFromStep1WithCurrentEmail = false; + if (currentStartStep <= 3) { const latestState = await getState(); const step3Status = latestState.stepStatuses?.[3] || 'pending'; @@ -10052,18 +10125,36 @@ async function runAutoSequenceFromStep(startStep, context = {}) { if (isStepDoneStatus(step3Status)) { await addLog(`自动运行:步骤 3 当前状态为 ${step3Status},将直接继续后续流程。`, 'info'); } else { - await executeStepAndWait(3, AUTO_STEP_DELAYS[3]); + try { + await executeStepAndWait(3, AUTO_STEP_DELAYS[3]); + } catch (err) { + if (isStopError(err)) { + throw err; + } + if (isSignupPhonePasswordMismatchFailure(err)) { + step4RestartCount += 1; + await restartSignupPhonePasswordMismatchAttemptFromStep(3, step4RestartCount, err); + currentStartStep = 1; + continueCurrentAttempt = true; + restartFromStep1WithCurrentEmail = true; + continue; + } + throw err; + } } } else { await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续执行剩余流程(第 ${attemptRuns} 次尝试)===`, 'info'); } + if (restartFromStep1WithCurrentEmail) { + continue; + } + const signupTabId = await getTabId('signup-page'); if (signupTabId) { await chrome.tabs.update(signupTabId, { active: true }); } - let restartFromStep1WithCurrentEmail = false; let step = Math.max(currentStartStep, 4); while (step <= (typeof getLastStepIdForState === 'function' ? getLastStepIdForState(await getState()) @@ -10113,22 +10204,26 @@ async function runAutoSequenceFromStep(startStep, context = {}) { throw err; } step4RestartCount += 1; - const preservedState = await getState(); - const preservedEmail = String(preservedState.email || '').trim(); - const preservedPassword = String(preservedState.password || '').trim(); - const emailSuffix = preservedEmail ? `当前邮箱:${preservedEmail};` : ''; - await addLog( - `步骤 4:执行失败,准备沿用当前邮箱回到步骤 1 重新开始(第 ${step4RestartCount} 次重开)。${emailSuffix}原因:${getErrorMessage(err)}`, - 'warn' - ); - await invalidateDownstreamAfterStepRestart(1, { - logLabel: `步骤 4 报错后准备回到步骤 1 沿用当前邮箱重试(第 ${step4RestartCount} 次重开)`, - }); - const restorePayload = {}; - if (preservedEmail) restorePayload.email = preservedEmail; - if (preservedPassword) restorePayload.password = preservedPassword; - if (Object.keys(restorePayload).length) { - await setState(restorePayload); + if (isSignupPhonePasswordMismatchFailure(err)) { + await restartSignupPhonePasswordMismatchAttemptFromStep(4, step4RestartCount, err); + } else { + const preservedState = await getState(); + const preservedEmail = String(preservedState.email || '').trim(); + const preservedPassword = String(preservedState.password || '').trim(); + const emailSuffix = preservedEmail ? `当前邮箱:${preservedEmail};` : ''; + await addLog( + `步骤 4:执行失败,准备沿用当前邮箱回到步骤 1 重新开始(第 ${step4RestartCount} 次重开)。${emailSuffix}原因:${getErrorMessage(err)}`, + 'warn' + ); + await invalidateDownstreamAfterStepRestart(1, { + logLabel: `步骤 4 报错后准备回到步骤 1 沿用当前邮箱重试(第 ${step4RestartCount} 次重开)`, + }); + const restorePayload = {}; + if (preservedEmail) restorePayload.email = preservedEmail; + if (preservedPassword) restorePayload.password = preservedPassword; + if (Object.keys(restorePayload).length) { + await setState(restorePayload); + } } currentStartStep = 1; continueCurrentAttempt = true; diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index f80608b..7abc462 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -166,7 +166,9 @@ const retryCount = getAutoRunRoundRetryCount(item); const finalReason = item.finalFailureReason || item.failureReasons[item.failureReasons.length - 1] || '未知错误'; const reasonSummary = formatAutoRunFailureReasons(item.failureReasons); - return `第 ${item.round} 轮(重试 ${retryCount} 次,最终原因:${finalReason};失败记录:${reasonSummary})`; + return !reasonSummary || reasonSummary === finalReason + ? `第 ${item.round} 轮(重试 ${retryCount} 次,最终原因:${finalReason})` + : `第 ${item.round} 轮(重试 ${retryCount} 次,最终原因:${finalReason};失败记录:${reasonSummary})`; }) .join(';')}`, 'error' diff --git a/background/logging-status.js b/background/logging-status.js index 5ce50f0..bc1b3a5 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -112,7 +112,7 @@ function isRestartCurrentAttemptError(error) { const message = String(typeof error === 'string' ? error : error?.message || ''); - return /当前邮箱已存在,需要重新开始新一轮/.test(message); + return /当前邮箱已存在,需要重新开始新一轮|SIGNUP_PHONE_PASSWORD_MISMATCH::/i.test(message); } function isSignupUserAlreadyExistsFailure(error) { diff --git a/background/message-router.js b/background/message-router.js index 3db7566..9960bde 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -589,15 +589,20 @@ notifyStepError(message.step, '流程已被用户停止。'); return { ok: true, error: userMessage }; } + const currentState = await getState(); + const currentStepStatus = currentState?.stepStatuses?.[message.step] || ''; + const isSignupPhonePasswordMismatch = /SIGNUP_PHONE_PASSWORD_MISMATCH::/i.test(String(message.error || '')); if (isStopError(message.error)) { await setStepStatus(message.step, 'stopped'); await addLog('已被用户停止', 'warn', { step: message.step }); await appendManualAccountRunRecordIfNeeded(`step${message.step}_stopped`, null, message.error); notifyStepError(message.step, message.error); } else { - await setStepStatus(message.step, 'failed'); - await addLog(`失败:${message.error}`, 'error', { step: message.step }); - await appendManualAccountRunRecordIfNeeded(`step${message.step}_failed`, null, message.error); + if (!(isSignupPhonePasswordMismatch && currentStepStatus === 'failed')) { + await setStepStatus(message.step, 'failed'); + await addLog(`失败:${message.error}`, 'error', { step: message.step }); + await appendManualAccountRunRecordIfNeeded(`step${message.step}_failed`, null, message.error); + } notifyStepError(message.step, message.error); } return { ok: true }; diff --git a/content/signup-page.js b/content/signup-page.js index f3a3615..797236a 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -450,6 +450,9 @@ const SIGNUP_PHONE_ACTION_PATTERN = /手机|手机号|电话号码|phone|telepho const SIGNUP_SWITCH_TO_PHONE_PATTERN = new RegExp([ String.raw`\u7ee7\u7eed\u4f7f\u7528(?:\u624b\u673a|\u624b\u673a\u53f7|\u7535\u8bdd\u53f7\u7801)(?:\u53f7\u7801)?\u767b\u5f55`, String.raw`\u6539\u7528(?:\u624b\u673a|\u624b\u673a\u53f7|\u7535\u8bdd\u53f7\u7801)(?:\u53f7\u7801)?\u767b\u5f55`, + String.raw`\u7ee7\u7eed\u4f7f\u7528(?:\u624b\u673a|\u624b\u673a\u53f7|\u624b\u673a\u53f7\u7801|\u7535\u8bdd\u53f7\u7801)(?:\u53f7\u7801)?`, + String.raw`\u6539\u7528(?:\u624b\u673a|\u624b\u673a\u53f7|\u624b\u673a\u53f7\u7801|\u7535\u8bdd\u53f7\u7801)(?:\u53f7\u7801)?`, + String.raw`\u4f7f\u7528(?:\u624b\u673a|\u624b\u673a\u53f7|\u624b\u673a\u53f7\u7801|\u7535\u8bdd\u53f7\u7801)(?:\u53f7\u7801)?`, String.raw`continue\s+(?:with|using)\s+(?:a\s+)?phone(?:\s+number)?`, String.raw`use\s+(?:a\s+)?phone(?:\s+number)?(?:\s+instead)?`, String.raw`sign\s*(?:in|up)\s+with\s+(?:a\s+)?phone`, @@ -615,6 +618,7 @@ function inspectSignupEntryState() { passwordInput, submitButton: getSignupPasswordSubmitButton({ allowDisabled: true }), displayedEmail: getSignupPasswordDisplayedEmail(), + passwordErrorText: getSignupPasswordFieldErrorText(), url: location.href, }; } @@ -927,6 +931,7 @@ function getSignupPasswordDiagnostics() { title: document.title || '', readyState: document.readyState || '', displayedEmail: getSignupPasswordDisplayedEmail(), + passwordErrorText: getSignupPasswordFieldErrorText(), hasVisiblePasswordInput: Boolean(getSignupPasswordInput()), passwordInputCount: passwordInputs.length, visiblePasswordInputCount: passwordInputs.filter((item) => item.visible).length, @@ -2484,9 +2489,11 @@ const AUTH_ROUTE_ERROR_PATTERN = /405\s+method\s+not\s+allowed|route\s+error.*40 const STEP4_405_RECOVERY_ERROR_PREFIX = 'STEP4_405_RECOVERY_LIMIT::'; const STEP4_405_RECOVERY_LIMIT = 3; const SIGNUP_USER_ALREADY_EXISTS_ERROR_PREFIX = 'SIGNUP_USER_ALREADY_EXISTS::'; +const SIGNUP_PHONE_PASSWORD_MISMATCH_ERROR_PREFIX = 'SIGNUP_PHONE_PASSWORD_MISMATCH::'; const AUTH_MAX_CHECK_ATTEMPTS_ERROR_PREFIX = 'AUTH_MAX_CHECK_ATTEMPTS::'; const STEP8_EMAIL_IN_USE_ERROR_PREFIX = 'STEP8_EMAIL_IN_USE::'; 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_PHONE_PASSWORD_MISMATCH_PATTERN = /incorrect\s+phone\s+number\s+or\s+password|phone\s+number\s+or\s+password/i; const authPageRecovery = self.MultiPageAuthPageRecovery?.createAuthPageRecovery?.({ detailPattern: AUTH_TIMEOUT_ERROR_DETAIL_PATTERN, @@ -2543,6 +2550,14 @@ function createSignupUserAlreadyExistsError() { ); } +function createSignupPhonePasswordMismatchError(detailText = '') { + const detail = String(detailText || '').replace(/\s+/g, ' ').trim(); + const suffix = detail ? `页面提示:${detail}` : '页面提示手机号或密码不正确。'; + return new Error( + `${SIGNUP_PHONE_PASSWORD_MISMATCH_ERROR_PREFIX}步骤 3:检测到注册手机号或密码不正确,需要重新开始当前轮。${suffix}` + ); +} + function createAuthMaxCheckAttemptsError() { return new Error(`${AUTH_MAX_CHECK_ATTEMPTS_ERROR_PREFIX}max_check_attempts on auth retry page; restart the current auth step without clicking Retry.`); } @@ -2576,6 +2591,24 @@ function getVisibleFieldErrorText() { return ''; } +function getSignupPasswordFieldErrorText() { + const text = getVisibleFieldErrorText(); + if (text && SIGNUP_PHONE_PASSWORD_MISMATCH_PATTERN.test(text)) { + return text; + } + + const passwordInput = getSignupPasswordInput(); + if (passwordInput) { + const wrapper = passwordInput.closest('form, [data-rac], [role="group"], section, div'); + const wrapperText = (wrapper?.textContent || '').replace(/\s+/g, ' ').trim(); + if (wrapperText && SIGNUP_PHONE_PASSWORD_MISMATCH_PATTERN.test(wrapperText)) { + return wrapperText; + } + } + + return ''; +} + function isStep5Ready() { return Boolean( document.querySelector('input[name="name"], input[autocomplete="name"], input[name="birthday"], input[name="age"], [role="spinbutton"][data-type="year"]') @@ -4372,6 +4405,7 @@ function inspectSignupVerificationState() { state: 'password', passwordInput, submitButton: getSignupPasswordSubmitButton({ allowDisabled: true }), + passwordErrorText: getSignupPasswordFieldErrorText(), }; } @@ -4465,6 +4499,10 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { } if (snapshot.state === 'password') { + if (snapshot.passwordErrorText) { + log(`${prepareLogLabel}:检测到密码页报错“${snapshot.passwordErrorText}”,当前轮将回到步骤 1 重新开始。`, 'warn'); + throw createSignupPhonePasswordMismatchError(snapshot.passwordErrorText); + } if (!passwordPageDiagnosticsLogged) { passwordPageDiagnosticsLogged = true; logSignupPasswordDiagnostics(`${prepareLogLabel}:页面仍停留在密码页`); diff --git a/content/utils.js b/content/utils.js index 0741e8f..4a35f3e 100644 --- a/content/utils.js +++ b/content/utils.js @@ -349,7 +349,6 @@ function reportComplete(step, data = {}) { */ function reportError(step, errorMessage) { console.error(LOG_PREFIX, `步骤 ${step} 失败: ${errorMessage}`); - log(`失败:${errorMessage}`, 'error', { step }); const message = { type: 'STEP_ERROR', source: getRuntimeScriptSource(), diff --git a/tests/auto-run-step4-restart.test.js b/tests/auto-run-step4-restart.test.js index 8e11b0d..6ee2104 100644 --- a/tests/auto-run-step4-restart.test.js +++ b/tests/auto-run-step4-restart.test.js @@ -56,6 +56,9 @@ const bundle = [ extractFunction('isAddPhoneAuthUrl'), extractFunction('isAddPhoneAuthState'), extractFunction('isMail2925ThreadTerminatedError'), + extractFunction('isSignupPhonePasswordMismatchFailure'), + extractFunction('getSignupPhonePasswordMismatchRestartPayload'), + extractFunction('restartSignupPhonePasswordMismatchAttemptFromStep'), extractFunction('isSignupUserAlreadyExistsFailure'), extractFunction('getPostStep6AutoRestartDecision'), extractFunction('runAutoSequenceFromStep'), @@ -457,3 +460,322 @@ return { assert.equal(events.logs.some(({ message }) => /步骤 4 当前状态为 skipped/.test(message)), true); assert.equal(events.logs.some(({ message }) => /步骤 5 当前状态为 skipped/.test(message)), true); }); + +test('auto-run clears fetched signup phone state before restarting when step 4 detects phone/password mismatch', 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 SIGNUP_METHOD_PHONE = 'phone'; +const chrome = { + tabs: { + update: async () => {}, + }, + runtime: { + sendMessage: async () => {}, + }, +}; + +let remainingFailures = 1; +let currentState = { + email: '', + password: 'Secret123!', + signupMethod: 'phone', + accountIdentifierType: 'phone', + accountIdentifier: '+56988841722', + signupPhoneNumber: '+56988841722', + signupPhoneActivation: { + activationId: 'act-1', + phoneNumber: '+56988841722', + }, + signupPhoneCompletedActivation: { + activationId: 'act-1', + phoneNumber: '+56988841722', + }, + 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: [], + setStateCalls: [], +}; + +async function addLog(message, level = 'info') { + events.logs.push({ message, level }); +} + +async function ensureAutoEmailReady() { + return ''; +} + +async function broadcastAutoRunStatus() {} +async function ensureResolvedSignupMethodForRun() { return 'phone'; } + +async function getState() { + return currentState; +} + +async function setState(updates) { + currentState = { + ...currentState, + ...updates, + stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses, + }; + events.setStateCalls.push(updates); +} + +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 && remainingFailures > 0) { + remainingFailures -= 1; + throw new Error('SIGNUP_PHONE_PASSWORD_MISMATCH::步骤 3:检测到注册手机号或密码不正确,需要重新开始当前轮。页面提示:Incorrect phone number or password'); + } +} + +async function getTabId() { + return 1; +} + +async function invalidateDownstreamAfterStepRestart(step, options = {}) { + events.invalidations.push({ step, options }); + currentState = { + ...currentState, + password: null, + stepStatuses: { + 1: currentState.stepStatuses[1] || 'completed', + 2: 'pending', + 3: 'pending', + 4: 'pending', + 5: 'pending', + 6: 'pending', + 7: 'pending', + 8: 'pending', + 9: 'pending', + 10: 'pending', + }, + }; +} + +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() { + await runAutoSequenceFromStep(1, { + targetRun: 1, + totalRuns: 1, + attemptRuns: 1, + continued: false, + }); + return { events, currentState }; + }, +}; +`)(); + + const { events, currentState } = await api.run(); + + assert.deepStrictEqual(events.invalidations, [ + { + step: 1, + options: { + logLabel: '步骤 4 检测到手机号/密码不匹配后准备回到步骤 1 重新获取手机号重试(第 1 次重开)', + }, + }, + ]); + assert.equal(currentState.signupPhoneNumber, ''); + assert.equal(currentState.signupPhoneActivation, null); + assert.equal(currentState.signupPhoneCompletedActivation, null); + assert.equal(currentState.accountIdentifierType, null); + assert.equal(currentState.accountIdentifier, ''); + assert.equal(currentState.password, 'Secret123!'); + assert.equal(events.logs.some(({ message }) => /丢弃当前注册手机号并回到步骤 1 重新开始/.test(message)), true); + assert.equal(events.logs.some(({ message }) => /已清空本轮注册手机号与接码订单/.test(message)), true); +}); + +test('auto-run clears manual signup phone state when step 3 detects phone/password mismatch', 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 SIGNUP_METHOD_PHONE = 'phone'; +const chrome = { + tabs: { + update: async () => {}, + }, + runtime: { + sendMessage: async () => {}, + }, +}; + +let remainingFailures = 1; +let currentState = { + email: '', + password: 'Secret123!', + signupMethod: 'phone', + accountIdentifierType: 'phone', + accountIdentifier: '+56988841722', + signupPhoneNumber: '+56988841722', + signupPhoneActivation: null, + signupPhoneCompletedActivation: null, + 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: [], + setStateCalls: [], +}; + +async function addLog(message, level = 'info') { + events.logs.push({ message, level }); +} + +async function ensureAutoEmailReady() { + return ''; +} + +async function broadcastAutoRunStatus() {} +async function ensureResolvedSignupMethodForRun() { return 'phone'; } + +async function getState() { + return currentState; +} + +async function setState(updates) { + currentState = { + ...currentState, + ...updates, + stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses, + }; + events.setStateCalls.push(updates); +} + +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 === 3 && remainingFailures > 0) { + remainingFailures -= 1; + throw new Error('SIGNUP_PHONE_PASSWORD_MISMATCH::步骤 3:检测到注册手机号或密码不正确,需要重新开始当前轮。页面提示:Incorrect phone number or password'); + } +} + +async function getTabId() { + return 1; +} + +async function invalidateDownstreamAfterStepRestart(step, options = {}) { + events.invalidations.push({ step, options }); + currentState = { + ...currentState, + password: null, + stepStatuses: { + 1: currentState.stepStatuses[1] || 'completed', + 2: 'pending', + 3: 'pending', + 4: 'pending', + 5: 'pending', + 6: 'pending', + 7: 'pending', + 8: 'pending', + 9: 'pending', + 10: 'pending', + }, + }; +} + +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() { + await runAutoSequenceFromStep(1, { + targetRun: 1, + totalRuns: 1, + attemptRuns: 1, + continued: false, + }); + return { events, currentState }; + }, +}; +`)(); + + const { events, currentState } = await api.run(); + + assert.deepStrictEqual(events.steps, [1, 2, 3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + assert.deepStrictEqual(events.invalidations, [ + { + step: 1, + options: { + logLabel: '步骤 3 检测到手机号/密码不匹配后准备回到步骤 1 重新获取手机号重试(第 1 次重开)', + }, + }, + ]); + assert.equal(currentState.signupPhoneNumber, ''); + assert.equal(currentState.signupPhoneActivation, null); + assert.equal(currentState.signupPhoneCompletedActivation, null); + assert.equal(currentState.accountIdentifierType, null); + assert.equal(currentState.accountIdentifier, ''); + assert.equal(currentState.password, 'Secret123!'); + assert.equal(events.logs.some(({ message }) => /步骤 3:检测到手机号\/密码不匹配/.test(message)), true); + assert.equal(events.logs.some(({ message }) => /步骤 3:已清空本轮注册手机号与接码订单/.test(message)), true); +}); diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js index 9cb5bfd..0c80804 100644 --- a/tests/background-message-router-step2-skip.test.js +++ b/tests/background-message-router-step2-skip.test.js @@ -435,6 +435,36 @@ test('message router marks step 3 failed when post-submit finalize fails', async assert.deepStrictEqual(response, { ok: true, error: '步骤 3 提交后仍停留在密码页。' }); }); +test('message router does not duplicate step 3 mismatch failure log after finalize already failed', async () => { + const mismatchError = 'SIGNUP_PHONE_PASSWORD_MISMATCH::步骤 3:检测到注册手机号或密码不正确,需要重新开始当前轮。页面提示:Incorrect phone number or password'; + const state = { + stepStatuses: { + 3: 'failed', + }, + }; + const { router, events } = createRouter({ + state, + }); + + const response = await router.handleMessage({ + type: 'STEP_ERROR', + step: 3, + source: 'signup-page', + payload: {}, + error: mismatchError, + }, {}); + + assert.deepStrictEqual(events.stepStatuses, []); + assert.equal(events.logs.some(({ message, step }) => /失败:SIGNUP_PHONE_PASSWORD_MISMATCH::/.test(message) && step === 3), false); + assert.deepStrictEqual(events.notifyErrors, [ + { + step: 3, + error: mismatchError, + }, + ]); + assert.deepStrictEqual(response, { ok: true }); +}); + test('message router stops the flow and surfaces cloudflare security block errors', async () => { const { router, events } = createRouter(); diff --git a/tests/signup-entry-diagnostics.test.js b/tests/signup-entry-diagnostics.test.js index d32df30..a0ff73b 100644 --- a/tests/signup-entry-diagnostics.test.js +++ b/tests/signup-entry-diagnostics.test.js @@ -450,6 +450,10 @@ function getSignupPasswordDisplayedEmail() { return 'user@example.com'; } +function getSignupPasswordFieldErrorText() { + return ''; +} + function findOneTimeCodeLoginTrigger() { return oneTimeCodeButton; } @@ -474,6 +478,7 @@ return { assert.equal(result.url, 'https://auth.openai.com/create-account/password'); assert.equal(result.displayedEmail, 'user@example.com'); + assert.equal(result.passwordErrorText, ''); assert.equal(result.hasVisiblePasswordInput, true); assert.equal(result.passwordInputCount, 1); assert.equal(result.visiblePasswordInputCount, 1); diff --git a/tests/signup-verification-state-guard.test.js b/tests/signup-verification-state-guard.test.js index be7a2c8..ec3f101 100644 --- a/tests/signup-verification-state-guard.test.js +++ b/tests/signup-verification-state-guard.test.js @@ -254,6 +254,64 @@ return { }); }); +test('signup verification state exposes password error text on password page', () => { + const api = new Function(` +const location = { + href: 'https://auth.openai.com/log-in/password', +}; + +function isStep5Ready() { + return false; +} + +function isVerificationPageStillVisible() { + return false; +} + +function isSignupPasswordErrorPage() { + return false; +} + +function getSignupPasswordTimeoutErrorPageState() { + return null; +} + +function isSignupEmailAlreadyExistsPage() { + return false; +} + +function getSignupPasswordInput() { + return { value: 'Secret123!' }; +} + +function getSignupPasswordSubmitButton() { + return { textContent: 'Continue' }; +} + +function getSignupPasswordFieldErrorText() { + return 'Incorrect phone number or password'; +} + +${extractFunction('isSignupProfilePageUrl')} +${extractFunction('isLikelyLoggedInChatgptHomeUrl')} +${extractFunction('getStep4PostVerificationState')} +${extractFunction('inspectSignupVerificationState')} + +return { + run() { + return inspectSignupVerificationState(); + }, +}; +`)(); + + assert.deepStrictEqual(api.run(), { + state: 'password', + passwordInput: { value: 'Secret123!' }, + submitButton: { textContent: 'Continue' }, + passwordErrorText: 'Incorrect phone number or password', + }); +}); + test('signup verification state keeps verification priority when email-verification page also shows profile fields', () => { const api = new Function(` const location = { diff --git a/tests/step4-split-code-submit.test.js b/tests/step4-split-code-submit.test.js index 52e022f..82037e5 100644 --- a/tests/step4-split-code-submit.test.js +++ b/tests/step4-split-code-submit.test.js @@ -717,3 +717,92 @@ return { assert.equal(snapshot.sleepCalls >= 3, true); assert.equal(snapshot.targetChecks >= 1, true); }); + +test('prepareSignupVerificationFlow stops immediately when password page shows phone/password mismatch', async () => { + const api = new Function(` +const logs = []; +const clicks = []; +let now = 0; + +Date.now = () => now; + +function throwIfStopped() {} +function log(message, level = 'info') { logs.push({ message, level }); } +async function sleep(ms = 0) { now += ms || 200; } +function isVisibleElement() { return true; } +function isActionEnabled() { return true; } +function getActionText(el) { return el?.textContent || ''; } +function getCurrentAuthRetryPageState() { return null; } +function isPhoneVerificationPageReady() { return false; } +function findResendVerificationCodeTrigger() { return null; } +function isEmailVerificationPage() { return false; } +function getPageTextSnapshot() { return 'Incorrect phone number or password'; } +function getVerificationCodeTarget() { return null; } +function is405MethodNotAllowedPage() { return false; } +async function recoverCurrentAuthRetryPage() {} +function createSignupUserAlreadyExistsError() { return new Error('user already exists'); } +function getSignupPasswordInput() { return { value: 'Secret123!' }; } +function getSignupPasswordSubmitButton() { return { textContent: 'Continue' }; } +function isSignupEmailAlreadyExistsPage() { return false; } +function isSignupPasswordErrorPage() { return false; } +function getSignupPasswordTimeoutErrorPageState() { return null; } +function isStep5Ready() { return false; } +function getSignupPasswordFieldErrorText() { return 'Incorrect phone number or password'; } +function simulateClick(target) { clicks.push(target?.textContent || 'clicked'); } +async function humanPause() {} +function fillInput() {} +function logSignupPasswordDiagnostics() {} +function createSignupPhonePasswordMismatchError(detailText = '') { + return new Error('SIGNUP_PHONE_PASSWORD_MISMATCH::' + detailText); +} + +const location = { + href: 'https://auth.openai.com/log-in/password', + pathname: '/log-in/password', +}; +const document = { + readyState: 'complete', + title: '', + body: { + textContent: 'Incorrect phone number or password', + innerText: 'Incorrect phone number or password', + }, + querySelector() { + return null; + }, + querySelectorAll() { + return []; + }, +}; + +${extractFunction('isSignupVerificationPageInteractiveReady')} +${extractFunction('isVerificationPageStillVisible')} +${extractFunction('isSignupProfilePageUrl')} +${extractFunction('isLikelyLoggedInChatgptHomeUrl')} +${extractFunction('getStep4PostVerificationState')} +${extractFunction('inspectSignupVerificationState')} +${extractFunction('waitForSignupVerificationTransition')} +${extractFunction('prepareSignupVerificationFlow')} + +return { + async run() { + try { + await prepareSignupVerificationFlow({ + password: 'Secret123!', + prepareLogLabel: '步骤 3 收尾', + }, 10000); + return { threw: false, logs, clicks }; + } catch (error) { + return { threw: true, error: error.message, logs, clicks }; + } + }, +}; +`)(); + + const result = await api.run(); + + assert.equal(result.threw, true); + assert.match(result.error, /SIGNUP_PHONE_PASSWORD_MISMATCH::Incorrect phone number or password/); + assert.equal(result.clicks.length, 0); + assert.equal(result.logs.some(({ message }) => /检测到密码页报错/.test(message)), true); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index ea9422e..e079e43 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -352,6 +352,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功 7. 延迟提交真正触发前会再次检查 Stop 状态,避免用户已停止时页面仍继续自动提交 8. 后台在真正确认 Step 3 完成前,会额外检查提交后是否切换页面;如果出现认证页 `Try again / 重试` 页面,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续后续链路 9. Step 3 收尾阶段如果页面切换导致旧内容脚本失联,后台会把单次消息等待收口到当前收尾预算内,优先尽快重试重连;若最终仍未恢复,则输出中文的步骤级错误,而不是直接暴露底层英文通信超时 +10. 手机号注册时,如果 Step 3 收尾或 Step 4 准备验证码阶段在密码页检测到 `Incorrect phone number or password`,后台会把它视为当前注册手机号/密码不匹配:清空本轮 `signupPhoneNumber / signupPhoneActivation / signupPhoneCompletedActivation` 和手机号账号身份,回到 Step 1 重新获取号码并重开当前轮,避免继续在密码页重复点击 补充: diff --git a/项目文件结构说明.md b/项目文件结构说明.md index e47682e..ee3b7eb 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -93,7 +93,7 @@ - `content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。 - `content/plus-checkout.js`:ChatGPT Plus checkout 页面脚本,负责读取 `/api/auth/session` 创建 Plus checkout、选择 PayPal、填写账单姓名、触发 Google 地址推荐、校验结构化地址字段并点击订阅。 - `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。 -- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、手机号国家下拉框同步校验、手机号填写与提交;Step 7 / 8 登录链路会额外识别手机号输入页、手机号登录入口、`phone-verification` 与真实 `add-email` 页面,避免把手机验证码页或添加邮箱页误判成普通邮箱登录入口;Step 9 后置手机号验证完成后若偶发进入资料页,会复用 Step 5 的姓名生日填写逻辑再继续等待 OAuth 同意页。 +- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、手机号国家下拉框同步校验、手机号填写与提交;Step 3 收尾会识别手机号注册密码页的 `Incorrect phone number or password` 并上报当前轮重开信号;Step 7 / 8 登录链路会额外识别手机号输入页、手机号登录入口、`phone-verification` 与真实 `add-email` 页面,避免把手机验证码页或添加邮箱页误判成普通邮箱登录入口;Step 9 后置手机号验证完成后若偶发进入资料页,会复用 Step 5 的姓名生日填写逻辑再继续等待 OAuth 同意页。 - `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;平台验证请求会读取 `visibleStep`,普通模式承接步骤 10,Plus 模式承接步骤 13。 - `content/utils.js`:内容脚本公共工具层,负责结构化日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击;内容脚本日志通过 `{ step, stepKey }` 上报,正文不再承担步骤号解析职责。 - `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做平台验证判定;当前支持普通步骤 10 与 Plus 可见步骤 13。 @@ -243,7 +243,7 @@ - `tests/sidepanel-mail2925-manager.test.js`:测试侧边栏 2925 管理器模块接线、共享号池表单脚本加载顺序、显隐交互与空态渲染。 - `tests/sidepanel-mail2925-mode.test.js`:测试侧边栏保留 `2925 provide / receive` 模式行与独立的号池配置行,并验证 sidepanel 只在 provide 模式下把 2925 视为别名邮箱 provider。 - `tests/sidepanel-paypal-manager.test.js`:测试侧边栏公共表单弹窗脚本与 PayPal manager 的加载顺序,以及 PayPal 账号保存后会立即选中当前账号。 -- `tests/signup-entry-diagnostics.test.js`:测试注册入口诊断快照输出。 +- `tests/signup-entry-diagnostics.test.js`:测试注册入口与密码页诊断快照输出,包括密码页错误文案字段。 - `tests/signup-step2-email-switch.test.js`:测试 Step 2 在手机号输入模式下切回邮箱输入模式、本地化邮箱输入框识别,以及手机号注册时国家下拉框可视区号同步。 - `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。 - `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。