diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index 3dee3e7..761a3e4 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -96,6 +96,12 @@ .join(';'); } + function shouldKeepCustomMailProviderPoolEmail(state = {}) { + return String(state?.mailProvider || '').trim().toLowerCase() === 'custom' + && Array.isArray(state?.customMailProviderPool) + && state.customMailProviderPool.length > 0; + } + async function logAutoRunFinalSummary(totalRuns, roundSummaries = []) { const summaries = buildAutoRunRoundSummaries(totalRuns, roundSummaries); const successRounds = summaries.filter((item) => item.status === 'success'); @@ -328,8 +334,10 @@ const resumingCurrentRound = continueCurrentOnFirstAttempt && targetRun === resumeCurrentRun; let attemptRun = resumingCurrentRound ? resumeAttemptRun : 1; let reuseExistingProgress = resumingCurrentRound; + const currentRoundState = await getState(); + const keepSameEmailUntilAddPhone = autoRunSkipFailures && shouldKeepCustomMailProviderPoolEmail(currentRoundState); const maxAttemptsForRound = autoRunSkipFailures - ? AUTO_RUN_MAX_RETRIES_PER_ROUND + 1 + ? (keepSameEmailUntilAddPhone ? Number.MAX_SAFE_INTEGER : AUTO_RUN_MAX_RETRIES_PER_ROUND + 1) : Math.max(1, attemptRun); while (attemptRun <= maxAttemptsForRound) { @@ -461,6 +469,7 @@ roundSummary.failureReasons.push(reason); const blockedByAddPhone = typeof isAddPhoneAuthFailure === 'function' && isAddPhoneAuthFailure(err); const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function' + && !keepSameEmailUntilAddPhone && isSignupUserAlreadyExistsFailure(err); const canRetry = !blockedByAddPhone && !blockedBySignupUserAlreadyExists && autoRunSkipFailures && attemptRun < maxAttemptsForRound; @@ -555,7 +564,9 @@ }); forceFreshTabsNextRun = true; await addLog( - `自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试(第 ${retryIndex}/${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试)。`, + keepSameEmailUntilAddPhone + ? `自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后继续使用当前邮箱,开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试。` + : `自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试(第 ${retryIndex}/${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试)。`, 'warn' ); try { diff --git a/background/verification-flow.js b/background/verification-flow.js index eba621d..36e9411 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -98,6 +98,9 @@ if (response?.error) { throw new Error(response.error); } + if (step === 8 && response?.addPhoneDetected) { + throw new Error('步骤 8:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。 URL: https://auth.openai.com/add-phone'); + } if (!response?.confirmed) { throw new Error(`步骤 ${step}:已取消手动${verificationLabel}验证码确认。`); } diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 91ee96f..b7af09f 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -2785,9 +2785,45 @@ function getCustomVerificationPromptCopy(step) { text: `点击确认后会跳过步骤 ${step}。`, tone: 'danger', }, + ...(step === 8 ? { + phoneActionLabel: '出现手机号验证', + phoneActionAlert: { + text: '如果当前页面已经进入手机号验证,可直接标记为失败并继续下一个邮箱。', + tone: 'danger', + }, + } : {}), }; } +async function openCustomVerificationConfirmDialog(step) { + const promptCopy = getCustomVerificationPromptCopy(step); + if (step === 8) { + return openActionModal({ + title: promptCopy.title, + message: promptCopy.message, + alert: promptCopy.alert, + actions: [ + { id: null, label: '取消', variant: 'btn-ghost' }, + { id: 'add_phone', label: promptCopy.phoneActionLabel || '出现手机号验证', variant: 'btn-outline' }, + { id: 'confirm', label: '确认跳过', variant: 'btn-danger' }, + ], + buildResult: (choice) => ({ + confirmed: choice === 'confirm', + addPhoneDetected: choice === 'add_phone', + }), + }); + } + + const confirmed = await openConfirmModal({ + title: promptCopy.title, + message: promptCopy.message, + confirmLabel: '确认跳过', + confirmVariant: 'btn-danger', + alert: promptCopy.alert, + }); + return { confirmed, addPhoneDetected: false }; +} + function getHotmailAccounts(state = latestState) { return Array.isArray(state?.hotmailAccounts) ? state.hotmailAccounts : []; } @@ -4950,15 +4986,8 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { case 'REQUEST_CUSTOM_VERIFICATION_BYPASS_CONFIRMATION': { (async () => { const step = Number(message.payload?.step); - const promptCopy = getCustomVerificationPromptCopy(step); - const confirmed = await openConfirmModal({ - title: promptCopy.title, - message: promptCopy.message, - confirmLabel: '确认跳过', - confirmVariant: 'btn-danger', - alert: promptCopy.alert, - }); - sendResponse({ confirmed }); + const result = await openCustomVerificationConfirmDialog(step); + sendResponse(result || { confirmed: false, addPhoneDetected: false }); })().catch((err) => { sendResponse({ error: err.message }); }); diff --git a/tests/auto-run-add-phone-stop.test.js b/tests/auto-run-add-phone-stop.test.js index 509a4c0..d425121 100644 --- a/tests/auto-run-add-phone-stop.test.js +++ b/tests/auto-run-add-phone-stop.test.js @@ -331,3 +331,168 @@ test('auto-run controller skips user_already_exists failures to the next round i assert.equal(runtime.state.autoRunActive, false); assert.equal(runtime.state.autoRunSessionId, 0); }); + +test('auto-run controller keeps retrying the same custom mail provider pool email until success', 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: 'custom', + customMailProviderPool: ['first@example.com'], + 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 || {}) }, + customMailProviderPool: [...(currentState.customMailProviderPool || [])], + }), + getStopRequested: () => false, + hasSavedProgress: () => false, + isAddPhoneAuthFailure: (error) => /add-phone|手机号页面|手机号页|手机号码|手机号/i.test(error?.message || String(error || '')), + 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 <= 2) { + throw new Error('步骤 3:页面异常,当前尝试失败。'); + } + }, + 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(1, { + autoRunSkipFailures: true, + mode: 'restart', + }); + + assert.equal(events.runCalls, 3, 'custom mail provider pool should keep retrying the same round until success'); + assert.equal(events.broadcasts.filter(({ phase }) => phase === 'retrying').length, 2); + assert.ok(events.broadcasts.filter(({ phase }) => phase === 'retrying').every(({ currentRun }) => currentRun === 1)); + assert.ok(events.logs.some(({ message }) => /继续使用当前邮箱/.test(message))); + assert.equal(events.logs.some(({ message }) => /达到 3 次重试上限,继续下一轮/.test(message)), false); + assert.equal(events.accountRecords.length, 0); + assert.equal(runtime.state.autoRunActive, false); + assert.equal(runtime.state.autoRunSessionId, 0); +}); diff --git a/tests/sidepanel-custom-email-pool.test.js b/tests/sidepanel-custom-email-pool.test.js index cfcaef2..a8e7b6e 100644 --- a/tests/sidepanel-custom-email-pool.test.js +++ b/tests/sidepanel-custom-email-pool.test.js @@ -174,3 +174,45 @@ return { assert.equal(api.getLockedRunCountFromEmailPool(), 3); assert.equal(api.getRunCountValue(), 3); }); + +test('sidepanel custom verification dialog exposes add-phone action for step 8', async () => { + const bundle = [ + extractFunction('getCustomVerificationPromptCopy'), + extractFunction('openCustomVerificationConfirmDialog'), + ].join('\n'); + + const api = new Function(` +let openActionModalPayload = null; + +async function openActionModal(options) { + openActionModalPayload = options; + return options.buildResult('add_phone'); +} + +async function openConfirmModal() { + throw new Error('step 8 should use action modal'); +} + +${bundle} + +return { + getCustomVerificationPromptCopy, + openCustomVerificationConfirmDialog, + getOpenActionModalPayload: () => openActionModalPayload, +}; +`)(); + + const prompt = api.getCustomVerificationPromptCopy(8); + assert.equal(prompt.phoneActionLabel, '出现手机号验证'); + + const result = await api.openCustomVerificationConfirmDialog(8); + assert.deepEqual(result, { + confirmed: false, + addPhoneDetected: true, + }); + + const modalPayload = api.getOpenActionModalPayload(); + assert.equal(modalPayload.actions.length, 3); + assert.equal(modalPayload.actions[1].id, 'add_phone'); + assert.equal(modalPayload.actions[1].label, '出现手机号验证'); +}); diff --git a/tests/verification-flow-polling.test.js b/tests/verification-flow-polling.test.js index ea37f4c..95c965f 100644 --- a/tests/verification-flow-polling.test.js +++ b/tests/verification-flow-polling.test.js @@ -345,6 +345,51 @@ test('verification flow treats add-phone after login code submit as fatal instea ]); }); +test('verification flow treats manual step 8 add-phone confirmation as the same fatal add-phone error', async () => { + const helpers = api.createVerificationFlowHelpers({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + completeStepFromBackground: async () => { + throw new Error('should not complete step 8'); + }, + confirmCustomVerificationStepBypassRequest: async () => ({ + confirmed: false, + addPhoneDetected: true, + }), + getHotmailVerificationPollConfig: () => ({}), + getHotmailVerificationRequestTimestamp: () => 0, + getState: async () => ({}), + getTabId: async () => 1, + HOTMAIL_PROVIDER: 'hotmail-api', + isStopError: () => false, + LUCKMAIL_PROVIDER: 'luckmail-api', + MAIL_2925_VERIFICATION_INTERVAL_MS: 15000, + MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15, + pollCloudflareTempEmailVerificationCode: async () => ({}), + pollHotmailVerificationCode: async () => ({}), + pollLuckmailVerificationCode: async () => ({}), + sendToContentScript: async () => ({}), + sendToMailContentScriptResilient: async () => ({}), + setState: async () => {}, + setStepStatus: async () => { + throw new Error('should not mark step skipped when add-phone is chosen'); + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + VERIFICATION_POLL_MAX_ROUNDS: 5, + }); + + await assert.rejects( + () => helpers.confirmCustomVerificationStepBypass(8), + /验证码提交后页面进入手机号页面/ + ); +}); + test('verification flow caps mail polling timeout to the remaining oauth budget', async () => { const mailPollCalls = [];