diff --git a/background.js b/background.js index c0e9f5f..c5fe3c3 100644 --- a/background.js +++ b/background.js @@ -272,6 +272,7 @@ const PERSISTED_SETTING_DEFAULTS = { customEmailPool: [], autoDeleteUsedIcloudAlias: false, icloudHostPreference: 'auto', + icloudFetchMode: 'reuse_existing', accountRunHistoryTextEnabled: false, accountRunHistoryHelperBaseUrl: DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL, gmailBaseEmail: '', @@ -674,6 +675,11 @@ function normalizeEmailGenerator(value = '') { return 'duck'; } +function normalizeIcloudFetchMode(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return normalized === 'always_new' ? 'always_new' : 'reuse_existing'; +} + function normalizeCustomEmailPool(value = []) { const source = Array.isArray(value) ? value @@ -979,6 +985,8 @@ function normalizePersistentSettingValue(key, value) { return Boolean(value); case 'icloudHostPreference': return normalizeIcloudHost(value) || 'auto'; + case 'icloudFetchMode': + return normalizeIcloudFetchMode(value); case 'accountRunHistoryHelperBaseUrl': return normalizeAccountRunHistoryHelperBaseUrl(value); case 'gmailBaseEmail': @@ -3106,6 +3114,16 @@ async function pollLuckmailVerificationCode(step, state, pollPayload = {}) { excludeCodes: pollPayload.excludeCodes || [], }; + const initialCursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor); + if (!initialCursor.messageId && !initialCursor.receivedAt) { + const mailList = await client.user.getTokenMails(purchase.token); + const baselineCursor = buildLuckmailBaselineCursor(mailList?.mails || []); + await setLuckmailMailCursorState(baselineCursor); + if (baselineCursor?.messageId || baselineCursor?.receivedAt) { + await addLog(`步骤 ${step}:LuckMail 已保存当前邮箱旧邮件快照,后续仅使用新收到的验证码。`, 'info'); + } + } + let lastError = null; for (let attempt = 1; attempt <= maxAttempts; attempt++) { throwIfStopped(); @@ -3449,10 +3467,16 @@ async function validateIcloudSession(setupUrl) { return data; } -async function resolveIcloudPremiumMailService() { +async function resolveIcloudPremiumMailService(options = {}) { const errors = []; const state = await getState(); - const setupUrls = await getPreferredIcloudSetupUrls(state); + const explicitHost = normalizeIcloudHost(options?.hostPreference || options?.preferredHost || ''); + const setupUrls = explicitHost + ? (() => { + const forcedSetupUrl = getIcloudSetupUrlForHost(explicitHost); + return forcedSetupUrl ? [forcedSetupUrl] : []; + })() + : await getPreferredIcloudSetupUrls(state); for (const setupUrl of setupUrls) { try { @@ -3481,17 +3505,19 @@ function getIcloudAliasLabel() { return `MultiPage ${dateStr}`; } -async function checkIcloudSession() { - return withIcloudLoginHelp('检查 iCloud 会话', async () => { - const { setupUrl } = await resolveIcloudPremiumMailService(); +async function checkIcloudSession(options = {}) { + const actionLabel = String(options?.actionLabel || '检查 iCloud 会话').trim() || '检查 iCloud 会话'; + const { actionLabel: _ignoredActionLabel, ...resolveOptions } = options || {}; + return withIcloudLoginHelp(actionLabel, async () => { + const { setupUrl } = await resolveIcloudPremiumMailService(resolveOptions); await addLog(`iCloud:会话校验通过(${new URL(setupUrl).host})`, 'ok'); return { ok: true, setupUrl }; }); } -async function listIcloudAliases() { +async function listIcloudAliases(options = {}) { return withIcloudLoginHelp('加载 iCloud 隐私邮箱列表', async () => { - const { serviceUrl } = await resolveIcloudPremiumMailService(); + const { serviceUrl } = await resolveIcloudPremiumMailService(options); const response = await icloudRequest('GET', `${serviceUrl}/v2/hme/list`); const state = await getState(); return normalizeIcloudAliasList(response, { @@ -3581,9 +3607,10 @@ async function deleteUsedIcloudAliases() { return { deleted, skipped }; } -async function fetchIcloudHideMyEmail() { +async function fetchIcloudHideMyEmail(options = {}) { return withIcloudLoginHelp('获取 iCloud 隐私邮箱', async () => { throwIfStopped(); + const generateNew = Boolean(options?.generateNew); await addLog('iCloud:正在校验当前浏览器登录状态...', 'info'); const { serviceUrl, setupUrl } = await resolveIcloudPremiumMailService(); @@ -3596,12 +3623,16 @@ async function fetchIcloudHideMyEmail() { preservedEmails: getPreservedAliasMap(state), }); - const reusableAlias = pickReusableIcloudAlias(existingAliases); - if (reusableAlias) { - await setEmailState(reusableAlias.email); - await addLog(`iCloud:复用未使用别名 ${reusableAlias.email}`, 'ok'); - broadcastIcloudAliasesChanged({ reason: 'selected', email: reusableAlias.email }); - return reusableAlias.email; + if (!generateNew) { + const reusableAlias = pickReusableIcloudAlias(existingAliases); + if (reusableAlias) { + await setEmailState(reusableAlias.email); + await addLog(`iCloud:复用未使用别名 ${reusableAlias.email}`, 'ok'); + broadcastIcloudAliasesChanged({ reason: 'selected', email: reusableAlias.email }); + return reusableAlias.email; + } + } else { + await addLog('iCloud:已启用“始终创建新别名”,本次将跳过复用。', 'info'); } await addLog('iCloud:没有可复用别名,开始生成新的 Hide My Email 地址...', 'warn'); @@ -5973,7 +6004,10 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { if (attempt > 1) { await addLog(`${generatorLabel}:正在进行第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次自动获取重试...`, 'warn'); } - const generatedEmail = await fetchGeneratedEmail(currentState, { generateNew: true, generator }); + const generatedEmail = await fetchGeneratedEmail(currentState, { + generateNew: generator !== 'icloud' || normalizeIcloudFetchMode(currentState.icloudFetchMode) === 'always_new', + generator, + }); await addLog( `=== 目标 ${targetRun}/${totalRuns} 轮:${generatorLabel}已就绪:${generatedEmail}(第 ${attemptRuns} 次尝试,第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次获取)===`, 'ok' @@ -6119,7 +6153,10 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { if (attempt > 1) { await addLog(`${generatorLabel}:正在进行第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次自动获取重试...`, 'warn'); } - const generatedEmail = await fetchGeneratedEmail(currentState, { generateNew: true, generator }); + const generatedEmail = await fetchGeneratedEmail(currentState, { + generateNew: generator !== 'icloud' || normalizeIcloudFetchMode(currentState.icloudFetchMode) === 'always_new', + generator, + }); await addLog( `=== 目标 ${targetRun}/${totalRuns} 轮:${generatorLabel}已就绪:${generatedEmail}(第 ${attemptRuns} 次尝试,第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次获取)===`, 'ok' @@ -6204,9 +6241,15 @@ async function runAutoSequenceFromStep(startStep, context = {}) { let restartFromStep1WithCurrentEmail = false; let step = Math.max(currentStartStep, 4); while (step <= LAST_STEP_ID) { + const latestState = await getState(); + const currentStatus = latestState.stepStatuses?.[step] || 'pending'; + if (isStepDoneStatus(currentStatus)) { + await addLog(`自动运行:步骤 ${step} 当前状态为 ${currentStatus},将直接继续后续流程。`, 'info'); + step += 1; + continue; + } try { await executeStepAndWait(step, AUTO_STEP_DELAYS[step]); - const latestState = await getState(); step += 1; } catch (err) { if (isStopError(err)) { @@ -6445,6 +6488,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create getTabId, HOTMAIL_PROVIDER, isMail2925LimitReachedError, + isRetryableContentScriptTransportError, isStopError, LUCKMAIL_PROVIDER, MAIL_2925_VERIFICATION_INTERVAL_MS, @@ -6453,6 +6497,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create pollHotmailVerificationCode, pollLuckmailVerificationCode, sendToContentScript, + sendToContentScriptResilient, sendToMailContentScriptResilient, setState, setStepStatus, @@ -6470,6 +6515,7 @@ const step2Executor = self.MultiPageBackgroundStep2?.createStep2Executor({ chrome, completeStepFromBackground, ensureContentScriptReadyOnTab, + ensureSignupAuthEntryPageReady, ensureSignupEntryPageReady, ensureSignupPostEmailPageReadyInTab, getTabId, @@ -6490,12 +6536,24 @@ const step3Executor = self.MultiPageBackgroundStep3?.createStep3Executor({ setState, SIGNUP_PAGE_INJECT_FILES, }); + +async function ensureIcloudMailSessionForVerification(options = {}) { + const flowState = options?.state || await getState().catch(() => ({})); + const hostPreference = getConfiguredIcloudHostPreference(flowState) + || normalizeIcloudHost(flowState?.preferredIcloudHost); + return checkIcloudSession({ + ...(hostPreference ? { hostPreference } : {}), + actionLabel: options?.actionLabel || '检查 iCloud 会话', + }); +} + const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({ addLog, chrome, completeStepFromBackground, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, ensureMail2925MailboxSession, + ensureIcloudMailSession: ensureIcloudMailSessionForVerification, getMailConfig, getTabId, HOTMAIL_PROVIDER, @@ -6542,6 +6600,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ CLOUDFLARE_TEMP_EMAIL_PROVIDER, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, ensureMail2925MailboxSession, + ensureIcloudMailSession: ensureIcloudMailSessionForVerification, ensureStep8VerificationPageReady, getOAuthFlowRemainingMs, getOAuthFlowStepTimeoutMs, @@ -6724,6 +6783,10 @@ async function ensureSignupEntryPageReady(step = 1) { return signupFlowHelpers.ensureSignupEntryPageReady(step); } +async function ensureSignupAuthEntryPageReady(step = 1) { + return signupFlowHelpers.ensureSignupEntryPageReady(step); +} + async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) { return signupFlowHelpers.ensureSignupPasswordPageReadyInTab(tabId, step, options); } diff --git a/background/generated-email-helpers.js b/background/generated-email-helpers.js index be121ea..022d6af 100644 --- a/background/generated-email-helpers.js +++ b/background/generated-email-helpers.js @@ -283,7 +283,10 @@ return fetchManagedAliasEmail(mergedState, options); } if (generator === 'icloud') { - return fetchIcloudHideMyEmail(); + const stateFetchMode = String(mergedState.icloudFetchMode || '').trim().toLowerCase(); + return fetchIcloudHideMyEmail({ + generateNew: Boolean(options.generateNew) || stateFetchMode === 'always_new', + }); } if (generator === 'cloudflare') { return fetchCloudflareEmail(mergedState, options); diff --git a/background/message-router.js b/background/message-router.js index c293b16..15c503d 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -151,6 +151,18 @@ if (payload.email) { await setEmailState(payload.email); } + if (payload.skipRegistrationFlow) { + const latestState = await getState(); + for (const skipStep of [3, 4, 5]) { + const status = latestState.stepStatuses?.[skipStep]; + if (status === 'running' || status === 'completed' || status === 'manual_completed') { + continue; + } + await setStepStatus(skipStep, 'skipped'); + } + await addLog('步骤 2:检测到当前已登录会话,已自动跳过步骤 3/4/5,流程将直接进入步骤 6。', 'warn'); + break; + } if (payload.skippedPasswordStep) { const latestState = await getState(); const step3Status = latestState.stepStatuses?.[3]; @@ -179,6 +191,14 @@ lastEmailTimestamp: payload.emailTimestamp || null, signupVerificationRequestedAt: null, }); + if (payload.skipProfileStep) { + const latestState = await getState(); + const step5Status = latestState.stepStatuses?.[5]; + if (step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') { + await setStepStatus(5, 'skipped'); + await addLog('步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。', 'warn'); + } + } break; case 8: await setState({ diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 7baad00..a379a77 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -10,6 +10,7 @@ CLOUDFLARE_TEMP_EMAIL_PROVIDER, confirmCustomVerificationStepBypass, ensureMail2925MailboxSession, + ensureIcloudMailSession, ensureStep8VerificationPageReady, getOAuthFlowRemainingMs, getOAuthFlowStepTimeoutMs, @@ -140,6 +141,15 @@ return; } + if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') { + await addLog('步骤 8:正在确认 iCloud 邮箱登录态...', 'info'); + await ensureIcloudMailSession({ + state, + step: 8, + actionLabel: '步骤 8:确认 iCloud 邮箱登录态', + }); + } + throwIfStopped(); if ( mail.provider === HOTMAIL_PROVIDER diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index 77dbeaa..02f9aa5 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -10,6 +10,7 @@ completeStepFromBackground, confirmCustomVerificationStepBypass, ensureMail2925MailboxSession, + ensureIcloudMailSession, getMailConfig, getTabId, HOTMAIL_PROVIDER, @@ -111,6 +112,15 @@ return; } + if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') { + await addLog('步骤 4:正在确认 iCloud 邮箱登录态...', 'info'); + await ensureIcloudMailSession({ + state, + step: 4, + actionLabel: '步骤 4:确认 iCloud 邮箱登录态', + }); + } + throwIfStopped(); if ( mail.provider === HOTMAIL_PROVIDER diff --git a/background/steps/open-chatgpt.js b/background/steps/open-chatgpt.js index 2476a9d..222454b 100644 --- a/background/steps/open-chatgpt.js +++ b/background/steps/open-chatgpt.js @@ -1,14 +1,142 @@ (function attachBackgroundStep1(root, factory) { root.MultiPageBackgroundStep1 = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep1Module() { + const STEP1_COOKIE_CLEAR_DOMAINS = [ + 'chatgpt.com', + 'chat.openai.com', + 'openai.com', + 'auth.openai.com', + 'auth0.openai.com', + 'accounts.openai.com', + ]; + const STEP1_COOKIE_CLEAR_ORIGINS = [ + 'https://chatgpt.com', + 'https://chat.openai.com', + 'https://auth.openai.com', + 'https://auth0.openai.com', + 'https://accounts.openai.com', + 'https://openai.com', + ]; + + function normalizeCookieDomainForStep1(domain) { + return String(domain || '').trim().replace(/^\.+/, '').toLowerCase(); + } + + function shouldClearStep1Cookie(cookie) { + const domain = normalizeCookieDomainForStep1(cookie?.domain); + if (!domain) return false; + return STEP1_COOKIE_CLEAR_DOMAINS.some((target) => ( + domain === target || domain.endsWith(`.${target}`) + )); + } + + function buildStep1CookieRemovalUrl(cookie) { + const host = normalizeCookieDomainForStep1(cookie?.domain); + const rawPath = String(cookie?.path || '/'); + const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`; + return `https://${host}${path}`; + } + + function getStep1ErrorMessage(error) { + return error?.message || String(error || '未知错误'); + } + + async function collectStep1Cookies(chromeApi) { + if (!chromeApi.cookies?.getAll) { + return []; + } + + const stores = chromeApi.cookies.getAllCookieStores + ? await chromeApi.cookies.getAllCookieStores() + : [{ id: undefined }]; + const cookies = []; + const seen = new Set(); + + for (const store of stores) { + const storeId = store?.id; + const batch = await chromeApi.cookies.getAll(storeId ? { storeId } : {}); + for (const cookie of batch || []) { + if (!shouldClearStep1Cookie(cookie)) continue; + const key = [ + cookie.storeId || storeId || '', + cookie.domain || '', + cookie.path || '', + cookie.name || '', + cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '', + ].join('|'); + if (seen.has(key)) continue; + seen.add(key); + cookies.push(cookie); + } + } + + return cookies; + } + + async function removeStep1Cookie(chromeApi, cookie) { + const details = { + url: buildStep1CookieRemovalUrl(cookie), + name: cookie.name, + }; + if (cookie.storeId) { + details.storeId = cookie.storeId; + } + if (cookie.partitionKey) { + details.partitionKey = cookie.partitionKey; + } + + try { + const result = await chromeApi.cookies.remove(details); + return Boolean(result); + } catch (error) { + console.warn('[MultiPage:step1] remove cookie failed', { + domain: cookie?.domain, + name: cookie?.name, + message: getStep1ErrorMessage(error), + }); + return false; + } + } + function createStep1Executor(deps = {}) { const { addLog, + chrome: chromeApi = globalThis.chrome, completeStepFromBackground, openSignupEntryTab, } = deps; + async function clearOpenAiCookiesBeforeStep1() { + if (!chromeApi?.cookies?.getAll || !chromeApi.cookies?.remove) { + await addLog('步骤 1:当前浏览器不支持 cookies API,跳过打开官网前 cookie 清理。', 'warn'); + return; + } + + await addLog('步骤 1:打开 ChatGPT 官网前清理 ChatGPT / OpenAI cookies...', 'info'); + const cookies = await collectStep1Cookies(chromeApi); + let removedCount = 0; + for (const cookie of cookies) { + if (await removeStep1Cookie(chromeApi, cookie)) { + removedCount += 1; + } + } + + if (chromeApi.browsingData?.removeCookies) { + try { + await chromeApi.browsingData.removeCookies({ + since: 0, + origins: STEP1_COOKIE_CLEAR_ORIGINS, + }); + } catch (error) { + await addLog(`步骤 1:browsingData 补扫 cookies 失败:${getStep1ErrorMessage(error)}`, 'warn'); + } + } + + await addLog(`步骤 1:已清理 ${removedCount} 个 ChatGPT / OpenAI cookies。`, 'ok'); + } + async function executeStep1() { + await clearOpenAiCookiesBeforeStep1(); await addLog('步骤 1:正在打开 ChatGPT 官网...'); await openSignupEntryTab(1); await completeStepFromBackground(1, {}); diff --git a/background/steps/submit-signup-email.js b/background/steps/submit-signup-email.js index 77e6b65..b8cad06 100644 --- a/background/steps/submit-signup-email.js +++ b/background/steps/submit-signup-email.js @@ -7,6 +7,7 @@ chrome, completeStepFromBackground, ensureContentScriptReadyOnTab, + ensureSignupAuthEntryPageReady, ensureSignupEntryPageReady, ensureSignupPostEmailPageReadyInTab, getTabId, @@ -16,6 +17,107 @@ SIGNUP_PAGE_INJECT_FILES, } = deps; + function getErrorMessage(error) { + return String(typeof error === 'string' ? error : error?.message || ''); + } + + function isSignupEntryUnavailableErrorMessage(errorLike) { + const message = getErrorMessage(errorLike); + return /未找到可用的邮箱输入入口|当前页面没有可用的注册入口,也不在邮箱\/密码页/.test(message); + } + + function isRetryableStep2TransportErrorMessage(errorLike) { + const message = getErrorMessage(errorLike); + return /Content script on signup-page did not respond in \d+s|Receiving end does not exist|message channel closed|A listener indicated an asynchronous response|port closed before a response was received|did not respond in \d+s/i.test(message); + } + + function isLikelyLoggedInChatgptHomeUrl(rawUrl) { + const url = String(rawUrl || '').trim(); + if (!url) { + return false; + } + try { + const parsed = new URL(url); + const host = String(parsed.hostname || '').toLowerCase(); + if (!['chatgpt.com', 'www.chatgpt.com'].includes(host)) { + return false; + } + const path = String(parsed.pathname || ''); + if (/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(path)) { + return false; + } + return true; + } catch { + return false; + } + } + + async function shouldForceAuthEntryRetry(tabId) { + if (!Number.isInteger(tabId) || typeof chrome?.tabs?.get !== 'function') { + return false; + } + try { + const tab = await chrome.tabs.get(tabId); + const currentUrl = String(tab?.url || ''); + return isLikelyLoggedInChatgptHomeUrl(currentUrl); + } catch { + return false; + } + } + + async function getTabUrl(tabId) { + if (!Number.isInteger(tabId) || typeof chrome?.tabs?.get !== 'function') { + return ''; + } + try { + const tab = await chrome.tabs.get(tabId); + return String(tab?.url || ''); + } catch { + return ''; + } + } + + async function completeStep2AsLoggedInSession(tabId, resolvedEmail, reasonMessage = '') { + const currentUrl = await getTabUrl(tabId); + if (!isLikelyLoggedInChatgptHomeUrl(currentUrl)) { + return false; + } + const reasonText = getErrorMessage(reasonMessage); + const reasonSuffix = reasonText ? `(触发原因:${reasonText})` : ''; + await addLog(`步骤 2:检测到当前会话已登录 ChatGPT,已跳过注册链路(步骤 3/4/5),将直接进入步骤 6。${reasonSuffix}`, 'warn'); + await completeStepFromBackground(2, { + email: resolvedEmail, + nextSignupState: 'already_logged_in_home', + nextSignupUrl: currentUrl || 'https://chatgpt.com/', + skippedPasswordStep: true, + skipRegistrationFlow: true, + }); + return true; + } + + async function submitSignupEmail(resolvedEmail, options = {}) { + const { + timeoutMs = 35000, + retryDelayMs = 700, + logMessage = '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...', + } = options; + + try { + return await sendToContentScriptResilient('signup-page', { + type: 'EXECUTE_STEP', + step: 2, + source: 'background', + payload: { email: resolvedEmail }, + }, { + timeoutMs, + retryDelayMs, + logMessage, + }); + } catch (error) { + return { error: getErrorMessage(error) }; + } + } + async function executeStep2(state) { const resolvedEmail = await resolveSignupEmailForFlow(state); @@ -34,19 +136,73 @@ }); } - const step2Result = await sendToContentScriptResilient('signup-page', { - type: 'EXECUTE_STEP', - step: 2, - source: 'background', - payload: { email: resolvedEmail }, - }, { - timeoutMs: 20000, + if (await shouldForceAuthEntryRetry(signupTabId)) { + await addLog('步骤 2:检测到当前位于已登录 ChatGPT 首页,先切换认证入口页再提交邮箱。', 'warn'); + try { + signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId; + } catch (entryError) { + const entryErrorMessage = getErrorMessage(entryError); + if (await completeStep2AsLoggedInSession(signupTabId, resolvedEmail, entryErrorMessage)) { + return; + } + await addLog('步骤 2:切换认证入口失败,正在重新打开官网入口并重试提交邮箱...', 'warn'); + signupTabId = (await ensureSignupEntryPageReady(2)).tabId; + } + } + + let step2Result = await submitSignupEmail(resolvedEmail, { + timeoutMs: 35000, retryDelayMs: 700, logMessage: '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...', }); if (step2Result?.error) { - throw new Error(step2Result.error); + const errorMessage = getErrorMessage(step2Result.error); + if (isSignupEntryUnavailableErrorMessage(errorMessage)) { + await addLog('步骤 2:未找到邮箱输入入口,正在切换认证入口页后重试一次...', 'warn'); + signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId; + step2Result = await submitSignupEmail(resolvedEmail, { + timeoutMs: 35000, + retryDelayMs: 700, + logMessage: '步骤 2:认证入口页已打开,正在重新提交邮箱...', + }); + + if (step2Result?.error) { + const retryErrorMessage = getErrorMessage(step2Result.error); + if (isSignupEntryUnavailableErrorMessage(retryErrorMessage)) { + if (await completeStep2AsLoggedInSession(signupTabId, resolvedEmail, retryErrorMessage)) { + return; + } + await addLog('步骤 2:认证入口仍不可用,正在重新进入官网注册入口再重试一次...', 'warn'); + signupTabId = (await ensureSignupEntryPageReady(2)).tabId; + step2Result = await submitSignupEmail(resolvedEmail, { + timeoutMs: 35000, + retryDelayMs: 700, + logMessage: '步骤 2:重试官网注册入口后正在重新提交邮箱...', + }); + } + } + } else if (isRetryableStep2TransportErrorMessage(errorMessage)) { + await addLog('步骤 2:注册入口页通信超时,正在切换认证入口页并重试提交邮箱...', 'warn'); + signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId; + step2Result = await submitSignupEmail(resolvedEmail, { + timeoutMs: 45000, + retryDelayMs: 700, + logMessage: '步骤 2:认证入口页已打开,正在重新提交邮箱...', + }); + } + } + + if (step2Result?.error) { + const finalErrorMessage = getErrorMessage(step2Result.error); + if ( + (isSignupEntryUnavailableErrorMessage(finalErrorMessage) + || isRetryableStep2TransportErrorMessage(finalErrorMessage)) + && await completeStep2AsLoggedInSession(signupTabId, resolvedEmail, finalErrorMessage) + ) { + return; + } + throw new Error(finalErrorMessage); } if (!step2Result?.alreadyOnPasswordPage) { diff --git a/background/verification-flow.js b/background/verification-flow.js index 36e9411..3b752eb 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -23,6 +23,7 @@ pollHotmailVerificationCode, pollLuckmailVerificationCode, sendToContentScript, + sendToContentScriptResilient, sendToMailContentScriptResilient, setState, sleepWithStop, @@ -30,6 +31,12 @@ VERIFICATION_POLL_MAX_ROUNDS, } = deps; + const isRetryableVerificationTransportError = typeof deps.isRetryableContentScriptTransportError === 'function' + ? deps.isRetryableContentScriptTransportError + : ((error) => /back\/forward cache|message channel is closed|Receiving end does not exist|port closed before a response was received|A listener indicated an asynchronous response|did not respond in \d+s/i.test( + String(typeof error === 'string' ? error : error?.message || '') + )); + function getVerificationCodeStateKey(step) { return step === 4 ? 'lastSignupCode' : 'lastLoginCode'; } @@ -38,6 +45,89 @@ return step === 4 ? '注册' : '登录'; } + function isLikelyLoggedInChatgptHomeUrl(rawUrl) { + const url = String(rawUrl || '').trim(); + if (!url) return false; + + try { + const parsed = new URL(url); + const host = String(parsed.hostname || '').toLowerCase(); + if (!['chatgpt.com', 'www.chatgpt.com'].includes(host)) { + return false; + } + const path = String(parsed.pathname || ''); + if (/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(path)) { + return false; + } + return true; + } catch { + return false; + } + } + + function isSignupProfilePageUrl(rawUrl) { + const url = String(rawUrl || '').trim(); + if (!url) return false; + + try { + const parsed = new URL(url); + const host = String(parsed.hostname || '').toLowerCase(); + if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) { + return false; + } + return /\/create-account\/profile(?:[/?#]|$)/i.test(String(parsed.pathname || '')); + } catch { + return false; + } + } + + async function detectStep4PostSubmitFallback(tabId, options = {}) { + const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 8000); + const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs) || 250); + const startedAt = Date.now(); + let lastUrl = ''; + + while (Date.now() - startedAt < timeoutMs) { + throwIfStopped(); + try { + const tab = await chrome.tabs.get(tabId); + const currentUrl = String(tab?.url || '').trim(); + if (currentUrl) { + lastUrl = currentUrl; + } + + if (isLikelyLoggedInChatgptHomeUrl(currentUrl)) { + return { + success: true, + reason: 'chatgpt_home', + skipProfileStep: true, + url: currentUrl, + }; + } + + if (isSignupProfilePageUrl(currentUrl)) { + return { + success: true, + reason: 'signup_profile', + skipProfileStep: false, + url: currentUrl, + }; + } + } catch { + // Keep polling until timeout; tab may be mid-navigation. + } + + await sleepWithStop(pollIntervalMs); + } + + return { + success: false, + reason: 'unknown', + skipProfileStep: false, + url: lastUrl, + }; + } + function getVerificationResendStateKey() { return 'verificationResendCount'; } @@ -448,6 +538,17 @@ `步骤 ${step}:距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,继续刷新邮箱(第 ${round}/${maxRounds} 轮)...`, 'info' ); + const configuredIntervalMs = Math.max( + 1, + Number(payloadOverrides.intervalMs) + || Number(pollOverrides.intervalMs) + || 3000 + ); + const cooldownSleepMs = Math.min( + remainingBeforeResendMs, + Math.max(1000, Math.min(configuredIntervalMs, 3000)) + ); + await sleepWithStop(cooldownSleepMs); continue; } @@ -597,19 +698,54 @@ } await chrome.tabs.update(signupTabId, { active: true }); - const result = await sendToContentScript('signup-page', { + const baseResponseTimeoutMs = await getResponseTimeoutMsForStep( + step, + options, + step === 7 ? 45000 : 30000, + `填写${getVerificationCodeLabel(step)}验证码` + ); + const message = { type: 'FILL_CODE', step, source: 'background', payload: { code }, - }, { - responseTimeoutMs: await getResponseTimeoutMsForStep( - step, - options, - step === 7 ? 45000 : 30000, - `填写${getVerificationCodeLabel(step)}验证码` - ), - }); + }; + let result; + if (typeof sendToContentScriptResilient === 'function') { + try { + result = await sendToContentScriptResilient('signup-page', message, { + timeoutMs: Math.max(baseResponseTimeoutMs + 15000, 30000), + retryDelayMs: 700, + responseTimeoutMs: baseResponseTimeoutMs, + logMessage: `步骤 ${step}:认证页正在切换,等待页面重新就绪后继续确认验证码提交结果...`, + }); + } catch (err) { + if (step === 4 && isRetryableVerificationTransportError(err)) { + const fallback = await detectStep4PostSubmitFallback(signupTabId, { + timeoutMs: 9000, + pollIntervalMs: 300, + }); + if (fallback.success) { + const fallbackLabel = fallback.reason === 'chatgpt_home' + ? 'ChatGPT 已登录首页' + : '注册资料页'; + await addLog(`步骤 4:验证码提交后页面已切换到${fallbackLabel},按提交成功继续。`, 'warn'); + return { + success: true, + assumed: true, + transportRecovered: true, + skipProfileStep: Boolean(fallback.skipProfileStep), + url: fallback.url, + }; + } + } + throw err; + } + } else { + result = await sendToContentScript('signup-page', message, { + responseTimeoutMs: baseResponseTimeoutMs, + }); + } if (result && result.error) { throw new Error(result.error); @@ -735,6 +871,7 @@ `步骤 ${step}:提交失败后距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,先继续刷新邮箱(${attempt + 1}/${maxSubmitAttempts})...`, 'warn' ); + await sleepWithStop(Math.min(remainingBeforeResendMs, 2000)); continue; } @@ -763,6 +900,7 @@ await completeStepFromBackground(step, { emailTimestamp: result.emailTimestamp, code: result.code, + ...(step === 4 && submitResult?.skipProfileStep ? { skipProfileStep: true } : {}), }); triggerPostSuccessMailboxCleanup(step, mail); return; diff --git a/content/signup-page.js b/content/signup-page.js index 0b74b3e..91c901d 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -2122,6 +2122,20 @@ async function fillVerificationCode(step, payload) { const { code } = payload; if (!code) throw new Error('未提供验证码。'); + if (step === 4 && isStep5Ready()) { + log(`步骤 ${step}:检测到页面已进入下一阶段,本次验证码提交按成功处理。`, 'ok'); + return { success: true, assumed: true, alreadyAdvanced: true }; + } + if (step === 8) { + if (isStep8Ready()) { + log(`步骤 ${step}:检测到页面已进入 OAuth 同意页,本次验证码提交按成功处理。`, 'ok'); + return { success: true, assumed: true, alreadyAdvanced: true }; + } + if (isAddPhonePageReady()) { + return { success: true, addPhonePage: true, url: location.href }; + } + } + log(`步骤 ${step}:正在填写验证码:${code}`); if (step === 8) { diff --git a/docs/使用教程.md b/docs/使用教程.md index d23a634..d419c53 100644 --- a/docs/使用教程.md +++ b/docs/使用教程.md @@ -1,11 +1,10 @@ -# Codex 注册扩展相关项目、更新、QQ 邮箱切换与 Clash Verge 配置教程 - 本教程用于说明相关项目地址、扩展更新方式、`QQ 邮箱`切换邮箱的使用方法,以及 `Clash Verge` 的 `🔁 非港轮询` 配置方法。 ## 适用场景 - 需要拉取并部署 `cpa` 或 `sub2api` 项目 - 已经安装本扩展,想更新到最新版本 +- 需要把 `Cloudflare Temp Email` 用作 `邮箱生成` 或 `邮箱服务` - 需要临时切换 `QQ 邮箱` 地址继续使用 - 需要在 `Clash Verge` 中启用 `🔁 非港轮询` @@ -14,6 +13,7 @@ - 可以访问 `GitHub` - 已安装好的扩展文件夹 - 可以打开浏览器的 `扩展程序管理` 页面 +- 已准备好 `Cloudflare Temp Email` 后端地址;如需随机子域,域名解析也已配置完成 - 一个可正常登录的 `QQ 邮箱` - 如需部署 `cpa`,部署环境必须可以访问 `OpenAI` - 已安装 `Clash Verge`,并已导入可用订阅 @@ -60,7 +60,42 @@ 找到该扩展后,手动点击一次 `重新加载`。 这一步一定要做,否则浏览器可能仍在使用旧版本。 -### 第三部分:`QQ 邮箱`切换邮箱使用教程 +### 第三部分:`Cloudflare Temp Email` 使用说明 + +1. 先确认当前用途 + `Cloudflare Temp Email` 可以作为 `邮箱生成`,也可以作为 `邮箱服务`。 + 如果两边都选择了它,就需要把两套配置都填完整。 + +2. 填写 `Temp API` + 这里填写 `Cloudflare Temp Email` 后端地址,例如 `https://your-worker-domain`。 + 不论你是拿它来生成邮箱,还是接收转发邮件,这一项都需要先配好。 + +3. 作为 `邮箱生成` 使用时填写 `Admin Auth` + `Admin Auth` 对应后端的 `admin auth`。 + 只有在 `邮箱生成 = Cloudflare Temp Email` 时,这一项才是必填。 + +4. 按需填写 `Custom Auth` + `Custom Auth` 只在站点额外开启访问密码时才需要填写。 + 如果没有开启额外访问密码,留空即可。 + 这一项不会替代 `Admin Auth`。 + +5. 配置 `Temp 域名` + 这里填写允许创建邮箱的基础域名。 + 即使开启了 `随机子域`,这里仍然填写基础域名,而不是随机出来的子域名。 + +6. 按需开启 `随机子域` + `随机子域` 只在 `邮箱生成 = Cloudflare Temp Email` 时使用。 + 后端需要提前配置 `RANDOM_SUBDOMAIN_DOMAINS`,Cloudflare DNS 也需要设置 `MX *`。 + 相关说明可参考 [Issue #942](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942)。 + +7. 作为 `邮箱服务` 使用时填写 `邮件接收` + 只有在 `邮箱服务 = Cloudflare Temp Email` 时,这一项才需要填写。 + 这里填写真正用于接收转发邮件的目标邮箱。 + +8. 查看搭建参考 + 如果你还没有部署后端,可参考 [LINUX DO 教程](https://linux.do/t/topic/316819)。 + +### 第四部分:`QQ 邮箱`切换邮箱使用教程 1. 登录 `QQ 邮箱` 先登录你当前正在使用的 `QQ 邮箱` 账号。 @@ -79,13 +114,15 @@ 这两个邮箱地址使用完成后,可以直接删除。 删除后再次创建新的英文邮箱和 `Foxmail` 邮箱,即可重复注册使用。 -### 第四部分:配置 `Clash Verge` 的 `🔁 非港轮询` +### 第五部分:配置 `Clash Verge` 的 `🔁 非港轮询` #### 第一步:添加扩展脚本 1. 打开 `Clash Verge`,进入左侧的 `订阅`(`Profiles`)界面。 -2. 在右上角或对应位置找到并双击打开 `全局扩展脚本`(`Merge/Script`)。 +2. 在右上角或对应位置找到并双击打开 `全局扩展脚本`。 + 3. 将里面的内容全部清空,替换为下方脚本代码。 + 4. 点击保存,使用右上角保存按钮或按 `Ctrl+S`。 💻 脚本代码(如果遇到格式错误,可让豆包帮你修复后再粘贴) @@ -184,7 +221,7 @@ function main(config, profileName) { 3. 在对应下拉框中选择 `🔁 非港轮询`。 4. 确认右上角的 `代理模式`(`Mode`)已经设置为 `规则模式`(`Rule`)。 5. 确认左侧 `设置` 中的 `系统代理`(`System Proxy`)已经开启。 - + ## 常见问题 ### 为什么第十步显示认证成功,但没有认证文件? @@ -206,6 +243,8 @@ function main(config, profileName) { ## 注意事项 - 不论使用哪种方式更新扩展,更新完成后都必须在浏览器的 `扩展程序管理` 页面重新加载一次该扩展。 +- 如果同时把 `Cloudflare Temp Email` 用作 `邮箱生成` 和 `邮箱服务`,请同时检查 `Admin Auth`、`Custom Auth`、`Temp 域名` 和 `邮件接收` 是否都已配置。 +- 开启 `随机子域` 前,请先确认后端已经配置 `RANDOM_SUBDOMAIN_DOMAINS`,并且 Cloudflare DNS 已完成 `MX *` 设置。 - 如需部署 `cpa`,请先确认部署环境可以访问 `OpenAI`,否则可能出现第十步认证成功但没有认证文件的问题。 - `QQ 邮箱`切换邮箱时,建议在使用完成后及时删除,再重新创建,避免混淆当前正在使用的邮箱地址。 - 使用 `git pull` 更新扩展是最方便、最推荐的方式。 diff --git a/manifest.json b/manifest.json index b55cc7a..d56cca7 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, "name": "codex-oauth-automation-extension", - "version": "7.5", - "version_name": "Pro7.5", + "version": "7.6", + "version_name": "Pro7.6", "description": "用于自动执行多步骤 OAuth 注册流程", "permissions": [ "sidePanel", diff --git a/sidepanel/contribution-mode.js b/sidepanel/contribution-mode.js index c54081c..d6331b5 100644 --- a/sidepanel/contribution-mode.js +++ b/sidepanel/contribution-mode.js @@ -12,7 +12,8 @@ constants = {}, } = context; - const contributionUploadUrl = constants.contributionUploadUrl || 'https://apikey.qzz.io'; + const contributionPortalUrl = constants.contributionPortalUrl || 'https://apikey.qzz.io'; + const contributionUploadUrl = constants.contributionUploadUrl || 'https://apikey.qzz.io/upload'; const pollIntervalMs = Math.max(1500, Math.floor(Number(constants.pollIntervalMs) || 2500)); const hiddenRows = [ @@ -175,10 +176,22 @@ return normalizeString(currentState.contributionStatusMessage) || DEFAULT_COPY; } + function getContributionPortalPageUrl() { + return normalizeString(contributionPortalUrl); + } + function getContributionUploadPageUrl() { return normalizeString(contributionUploadUrl); } + function openContributionPortalPage() { + const targetUrl = getContributionPortalPageUrl(); + if (!targetUrl) { + return; + } + helpers.openExternalUrl?.(targetUrl); + } + function openContributionUploadPage() { const targetUrl = getContributionUploadPageUrl(); if (!targetUrl) { @@ -375,9 +388,9 @@ } actionInFlight = true; try { - openContributionUploadPage(); + openContributionPortalPage(); } catch (error) { - helpers.showToast?.(`打开上传页面失败:${error.message}`, 'error'); + helpers.showToast?.(`打开官网页面失败:${error.message}`, 'error'); } render(); try { diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index c96c47d..923983c 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -523,6 +523,18 @@ header { line-height: 1.6; } +.update-card-reminder { + margin: 0; + padding: 8px 10px; + border: 1px solid color-mix(in srgb, var(--orange) 28%, var(--border)); + border-radius: var(--radius-sm); + background: color-mix(in srgb, var(--orange-soft) 70%, var(--bg-base)); + color: var(--orange); + font-size: 12px; + font-weight: 700; + line-height: 1.5; +} + .update-release-list { display: flex; flex-direction: column; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index c66ccc2..5ae0992 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -16,8 +16,7 @@