From 4f12d67d9ff93900780bbf65f5dd26ffa4623f73 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Thu, 16 Apr 2026 23:38:43 +0800 Subject: [PATCH] feat: implement multi-step background process for user registration - Add step 1: Open ChatGPT homepage and prepare for signup. - Add step 2: Handle email submission and ensure the signup entry page is ready. - Add step 3: Fill in the password using either a custom or generated password. - Add step 4: Confirm the registration verification page and handle email verification. - Add step 5: Generate random user details and handle onboarding redirection. - Add step 6: Manage OAuth login process and handle login verification. - Add step 7: Retrieve verification code from email and manage retries. - Add step 8: Handle consent page interactions and manage callback URLs. - Add step 9: Submit callback information to CPA or SUB2API based on user settings. - Implement tests to ensure all steps are correctly imported and functional. --- background.js | 833 ++++++-------------------- background/steps/step1.js | 21 + background/steps/step2.js | 66 ++ background/steps/step3.js | 60 ++ background/steps/step4.js | 100 ++++ background/steps/step5.js | 69 +++ background/steps/step6.js | 93 +++ background/steps/step7.js | 146 +++++ background/steps/step8.js | 174 ++++++ background/steps/step9.js | 167 ++++++ tests/background-step-modules.test.js | 21 + tests/step8-stop-cleanup.test.js | 121 +++- 12 files changed, 1212 insertions(+), 659 deletions(-) create mode 100644 background/steps/step1.js create mode 100644 background/steps/step2.js create mode 100644 background/steps/step3.js create mode 100644 background/steps/step4.js create mode 100644 background/steps/step5.js create mode 100644 background/steps/step6.js create mode 100644 background/steps/step7.js create mode 100644 background/steps/step8.js create mode 100644 background/steps/step9.js create mode 100644 tests/background-step-modules.test.js diff --git a/background.js b/background.js index 1e93f41..c4d848b 100644 --- a/background.js +++ b/background.js @@ -4,6 +4,15 @@ importScripts( 'background/panel-bridge.js', 'background/generated-email-helpers.js', 'background/signup-flow-helpers.js', + 'background/steps/step1.js', + 'background/steps/step2.js', + 'background/steps/step3.js', + 'background/steps/step4.js', + 'background/steps/step5.js', + 'background/steps/step6.js', + 'background/steps/step7.js', + 'background/steps/step8.js', + 'background/steps/step9.js', 'data/names.js', 'hotmail-utils.js', 'microsoft-email.js', @@ -6407,6 +6416,123 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe SIGNUP_PAGE_INJECT_FILES, waitForTabUrlMatch, }); +const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({ + addLog, + completeStepFromBackground, + ensureSignupEntryPageReady, +}); +const step2Executor = self.MultiPageBackgroundStep2?.createStep2Executor({ + addLog, + chrome, + completeStepFromBackground, + ensureContentScriptReadyOnTab, + ensureSignupEntryPageReady, + ensureSignupPasswordPageReadyInTab, + getTabId, + isTabAlive, + resolveSignupEmailForFlow, + sendToContentScriptResilient, + SIGNUP_PAGE_INJECT_FILES, +}); +const step3Executor = self.MultiPageBackgroundStep3?.createStep3Executor({ + addLog, + chrome, + ensureContentScriptReadyOnTab, + generatePassword, + getTabId, + isTabAlive, + sendToContentScript, + setPasswordState, + setState, + SIGNUP_PAGE_INJECT_FILES, +}); +const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({ + addLog, + chrome, + completeStepFromBackground, + confirmCustomVerificationStepBypass, + getMailConfig, + getTabId, + HOTMAIL_PROVIDER, + isTabAlive, + LUCKMAIL_PROVIDER, + CLOUDFLARE_TEMP_EMAIL_PROVIDER, + resolveVerificationStep, + reuseOrCreateTab, + sendToContentScriptResilient, + shouldUseCustomRegistrationEmail, + STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, + throwIfStopped, +}); +const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({ + addLog, + generateRandomBirthday, + generateRandomName, + getTabId, + handleChatgptOnboardingSkip, + isRetryableContentScriptTransportError, + LOG_PREFIX, + sendToContentScript, + waitForStep5ChatgptRedirect, +}); +const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({ + addLog, + completeStepFromBackground, + getLoginAuthStateLabel, + getState, + isStep6RecoverableResult, + isStep6SuccessResult, + refreshOAuthUrlBeforeStep6, + reuseOrCreateTab, + runPreStep6CookieCleanup, + sendToContentScriptResilient, + shouldSkipLoginVerificationForCpaCallback, + skipLoginVerificationStepsForCpaCallback, + throwIfStopped, +}); +const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({ + addLog, + chrome, + CLOUDFLARE_TEMP_EMAIL_PROVIDER, + confirmCustomVerificationStepBypass, + ensureStep7VerificationPageReady, + executeStep6: (...args) => executeStep6(...args), + getMailConfig, + getState, + getTabId, + HOTMAIL_PROVIDER, + isTabAlive, + isVerificationMailPollingError, + LUCKMAIL_PROVIDER, + resolveVerificationStep, + reuseOrCreateTab, + setState, + setStepStatus, + shouldSkipLoginVerificationForCpaCallback, + shouldUseCustomRegistrationEmail, + sleepWithStop, + STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, + STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS, + throwIfStopped, +}); +const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({ + addLog, + chrome, + closeConflictingTabsForSource, + completeStepFromBackground, + ensureContentScriptReadyOnTab, + getPanelMode, + getTabId, + isLocalhostOAuthCallbackUrl, + isTabAlive, + normalizeSub2ApiUrl, + rememberSourceLastUrl, + reuseOrCreateTab, + sendToContentScript, + sendToContentScriptResilient, + shouldBypassStep9ForLocalCpa, + SUB2API_STEP9_RESPONSE_TIMEOUT_MS, +}); async function requestOAuthUrlFromPanel(state, options = {}) { return panelBridge.requestOAuthUrlFromPanel(state, options); @@ -6441,9 +6567,7 @@ async function resolveSignupEmailForFlow(state) { // ============================================================ async function executeStep1() { - await addLog('步骤 1:正在打开 ChatGPT 官网...'); - await ensureSignupEntryPageReady(1); - await completeStepFromBackground(1, {}); + return step1Executor.executeStep1(); } // ============================================================ @@ -6451,46 +6575,7 @@ async function executeStep1() { // ============================================================ async function executeStep2(state) { - const resolvedEmail = await resolveSignupEmailForFlow(state); - - let signupTabId = await getTabId('signup-page'); - if (!signupTabId || !(await isTabAlive('signup-page'))) { - await addLog('步骤 2:未发现可用的注册页标签,正在重新打开 ChatGPT 官网...', 'warn'); - signupTabId = (await ensureSignupEntryPageReady(2)).tabId; - } else { - await chrome.tabs.update(signupTabId, { active: true }); - await ensureContentScriptReadyOnTab('signup-page', signupTabId, { - inject: SIGNUP_PAGE_INJECT_FILES, - injectSource: 'signup-page', - timeoutMs: 45000, - retryDelayMs: 900, - logMessage: '步骤 2:注册入口页内容脚本未就绪,正在等待页面恢复...', - }); - } - - const step2Result = await sendToContentScriptResilient('signup-page', { - type: 'EXECUTE_STEP', - step: 2, - source: 'background', - payload: { email: resolvedEmail }, - }, { - timeoutMs: 20000, - retryDelayMs: 700, - logMessage: '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...', - }); - - if (step2Result?.error) { - throw new Error(step2Result.error); - } - - if (!step2Result?.alreadyOnPasswordPage) { - await addLog(`步骤 2:邮箱 ${resolvedEmail} 已提交,正在等待进入密码页...`); - } - - await ensureSignupPasswordPageReadyInTab(signupTabId, 2, { - skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage), - }); - await completeStepFromBackground(2, {}); + return step2Executor.executeStep2(state); } // ============================================================ @@ -6498,41 +6583,7 @@ async function executeStep2(state) { // ============================================================ async function executeStep3(state) { - const resolvedEmail = state.email; - if (!resolvedEmail) { - throw new Error('缺少邮箱地址,请先完成步骤 2。'); - } - - const signupTabId = await getTabId('signup-page'); - if (!signupTabId || !(await isTabAlive('signup-page'))) { - throw new Error('认证页面标签页已关闭,请先重新完成步骤 2。'); - } - - const password = state.customPassword || generatePassword(); - await setPasswordState(password); - - const accounts = state.accounts || []; - accounts.push({ email: resolvedEmail, password, createdAt: new Date().toISOString() }); - await setState({ accounts }); - - await chrome.tabs.update(signupTabId, { active: true }); - await ensureContentScriptReadyOnTab('signup-page', signupTabId, { - inject: SIGNUP_PAGE_INJECT_FILES, - injectSource: 'signup-page', - timeoutMs: 45000, - retryDelayMs: 900, - logMessage: '步骤 3:密码页内容脚本未就绪,正在等待页面恢复...', - }); - - await addLog( - `步骤 3:正在填写密码,邮箱为 ${resolvedEmail},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)` - ); - await sendToContentScript('signup-page', { - type: 'EXECUTE_STEP', - step: 3, - source: 'background', - payload: { email: resolvedEmail, password }, - }); + return step3Executor.executeStep3(state); } // ============================================================ @@ -7075,77 +7126,7 @@ async function resolveVerificationStep(step, state, mail, options = {}) { } async function executeStep4(state) { - const mail = getMailConfig(state); - if (mail.error) throw new Error(mail.error); - const stepStartedAt = Date.now(); - const signupTabId = await getTabId('signup-page'); - if (!signupTabId) { - throw new Error('认证页面标签页已关闭,无法继续步骤 4。'); - } - - await chrome.tabs.update(signupTabId, { active: true }); - throwIfStopped(); - await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...'); - const prepareResult = await sendToContentScriptResilient( - 'signup-page', - { - type: 'PREPARE_SIGNUP_VERIFICATION', - step: 4, - source: 'background', - payload: { password: state.password || state.customPassword || '' }, - }, - { - timeoutMs: 30000, - retryDelayMs: 700, - logMessage: '步骤 4:认证页正在切换,等待页面重新就绪后继续检测...', - } - ); - - if (prepareResult && prepareResult.error) { - throw new Error(prepareResult.error); - } - if (prepareResult?.alreadyVerified) { - await completeStepFromBackground(4, {}); - return; - } - - if (shouldUseCustomRegistrationEmail(state)) { - await confirmCustomVerificationStepBypass(4); - return; - } - - throwIfStopped(); - if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { - await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); - } else { - await addLog(`步骤 4:正在打开${mail.label}...`); - - // For mail tabs, only create if not alive — don't navigate (preserves login session) - const alive = await isTabAlive(mail.source); - if (alive) { - if (mail.navigateOnReuse) { - await reuseOrCreateTab(mail.source, mail.url, { - inject: mail.inject, - injectSource: mail.injectSource, - }); - } else { - const tabId = await getTabId(mail.source); - await chrome.tabs.update(tabId, { active: true }); - } - } else { - await reuseOrCreateTab(mail.source, mail.url, { - inject: mail.inject, - injectSource: mail.injectSource, - }); - } - } - - await resolveVerificationStep(4, state, mail, { - filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt, - requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true, - resendIntervalMs: mail.provider === HOTMAIL_PROVIDER ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, - }); - return; + return step4Executor.executeStep4(state); } // ============================================================ @@ -7174,57 +7155,7 @@ async function waitForStep5ChatgptRedirect(tabId, timeoutMs = 15000) { } async function executeStep5(state) { - const { firstName, lastName } = generateRandomName(); - const { year, month, day } = generateRandomBirthday(); - - await addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`); - - let step5Result = null; - let step5TransportError = null; - - try { - step5Result = await sendToContentScript('signup-page', { - type: 'EXECUTE_STEP', - step: 5, - source: 'background', - payload: { firstName, lastName, year, month, day }, - }); - } catch (err) { - // Cross-origin navigation (auth page → chatgpt.com) destroys the content - // script, causing a transport error. Capture it so we can check if the tab - // ended up on chatgpt.com onboarding. - if (isRetryableContentScriptTransportError(err)) { - step5TransportError = err; - console.log(LOG_PREFIX, '步骤 5:内容脚本通信中断,正在检查是否跳转到 ChatGPT 引导页...', err?.message); - } else { - throw err; - } - } - - // Case 1: content script successfully detected chatgpt redirect before dying - if (step5Result?.chatgptOnboarding) { - await addLog('步骤 5:检测到 ChatGPT 引导页跳转,正在处理引导页跳过...'); - await handleChatgptOnboardingSkip(); - return; - } - - if (step5Result?.chatgptHome) { - await addLog('步骤 5:检测到已进入 ChatGPT 页面,注册成功。', 'ok'); - return; - } - - // Case 2: content script died due to cross-origin navigation — check tab URL - if (step5TransportError) { - const signupTabId = await getTabId('signup-page'); - const redirectedTab = await waitForStep5ChatgptRedirect(signupTabId); - if (redirectedTab) { - await addLog('步骤 5:内容脚本因页面跳转到 ChatGPT 而断开,正在处理引导页跳过...'); - await handleChatgptOnboardingSkip(redirectedTab.id); - return; - } - // Not on chatgpt.com — re-throw the original error - throw step5TransportError; - } + return step5Executor.executeStep5(state); } async function handleChatgptOnboardingSkip(knownTabId) { @@ -7479,186 +7410,15 @@ async function skipLoginVerificationStepsForCpaCallback() { } async function executeStep6(state) { - if (shouldSkipLoginVerificationForCpaCallback(state)) { - await skipLoginVerificationStepsForCpaCallback(); - return; - } - if (!state.email) { - throw new Error('缺少邮箱地址,请先完成步骤 3。'); - } - - await runPreStep6CookieCleanup(); - - let attempt = 0; - - while (true) { - throwIfStopped(); - attempt += 1; - const currentState = attempt === 1 ? state : await getState(); - const password = currentState.password || currentState.customPassword || ''; - const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState); - - if (attempt === 1) { - await addLog('步骤 6:正在打开最新 OAuth 链接并登录...'); - } else { - await addLog(`步骤 6:上一轮登录未进入验证码页,正在重新发起第 ${attempt} 轮登录尝试...`, 'warn'); - } - - await reuseOrCreateTab('signup-page', oauthUrl); - - const result = await sendToContentScriptResilient( - 'signup-page', - { - type: 'EXECUTE_STEP', - step: 6, - source: 'background', - payload: { - email: currentState.email, - password, - }, - }, - { - timeoutMs: 180000, - retryDelayMs: 700, - logMessage: '步骤 6:认证页正在切换,等待页面重新就绪后继续登录...', - } - ); - - if (result?.error) { - throw new Error(result.error); - } - - if (isStep6SuccessResult(result)) { - await completeStepFromBackground(6, { - loginVerificationRequestedAt: result.loginVerificationRequestedAt || null, - }); - return; - } - - if (isStep6RecoverableResult(result)) { - const reasonMessage = result.message - || `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 6。`; - await addLog(`步骤 6:${reasonMessage}`, 'warn'); - continue; - } - - throw new Error('步骤 6:认证页未返回可识别的登录结果。'); - } + return step6Executor.executeStep6(state); } // ============================================================ // Step 7: Poll login verification mail and submit the login code // ============================================================ -async function runStep7Attempt(state) { - const mail = getMailConfig(state); - if (mail.error) throw new Error(mail.error); - const stepStartedAt = Date.now(); - const authTabId = await getTabId('signup-page'); - - if (authTabId) { - await chrome.tabs.update(authTabId, { active: true }); - } else { - if (!state.oauthUrl) { - throw new Error('缺少登录用 OAuth 链接,请先完成步骤 6。'); - } - await reuseOrCreateTab('signup-page', state.oauthUrl); - } - - throwIfStopped(); - await ensureStep7VerificationPageReady(); - await addLog('步骤 7:登录验证码页面已就绪,开始获取验证码。', 'info'); - - if (shouldUseCustomRegistrationEmail(state)) { - await confirmCustomVerificationStepBypass(7); - return; - } - - throwIfStopped(); - if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { - await addLog(`步骤 7:正在通过 ${mail.label} 轮询验证码...`); - } else { - await addLog(`步骤 7:正在打开${mail.label}...`); - - const alive = await isTabAlive(mail.source); - if (alive) { - if (mail.navigateOnReuse) { - await reuseOrCreateTab(mail.source, mail.url, { - inject: mail.inject, - injectSource: mail.injectSource, - }); - } else { - const tabId = await getTabId(mail.source); - await chrome.tabs.update(tabId, { active: true }); - } - } else { - await reuseOrCreateTab(mail.source, mail.url, { - inject: mail.inject, - injectSource: mail.injectSource, - }); - } - } - - await resolveVerificationStep(7, state, mail, { - filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : Math.max(0, stepStartedAt - 60000), - requestFreshCodeFirst: false, - resendIntervalMs: mail.provider === HOTMAIL_PROVIDER ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, - }); -} - -async function rerunStep6ForStep7Recovery() { - const currentState = await getState(); - await addLog('步骤 7:正在回到步骤 6,重新发起登录验证码流程...', 'warn'); - await executeStep6(currentState); - await sleepWithStop(3000); -} - async function executeStep7(state) { - if (shouldSkipLoginVerificationForCpaCallback(state)) { - await setState({ - lastLoginCode: null, - loginVerificationRequestedAt: null, - }); - await setStepStatus(7, 'skipped'); - await addLog('步骤 7:当前已选择“第六步回调”,本轮无需获取登录验证码。', 'warn'); - return; - } - - let currentState = state; - let mailPollingAttempt = 1; - let lastMailPollingError = null; - - while (true) { - try { - await runStep7Attempt(currentState); - return; - } catch (err) { - if (!isVerificationMailPollingError(err)) { - throw err; - } - - lastMailPollingError = err; - if (mailPollingAttempt >= STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS) { - break; - } - - mailPollingAttempt += 1; - await addLog( - `步骤 7:检测到邮箱轮询类失败,准备从步骤 6 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...`, - 'warn' - ); - await rerunStep6ForStep7Recovery(); - currentState = await getState(); - } - } - - if (lastMailPollingError) { - throw new Error( - `步骤 7:登录验证码流程在 ${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS} 轮邮箱轮询恢复后仍未成功。最后一次原因:${lastMailPollingError.message}` - ); - } - - throw new Error('步骤 7:登录验证码流程未成功完成。'); + return step7Executor.executeStep7(state); } // ============================================================ @@ -7681,6 +7441,34 @@ const STEP8_STRATEGIES = [ { mode: 'debugger', label: 'debugger click retry' }, ]; +function setWebNavListener(listener) { + webNavListener = listener; +} + +function getWebNavListener() { + return webNavListener; +} + +function setWebNavCommittedListener(listener) { + webNavCommittedListener = listener; +} + +function getWebNavCommittedListener() { + return webNavCommittedListener; +} + +function setStep8TabUpdatedListener(listener) { + step8TabUpdatedListener = listener; +} + +function getStep8TabUpdatedListener() { + return step8TabUpdatedListener; +} + +function setStep8PendingReject(handler) { + step8PendingReject = handler; +} + function cleanupStep8NavigationListeners() { if (webNavListener) { chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener); @@ -7909,139 +7697,41 @@ function getStep8EffectLabel(effect) { } } +const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ + addLog, + chrome, + cleanupStep8NavigationListeners, + clickWithDebugger, + completeStepFromBackground, + ensureStep8SignupPageReady, + getStep8CallbackUrlFromNavigation, + getStep8CallbackUrlFromTabUpdate, + getStep8EffectLabel, + getTabId, + getWebNavCommittedListener, + getWebNavListener, + getStep8TabUpdatedListener, + isTabAlive, + prepareStep8DebuggerClick, + reloadStep8ConsentPage, + reuseOrCreateTab, + setStep8PendingReject, + setStep8TabUpdatedListener, + setWebNavCommittedListener, + setWebNavListener, + sleepWithStop, + STEP8_CLICK_RETRY_DELAY_MS, + STEP8_MAX_ROUNDS, + STEP8_READY_WAIT_TIMEOUT_MS, + STEP8_STRATEGIES, + throwIfStep8SettledOrStopped, + triggerStep8ContentStrategy, + waitForStep8ClickEffect, + waitForStep8Ready, +}); + async function executeStep8(state) { - if (!state.oauthUrl) { - throw new Error('缺少登录用 OAuth 链接,请先完成步骤 6。'); - } - - await addLog('步骤 8:正在监听 localhost 回调地址...'); - - return new Promise((resolve, reject) => { - let resolved = false; - let signupTabId = null; - - const cleanupListener = () => { - cleanupStep8NavigationListeners(); - step8PendingReject = null; - }; - - const rejectStep8 = (error) => { - if (resolved) return; - resolved = true; - clearTimeout(timeout); - cleanupListener(); - reject(error); - }; - - const finalizeStep8Callback = (callbackUrl) => { - if (resolved || !callbackUrl) return; - - resolved = true; - cleanupListener(); - clearTimeout(timeout); - - addLog(`步骤 8:已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => { - return completeStepFromBackground(8, { localhostUrl: callbackUrl }); - }).then(() => { - resolve(); - }).catch((err) => { - reject(err); - }); - }; - - const timeout = setTimeout(() => { - rejectStep8(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 8 的点击可能被拦截了。')); - }, 120000); - - step8PendingReject = (error) => { - rejectStep8(error); - }; - - webNavListener = (details) => { - const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId); - finalizeStep8Callback(callbackUrl); - }; - - webNavCommittedListener = (details) => { - const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId); - finalizeStep8Callback(callbackUrl); - }; - - step8TabUpdatedListener = (tabId, changeInfo, tab) => { - const callbackUrl = getStep8CallbackUrlFromTabUpdate(tabId, changeInfo, tab, signupTabId); - finalizeStep8Callback(callbackUrl); - }; - - (async () => { - try { - throwIfStep8SettledOrStopped(resolved); - signupTabId = await getTabId('signup-page'); - throwIfStep8SettledOrStopped(resolved); - - if (signupTabId && await isTabAlive('signup-page')) { - await chrome.tabs.update(signupTabId, { active: true }); - await addLog('步骤 8:已切回认证页,正在准备调试器点击...'); - } else { - signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl); - await addLog('步骤 8:已重新打开认证页,正在准备调试器点击...'); - } - - throwIfStep8SettledOrStopped(resolved); - chrome.webNavigation.onBeforeNavigate.addListener(webNavListener); - chrome.webNavigation.onCommitted.addListener(webNavCommittedListener); - chrome.tabs.onUpdated.addListener(step8TabUpdatedListener); - await ensureStep8SignupPageReady(signupTabId, { - timeoutMs: 15000, - logMessage: '步骤 8:认证页内容脚本尚未就绪,正在等待页面恢复...', - }); - - for (let round = 1; round <= STEP8_MAX_ROUNDS && !resolved; round++) { - throwIfStep8SettledOrStopped(resolved); - const pageState = await waitForStep8Ready(signupTabId); - if (!pageState?.consentReady) { - await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS); - continue; - } - - const strategy = STEP8_STRATEGIES[Math.min(round - 1, STEP8_STRATEGIES.length - 1)]; - - await addLog(`步骤 8:第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label})...`); - - if (strategy.mode === 'debugger') { - const clickTarget = await prepareStep8DebuggerClick(signupTabId); - throwIfStep8SettledOrStopped(resolved); - await clickWithDebugger(signupTabId, clickTarget?.rect); - } else { - await triggerStep8ContentStrategy(signupTabId, strategy.strategy); - } - - if (resolved) { - return; - } - - const effect = await waitForStep8ClickEffect(signupTabId, pageState.url); - if (resolved) { - return; - } - - if (effect.progressed) { - await addLog(`步骤 8:检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info'); - break; - } - - if (round >= STEP8_MAX_ROUNDS) { - throw new Error(`步骤 8:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`); - } - - await addLog(`步骤 8:${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS})...`, 'warn'); - await reloadStep8ConsentPage(signupTabId); - await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS); - } - } catch (err) { - rejectStep8(err); - } - })(); - }); + return step8Executor.executeStep8(state); } // ============================================================ @@ -8049,138 +7739,7 @@ async function executeStep8(state) { // ============================================================ async function executeStep9(state) { - if (getPanelMode(state) === 'sub2api') { - return executeSub2ApiStep9(state); - } - return executeCpaStep9(state); -} - -async function executeCpaStep9(state) { - if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { - throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。'); - } - if (!state.localhostUrl) { - throw new Error('缺少 localhost 回调地址,请先完成步骤 8。'); - } - if (!state.vpsUrl) { - throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。'); - } - - if (shouldBypassStep9ForLocalCpa(state)) { - await addLog('步骤 9:检测到本地 CPA,且当前策略为“跳过第9步”,本轮不再重复提交回调地址。', 'info'); - await completeStepFromBackground(9, { - localhostUrl: state.localhostUrl, - verifiedStatus: 'local-auto', - }); - return; - } - - await addLog('步骤 9:正在打开 CPA 面板...'); - - const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js']; - let tabId = await getTabId('vps-panel'); - const alive = tabId && await isTabAlive('vps-panel'); - - if (!alive) { - tabId = await reuseOrCreateTab('vps-panel', state.vpsUrl, { - inject: injectFiles, - reloadIfSameUrl: true, - }); - } else { - await closeConflictingTabsForSource('vps-panel', state.vpsUrl, { excludeTabIds: [tabId] }); - await chrome.tabs.update(tabId, { active: true }); - await rememberSourceLastUrl('vps-panel', state.vpsUrl); - } - - await ensureContentScriptReadyOnTab('vps-panel', tabId, { - inject: injectFiles, - timeoutMs: 45000, - retryDelayMs: 900, - logMessage: '步骤 9:CPA 面板仍在加载,正在重试连接...', - }); - - await addLog('步骤 9:正在填写回调地址...'); - const result = await sendToContentScriptResilient('vps-panel', { - type: 'EXECUTE_STEP', - step: 9, - source: 'background', - payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword }, - }, { - timeoutMs: 30000, - retryDelayMs: 700, - logMessage: '步骤 9:CPA 面板通信未就绪,正在等待页面恢复...', - }); - - if (result?.error) { - throw new Error(result.error); - } -} - -async function executeSub2ApiStep9(state) { - if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { - throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。'); - } - if (!state.localhostUrl) { - throw new Error('缺少 localhost 回调地址,请先完成步骤 8。'); - } - if (!state.sub2apiSessionId) { - throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。'); - } - if (!state.sub2apiEmail) { - throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。'); - } - if (!state.sub2apiPassword) { - throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。'); - } - - const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl); - const injectFiles = ['content/utils.js', 'content/sub2api-panel.js']; - - await addLog('步骤 9:正在打开 SUB2API 后台...'); - - let tabId = await getTabId('sub2api-panel'); - const alive = tabId && await isTabAlive('sub2api-panel'); - - if (!alive) { - tabId = await reuseOrCreateTab('sub2api-panel', sub2apiUrl, { - inject: injectFiles, - injectSource: 'sub2api-panel', - reloadIfSameUrl: true, - }); - } else { - await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl, { excludeTabIds: [tabId] }); - await chrome.tabs.update(tabId, { active: true }); - await rememberSourceLastUrl('sub2api-panel', sub2apiUrl); - } - - await ensureContentScriptReadyOnTab('sub2api-panel', tabId, { - inject: injectFiles, - injectSource: 'sub2api-panel', - }); - - await addLog('步骤 9:正在向 SUB2API 提交回调并创建账号...'); - const result = await sendToContentScript('sub2api-panel', { - type: 'EXECUTE_STEP', - step: 9, - source: 'background', - payload: { - localhostUrl: state.localhostUrl, - sub2apiUrl, - sub2apiEmail: state.sub2apiEmail, - sub2apiPassword: state.sub2apiPassword, - sub2apiGroupName: state.sub2apiGroupName, - sub2apiSessionId: state.sub2apiSessionId, - sub2apiOAuthState: state.sub2apiOAuthState, - sub2apiGroupId: state.sub2apiGroupId, - sub2apiDraftName: state.sub2apiDraftName, - }, - }, { - responseTimeoutMs: SUB2API_STEP9_RESPONSE_TIMEOUT_MS, - }); - - if (result?.error) { - throw new Error(result.error); - } + return step9Executor.executeStep9(state); } // ============================================================ diff --git a/background/steps/step1.js b/background/steps/step1.js new file mode 100644 index 0000000..13042e3 --- /dev/null +++ b/background/steps/step1.js @@ -0,0 +1,21 @@ +(function attachBackgroundStep1(root, factory) { + root.MultiPageBackgroundStep1 = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep1Module() { + function createStep1Executor(deps = {}) { + const { + addLog, + completeStepFromBackground, + ensureSignupEntryPageReady, + } = deps; + + async function executeStep1() { + await addLog('步骤 1:正在打开 ChatGPT 官网...'); + await ensureSignupEntryPageReady(1); + await completeStepFromBackground(1, {}); + } + + return { executeStep1 }; + } + + return { createStep1Executor }; +}); diff --git a/background/steps/step2.js b/background/steps/step2.js new file mode 100644 index 0000000..669b828 --- /dev/null +++ b/background/steps/step2.js @@ -0,0 +1,66 @@ +(function attachBackgroundStep2(root, factory) { + root.MultiPageBackgroundStep2 = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep2Module() { + function createStep2Executor(deps = {}) { + const { + addLog, + chrome, + completeStepFromBackground, + ensureContentScriptReadyOnTab, + ensureSignupEntryPageReady, + ensureSignupPasswordPageReadyInTab, + getTabId, + isTabAlive, + resolveSignupEmailForFlow, + sendToContentScriptResilient, + SIGNUP_PAGE_INJECT_FILES, + } = deps; + + async function executeStep2(state) { + const resolvedEmail = await resolveSignupEmailForFlow(state); + + let signupTabId = await getTabId('signup-page'); + if (!signupTabId || !(await isTabAlive('signup-page'))) { + await addLog('步骤 2:未发现可用的注册页标签,正在重新打开 ChatGPT 官网...', 'warn'); + signupTabId = (await ensureSignupEntryPageReady(2)).tabId; + } else { + await chrome.tabs.update(signupTabId, { active: true }); + await ensureContentScriptReadyOnTab('signup-page', signupTabId, { + inject: SIGNUP_PAGE_INJECT_FILES, + injectSource: 'signup-page', + timeoutMs: 45000, + retryDelayMs: 900, + logMessage: '步骤 2:注册入口页内容脚本未就绪,正在等待页面恢复...', + }); + } + + const step2Result = await sendToContentScriptResilient('signup-page', { + type: 'EXECUTE_STEP', + step: 2, + source: 'background', + payload: { email: resolvedEmail }, + }, { + timeoutMs: 20000, + retryDelayMs: 700, + logMessage: '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...', + }); + + if (step2Result?.error) { + throw new Error(step2Result.error); + } + + if (!step2Result?.alreadyOnPasswordPage) { + await addLog(`步骤 2:邮箱 ${resolvedEmail} 已提交,正在等待进入密码页...`); + } + + await ensureSignupPasswordPageReadyInTab(signupTabId, 2, { + skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage), + }); + await completeStepFromBackground(2, {}); + } + + return { executeStep2 }; + } + + return { createStep2Executor }; +}); diff --git a/background/steps/step3.js b/background/steps/step3.js new file mode 100644 index 0000000..d3937a2 --- /dev/null +++ b/background/steps/step3.js @@ -0,0 +1,60 @@ +(function attachBackgroundStep3(root, factory) { + root.MultiPageBackgroundStep3 = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep3Module() { + function createStep3Executor(deps = {}) { + const { + addLog, + chrome, + ensureContentScriptReadyOnTab, + generatePassword, + getTabId, + isTabAlive, + sendToContentScript, + setPasswordState, + setState, + SIGNUP_PAGE_INJECT_FILES, + } = deps; + + async function executeStep3(state) { + const resolvedEmail = state.email; + if (!resolvedEmail) { + throw new Error('缺少邮箱地址,请先完成步骤 2。'); + } + + const signupTabId = await getTabId('signup-page'); + if (!signupTabId || !(await isTabAlive('signup-page'))) { + throw new Error('认证页面标签页已关闭,请先重新完成步骤 2。'); + } + + const password = state.customPassword || generatePassword(); + await setPasswordState(password); + + const accounts = state.accounts || []; + accounts.push({ email: resolvedEmail, password, createdAt: new Date().toISOString() }); + await setState({ accounts }); + + await chrome.tabs.update(signupTabId, { active: true }); + await ensureContentScriptReadyOnTab('signup-page', signupTabId, { + inject: SIGNUP_PAGE_INJECT_FILES, + injectSource: 'signup-page', + timeoutMs: 45000, + retryDelayMs: 900, + logMessage: '步骤 3:密码页内容脚本未就绪,正在等待页面恢复...', + }); + + await addLog( + `步骤 3:正在填写密码,邮箱为 ${resolvedEmail},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)` + ); + await sendToContentScript('signup-page', { + type: 'EXECUTE_STEP', + step: 3, + source: 'background', + payload: { email: resolvedEmail, password }, + }); + } + + return { executeStep3 }; + } + + return { createStep3Executor }; +}); diff --git a/background/steps/step4.js b/background/steps/step4.js new file mode 100644 index 0000000..a7ddc11 --- /dev/null +++ b/background/steps/step4.js @@ -0,0 +1,100 @@ +(function attachBackgroundStep4(root, factory) { + root.MultiPageBackgroundStep4 = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep4Module() { + function createStep4Executor(deps = {}) { + const { + addLog, + chrome, + completeStepFromBackground, + confirmCustomVerificationStepBypass, + getMailConfig, + getTabId, + HOTMAIL_PROVIDER, + isTabAlive, + LUCKMAIL_PROVIDER, + CLOUDFLARE_TEMP_EMAIL_PROVIDER, + resolveVerificationStep, + reuseOrCreateTab, + sendToContentScriptResilient, + shouldUseCustomRegistrationEmail, + STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, + throwIfStopped, + } = deps; + + async function executeStep4(state) { + const mail = getMailConfig(state); + if (mail.error) throw new Error(mail.error); + const stepStartedAt = Date.now(); + const signupTabId = await getTabId('signup-page'); + if (!signupTabId) { + throw new Error('认证页面标签页已关闭,无法继续步骤 4。'); + } + + await chrome.tabs.update(signupTabId, { active: true }); + throwIfStopped(); + await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...'); + const prepareResult = await sendToContentScriptResilient( + 'signup-page', + { + type: 'PREPARE_SIGNUP_VERIFICATION', + step: 4, + source: 'background', + payload: { password: state.password || state.customPassword || '' }, + }, + { + timeoutMs: 30000, + retryDelayMs: 700, + logMessage: '步骤 4:认证页正在切换,等待页面重新就绪后继续检测...', + } + ); + + if (prepareResult && prepareResult.error) { + throw new Error(prepareResult.error); + } + if (prepareResult?.alreadyVerified) { + await completeStepFromBackground(4, {}); + return; + } + + if (shouldUseCustomRegistrationEmail(state)) { + await confirmCustomVerificationStepBypass(4); + return; + } + + throwIfStopped(); + if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { + await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); + } else { + await addLog(`步骤 4:正在打开${mail.label}...`); + + const alive = await isTabAlive(mail.source); + if (alive) { + if (mail.navigateOnReuse) { + await reuseOrCreateTab(mail.source, mail.url, { + inject: mail.inject, + injectSource: mail.injectSource, + }); + } else { + const tabId = await getTabId(mail.source); + await chrome.tabs.update(tabId, { active: true }); + } + } else { + await reuseOrCreateTab(mail.source, mail.url, { + inject: mail.inject, + injectSource: mail.injectSource, + }); + } + } + + await resolveVerificationStep(4, state, mail, { + filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt, + requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true, + resendIntervalMs: mail.provider === HOTMAIL_PROVIDER ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, + }); + } + + return { executeStep4 }; + } + + return { createStep4Executor }; +}); diff --git a/background/steps/step5.js b/background/steps/step5.js new file mode 100644 index 0000000..53699e0 --- /dev/null +++ b/background/steps/step5.js @@ -0,0 +1,69 @@ +(function attachBackgroundStep5(root, factory) { + root.MultiPageBackgroundStep5 = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep5Module() { + function createStep5Executor(deps = {}) { + const { + addLog, + generateRandomBirthday, + generateRandomName, + getTabId, + handleChatgptOnboardingSkip, + isRetryableContentScriptTransportError, + LOG_PREFIX, + sendToContentScript, + waitForStep5ChatgptRedirect, + } = deps; + + async function executeStep5() { + const { firstName, lastName } = generateRandomName(); + const { year, month, day } = generateRandomBirthday(); + + await addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`); + + let step5Result = null; + let step5TransportError = null; + + try { + step5Result = await sendToContentScript('signup-page', { + type: 'EXECUTE_STEP', + step: 5, + source: 'background', + payload: { firstName, lastName, year, month, day }, + }); + } catch (err) { + if (isRetryableContentScriptTransportError(err)) { + step5TransportError = err; + console.log(LOG_PREFIX, '步骤 5:内容脚本通信中断,正在检查是否跳转到 ChatGPT 引导页...', err?.message); + } else { + throw err; + } + } + + if (step5Result?.chatgptOnboarding) { + await addLog('步骤 5:检测到 ChatGPT 引导页跳转,正在处理引导页跳过...'); + await handleChatgptOnboardingSkip(); + return; + } + + if (step5Result?.chatgptHome) { + await addLog('步骤 5:检测到已进入 ChatGPT 页面,注册成功。', 'ok'); + return; + } + + if (step5TransportError) { + const signupTabId = await getTabId('signup-page'); + const redirectedTab = await waitForStep5ChatgptRedirect(signupTabId); + if (redirectedTab) { + await addLog('步骤 5:内容脚本因页面跳转到 ChatGPT 而断开,正在处理引导页跳过...'); + await handleChatgptOnboardingSkip(redirectedTab.id); + return; + } + throw step5TransportError; + } + } + + return { executeStep5 }; + } + + return { createStep5Executor }; +}); diff --git a/background/steps/step6.js b/background/steps/step6.js new file mode 100644 index 0000000..ce77fbc --- /dev/null +++ b/background/steps/step6.js @@ -0,0 +1,93 @@ +(function attachBackgroundStep6(root, factory) { + root.MultiPageBackgroundStep6 = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep6Module() { + function createStep6Executor(deps = {}) { + const { + addLog, + completeStepFromBackground, + getLoginAuthStateLabel, + getState, + isStep6RecoverableResult, + isStep6SuccessResult, + refreshOAuthUrlBeforeStep6, + reuseOrCreateTab, + runPreStep6CookieCleanup, + sendToContentScriptResilient, + shouldSkipLoginVerificationForCpaCallback, + skipLoginVerificationStepsForCpaCallback, + throwIfStopped, + } = deps; + + async function executeStep6(state) { + if (shouldSkipLoginVerificationForCpaCallback(state)) { + await skipLoginVerificationStepsForCpaCallback(); + return; + } + if (!state.email) { + throw new Error('缺少邮箱地址,请先完成步骤 3。'); + } + + await runPreStep6CookieCleanup(); + + let attempt = 0; + + while (true) { + throwIfStopped(); + attempt += 1; + const currentState = attempt === 1 ? state : await getState(); + const password = currentState.password || currentState.customPassword || ''; + const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState); + + if (attempt === 1) { + await addLog('步骤 6:正在打开最新 OAuth 链接并登录...'); + } else { + await addLog(`步骤 6:上一轮登录未进入验证码页,正在重新发起第 ${attempt} 轮登录尝试...`, 'warn'); + } + + await reuseOrCreateTab('signup-page', oauthUrl); + + const result = await sendToContentScriptResilient( + 'signup-page', + { + type: 'EXECUTE_STEP', + step: 6, + source: 'background', + payload: { + email: currentState.email, + password, + }, + }, + { + timeoutMs: 180000, + retryDelayMs: 700, + logMessage: '步骤 6:认证页正在切换,等待页面重新就绪后继续登录...', + } + ); + + if (result?.error) { + throw new Error(result.error); + } + + if (isStep6SuccessResult(result)) { + await completeStepFromBackground(6, { + loginVerificationRequestedAt: result.loginVerificationRequestedAt || null, + }); + return; + } + + if (isStep6RecoverableResult(result)) { + const reasonMessage = result.message + || `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 6。`; + await addLog(`步骤 6:${reasonMessage}`, 'warn'); + continue; + } + + throw new Error('步骤 6:认证页未返回可识别的登录结果。'); + } + } + + return { executeStep6 }; + } + + return { createStep6Executor }; +}); diff --git a/background/steps/step7.js b/background/steps/step7.js new file mode 100644 index 0000000..10100db --- /dev/null +++ b/background/steps/step7.js @@ -0,0 +1,146 @@ +(function attachBackgroundStep7(root, factory) { + root.MultiPageBackgroundStep7 = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep7Module() { + function createStep7Executor(deps = {}) { + const { + addLog, + chrome, + CLOUDFLARE_TEMP_EMAIL_PROVIDER, + confirmCustomVerificationStepBypass, + ensureStep7VerificationPageReady, + executeStep6, + getMailConfig, + getState, + getTabId, + HOTMAIL_PROVIDER, + isTabAlive, + isVerificationMailPollingError, + LUCKMAIL_PROVIDER, + resolveVerificationStep, + reuseOrCreateTab, + setState, + setStepStatus, + shouldSkipLoginVerificationForCpaCallback, + shouldUseCustomRegistrationEmail, + sleepWithStop, + STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, + STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS, + throwIfStopped, + } = deps; + + async function runStep7Attempt(state) { + const mail = getMailConfig(state); + if (mail.error) throw new Error(mail.error); + const stepStartedAt = Date.now(); + const authTabId = await getTabId('signup-page'); + + if (authTabId) { + await chrome.tabs.update(authTabId, { active: true }); + } else { + if (!state.oauthUrl) { + throw new Error('缺少登录用 OAuth 链接,请先完成步骤 6。'); + } + await reuseOrCreateTab('signup-page', state.oauthUrl); + } + + throwIfStopped(); + await ensureStep7VerificationPageReady(); + await addLog('步骤 7:登录验证码页面已就绪,开始获取验证码。', 'info'); + + if (shouldUseCustomRegistrationEmail(state)) { + await confirmCustomVerificationStepBypass(7); + return; + } + + throwIfStopped(); + if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { + await addLog(`步骤 7:正在通过 ${mail.label} 轮询验证码...`); + } else { + await addLog(`步骤 7:正在打开${mail.label}...`); + + const alive = await isTabAlive(mail.source); + if (alive) { + if (mail.navigateOnReuse) { + await reuseOrCreateTab(mail.source, mail.url, { + inject: mail.inject, + injectSource: mail.injectSource, + }); + } else { + const tabId = await getTabId(mail.source); + await chrome.tabs.update(tabId, { active: true }); + } + } else { + await reuseOrCreateTab(mail.source, mail.url, { + inject: mail.inject, + injectSource: mail.injectSource, + }); + } + } + + await resolveVerificationStep(7, state, mail, { + filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : Math.max(0, stepStartedAt - 60000), + requestFreshCodeFirst: false, + resendIntervalMs: mail.provider === HOTMAIL_PROVIDER ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, + }); + } + + async function rerunStep6ForStep7Recovery() { + const currentState = await getState(); + await addLog('步骤 7:正在回到步骤 6,重新发起登录验证码流程...', 'warn'); + await executeStep6(currentState); + await sleepWithStop(3000); + } + + async function executeStep7(state) { + if (shouldSkipLoginVerificationForCpaCallback(state)) { + await setState({ + lastLoginCode: null, + loginVerificationRequestedAt: null, + }); + await setStepStatus(7, 'skipped'); + await addLog('步骤 7:当前已选择“第六步回调”,本轮无需获取登录验证码。', 'warn'); + return; + } + + let currentState = state; + let mailPollingAttempt = 1; + let lastMailPollingError = null; + + while (true) { + try { + await runStep7Attempt(currentState); + return; + } catch (err) { + if (!isVerificationMailPollingError(err)) { + throw err; + } + + lastMailPollingError = err; + if (mailPollingAttempt >= STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS) { + break; + } + + mailPollingAttempt += 1; + await addLog( + `步骤 7:检测到邮箱轮询类失败,准备从步骤 6 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS})...`, + 'warn' + ); + await rerunStep6ForStep7Recovery(); + currentState = await getState(); + } + } + + if (lastMailPollingError) { + throw new Error( + `步骤 7:登录验证码流程在 ${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS} 轮邮箱轮询恢复后仍未成功。最后一次原因:${lastMailPollingError.message}` + ); + } + + throw new Error('步骤 7:登录验证码流程未成功完成。'); + } + + return { executeStep7 }; + } + + return { createStep7Executor }; +}); diff --git a/background/steps/step8.js b/background/steps/step8.js new file mode 100644 index 0000000..cd05537 --- /dev/null +++ b/background/steps/step8.js @@ -0,0 +1,174 @@ +(function attachBackgroundStep8(root, factory) { + root.MultiPageBackgroundStep8 = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() { + function createStep8Executor(deps = {}) { + const { + addLog, + chrome, + cleanupStep8NavigationListeners, + clickWithDebugger, + completeStepFromBackground, + ensureStep8SignupPageReady, + getStep8CallbackUrlFromNavigation, + getStep8CallbackUrlFromTabUpdate, + getStep8EffectLabel, + getTabId, + isTabAlive, + prepareStep8DebuggerClick, + reloadStep8ConsentPage, + reuseOrCreateTab, + sleepWithStop, + STEP8_CLICK_RETRY_DELAY_MS, + STEP8_MAX_ROUNDS, + STEP8_READY_WAIT_TIMEOUT_MS, + STEP8_STRATEGIES, + throwIfStep8SettledOrStopped, + triggerStep8ContentStrategy, + waitForStep8ClickEffect, + waitForStep8Ready, + setWebNavListener, + setWebNavCommittedListener, + setStep8PendingReject, + setStep8TabUpdatedListener, + } = deps; + + async function executeStep8(state) { + if (!state.oauthUrl) { + throw new Error('缺少登录用 OAuth 链接,请先完成步骤 6。'); + } + + await addLog('步骤 8:正在监听 localhost 回调地址...'); + + return new Promise((resolve, reject) => { + let resolved = false; + let signupTabId = null; + + const cleanupListener = () => { + cleanupStep8NavigationListeners(); + setStep8PendingReject(null); + }; + + const rejectStep8 = (error) => { + if (resolved) return; + resolved = true; + clearTimeout(timeout); + cleanupListener(); + reject(error); + }; + + const finalizeStep8Callback = (callbackUrl) => { + if (resolved || !callbackUrl) return; + + resolved = true; + cleanupListener(); + clearTimeout(timeout); + + addLog(`步骤 8:已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => { + return completeStepFromBackground(8, { localhostUrl: callbackUrl }); + }).then(() => { + resolve(); + }).catch((err) => { + reject(err); + }); + }; + + const timeout = setTimeout(() => { + rejectStep8(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 8 的点击可能被拦截了。')); + }, 120000); + + setStep8PendingReject((error) => { + rejectStep8(error); + }); + + setWebNavListener((details) => { + const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId); + finalizeStep8Callback(callbackUrl); + }); + + setWebNavCommittedListener((details) => { + const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId); + finalizeStep8Callback(callbackUrl); + }); + + setStep8TabUpdatedListener((tabId, changeInfo, tab) => { + const callbackUrl = getStep8CallbackUrlFromTabUpdate(tabId, changeInfo, tab, signupTabId); + finalizeStep8Callback(callbackUrl); + }); + + (async () => { + try { + throwIfStep8SettledOrStopped(resolved); + signupTabId = await getTabId('signup-page'); + throwIfStep8SettledOrStopped(resolved); + + if (signupTabId && await isTabAlive('signup-page')) { + await chrome.tabs.update(signupTabId, { active: true }); + await addLog('步骤 8:已切回认证页,正在准备调试器点击...'); + } else { + signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl); + await addLog('步骤 8:已重新打开认证页,正在准备调试器点击...'); + } + + throwIfStep8SettledOrStopped(resolved); + chrome.webNavigation.onBeforeNavigate.addListener(deps.getWebNavListener()); + chrome.webNavigation.onCommitted.addListener(deps.getWebNavCommittedListener()); + chrome.tabs.onUpdated.addListener(deps.getStep8TabUpdatedListener()); + await ensureStep8SignupPageReady(signupTabId, { + timeoutMs: 15000, + logMessage: '步骤 8:认证页内容脚本尚未就绪,正在等待页面恢复...', + }); + + for (let round = 1; round <= STEP8_MAX_ROUNDS && !resolved; round++) { + throwIfStep8SettledOrStopped(resolved); + const pageState = await waitForStep8Ready(signupTabId, STEP8_READY_WAIT_TIMEOUT_MS); + if (!pageState?.consentReady) { + await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS); + continue; + } + + const strategy = STEP8_STRATEGIES[Math.min(round - 1, STEP8_STRATEGIES.length - 1)]; + + await addLog(`步骤 8:第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label})...`); + + if (strategy.mode === 'debugger') { + const clickTarget = await prepareStep8DebuggerClick(signupTabId); + throwIfStep8SettledOrStopped(resolved); + await clickWithDebugger(signupTabId, clickTarget?.rect); + } else { + await triggerStep8ContentStrategy(signupTabId, strategy.strategy); + } + + if (resolved) { + return; + } + + const effect = await waitForStep8ClickEffect(signupTabId, pageState.url); + if (resolved) { + return; + } + + if (effect.progressed) { + await addLog(`步骤 8:检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info'); + break; + } + + if (round >= STEP8_MAX_ROUNDS) { + throw new Error(`步骤 8:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`); + } + + await addLog(`步骤 8:${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS})...`, 'warn'); + await reloadStep8ConsentPage(signupTabId); + await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS); + } + } catch (err) { + rejectStep8(err); + } + })(); + }); + } + + return { executeStep8 }; + } + + return { createStep8Executor }; +}); diff --git a/background/steps/step9.js b/background/steps/step9.js new file mode 100644 index 0000000..7e46b05 --- /dev/null +++ b/background/steps/step9.js @@ -0,0 +1,167 @@ +(function attachBackgroundStep9(root, factory) { + root.MultiPageBackgroundStep9 = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep9Module() { + function createStep9Executor(deps = {}) { + const { + addLog, + chrome, + closeConflictingTabsForSource, + completeStepFromBackground, + ensureContentScriptReadyOnTab, + getPanelMode, + getTabId, + isLocalhostOAuthCallbackUrl, + isTabAlive, + normalizeSub2ApiUrl, + rememberSourceLastUrl, + reuseOrCreateTab, + sendToContentScript, + sendToContentScriptResilient, + shouldBypassStep9ForLocalCpa, + SUB2API_STEP9_RESPONSE_TIMEOUT_MS, + } = deps; + + async function executeStep9(state) { + if (getPanelMode(state) === 'sub2api') { + return executeSub2ApiStep9(state); + } + return executeCpaStep9(state); + } + + async function executeCpaStep9(state) { + if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { + throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。'); + } + if (!state.localhostUrl) { + throw new Error('缺少 localhost 回调地址,请先完成步骤 8。'); + } + if (!state.vpsUrl) { + throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。'); + } + + if (shouldBypassStep9ForLocalCpa(state)) { + await addLog('步骤 9:检测到本地 CPA,且当前策略为“跳过第9步”,本轮不再重复提交回调地址。', 'info'); + await completeStepFromBackground(9, { + localhostUrl: state.localhostUrl, + verifiedStatus: 'local-auto', + }); + return; + } + + await addLog('步骤 9:正在打开 CPA 面板...'); + + const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js']; + let tabId = await getTabId('vps-panel'); + const alive = tabId && await isTabAlive('vps-panel'); + + if (!alive) { + tabId = await reuseOrCreateTab('vps-panel', state.vpsUrl, { + inject: injectFiles, + reloadIfSameUrl: true, + }); + } else { + await closeConflictingTabsForSource('vps-panel', state.vpsUrl, { excludeTabIds: [tabId] }); + await chrome.tabs.update(tabId, { active: true }); + await rememberSourceLastUrl('vps-panel', state.vpsUrl); + } + + await ensureContentScriptReadyOnTab('vps-panel', tabId, { + inject: injectFiles, + timeoutMs: 45000, + retryDelayMs: 900, + logMessage: '步骤 9:CPA 面板仍在加载,正在重试连接...', + }); + + await addLog('步骤 9:正在填写回调地址...'); + const result = await sendToContentScriptResilient('vps-panel', { + type: 'EXECUTE_STEP', + step: 9, + source: 'background', + payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword }, + }, { + timeoutMs: 30000, + retryDelayMs: 700, + logMessage: '步骤 9:CPA 面板通信未就绪,正在等待页面恢复...', + }); + + if (result?.error) { + throw new Error(result.error); + } + } + + async function executeSub2ApiStep9(state) { + if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { + throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。'); + } + if (!state.localhostUrl) { + throw new Error('缺少 localhost 回调地址,请先完成步骤 8。'); + } + if (!state.sub2apiSessionId) { + throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。'); + } + if (!state.sub2apiEmail) { + throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。'); + } + if (!state.sub2apiPassword) { + throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。'); + } + + const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl); + const injectFiles = ['content/utils.js', 'content/sub2api-panel.js']; + + await addLog('步骤 9:正在打开 SUB2API 后台...'); + + let tabId = await getTabId('sub2api-panel'); + const alive = tabId && await isTabAlive('sub2api-panel'); + + if (!alive) { + tabId = await reuseOrCreateTab('sub2api-panel', sub2apiUrl, { + inject: injectFiles, + injectSource: 'sub2api-panel', + reloadIfSameUrl: true, + }); + } else { + await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl, { excludeTabIds: [tabId] }); + await chrome.tabs.update(tabId, { active: true }); + await rememberSourceLastUrl('sub2api-panel', sub2apiUrl); + } + + await ensureContentScriptReadyOnTab('sub2api-panel', tabId, { + inject: injectFiles, + injectSource: 'sub2api-panel', + }); + + await addLog('步骤 9:正在向 SUB2API 提交回调并创建账号...'); + const result = await sendToContentScript('sub2api-panel', { + type: 'EXECUTE_STEP', + step: 9, + source: 'background', + payload: { + localhostUrl: state.localhostUrl, + sub2apiUrl, + sub2apiEmail: state.sub2apiEmail, + sub2apiPassword: state.sub2apiPassword, + sub2apiGroupName: state.sub2apiGroupName, + sub2apiSessionId: state.sub2apiSessionId, + sub2apiOAuthState: state.sub2apiOAuthState, + sub2apiGroupId: state.sub2apiGroupId, + sub2apiDraftName: state.sub2apiDraftName, + }, + }, { + responseTimeoutMs: SUB2API_STEP9_RESPONSE_TIMEOUT_MS, + }); + + if (result?.error) { + throw new Error(result.error); + } + } + + return { + executeCpaStep9, + executeStep9, + executeSub2ApiStep9, + }; + } + + return { createStep9Executor }; +}); diff --git a/tests/background-step-modules.test.js b/tests/background-step-modules.test.js new file mode 100644 index 0000000..16da9f9 --- /dev/null +++ b/tests/background-step-modules.test.js @@ -0,0 +1,21 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +test('background imports step 1~9 modules', () => { + const source = fs.readFileSync('background.js', 'utf8'); + + [ + 'background/steps/step1.js', + 'background/steps/step2.js', + 'background/steps/step3.js', + 'background/steps/step4.js', + 'background/steps/step5.js', + 'background/steps/step6.js', + 'background/steps/step7.js', + 'background/steps/step8.js', + 'background/steps/step9.js', + ].forEach((path) => { + assert.match(source, new RegExp(path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + }); +}); diff --git a/tests/step8-stop-cleanup.test.js b/tests/step8-stop-cleanup.test.js index 7931295..eba5a7f 100644 --- a/tests/step8-stop-cleanup.test.js +++ b/tests/step8-stop-cleanup.test.js @@ -1,9 +1,10 @@ const assert = require('assert'); const fs = require('fs'); -const source = fs.readFileSync('background.js', 'utf8'); +const helperSource = fs.readFileSync('background.js', 'utf8'); +const step8ModuleSource = fs.readFileSync('background/steps/step8.js', 'utf8'); -function extractFunction(name) { +function extractFunction(source, name) { const markers = [`async function ${name}(`, `function ${name}(`]; const start = markers .map(marker => source.indexOf(marker)) @@ -50,16 +51,16 @@ function extractFunction(name) { return source.slice(start, end); } -const bundle = [ - extractFunction('throwIfStopped'), - extractFunction('cleanupStep8NavigationListeners'), - extractFunction('rejectPendingStep8'), - extractFunction('throwIfStep8SettledOrStopped'), - extractFunction('requestStop'), - extractFunction('executeStep8'), +const helperBundle = [ + extractFunction(helperSource, 'throwIfStopped'), + extractFunction(helperSource, 'cleanupStep8NavigationListeners'), + extractFunction(helperSource, 'rejectPendingStep8'), + extractFunction(helperSource, 'throwIfStep8SettledOrStopped'), + extractFunction(helperSource, 'requestStop'), ].join('\n'); -const api = new Function(` +const api = new Function('step8ModuleSource', ` +const self = {}; let stopRequested = false; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; let webNavListener = null; @@ -71,6 +72,13 @@ let autoRunCurrentRun = 2; let autoRunTotalRuns = 3; let autoRunAttemptRun = 4; const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start'; +const STEP8_CLICK_RETRY_DELAY_MS = 500; +const STEP8_MAX_ROUNDS = 5; +const STEP8_READY_WAIT_TIMEOUT_MS = 30000; +const STEP8_STRATEGIES = [ + { mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' }, + { mode: 'debugger', label: 'debugger click' }, +]; const added = { beforeNavigate: 0, @@ -89,28 +97,28 @@ let resolveTabId = null; const chrome = { webNavigation: { onBeforeNavigate: { - addListener(listener) { + addListener() { added.beforeNavigate += 1; }, - removeListener(listener) { + removeListener() { removed.beforeNavigate += 1; }, }, onCommitted: { - addListener(listener) { + addListener() { added.committed += 1; }, - removeListener(listener) { + removeListener() { removed.committed += 1; }, }, }, tabs: { onUpdated: { - addListener(listener) { + addListener() { added.tabUpdated += 1; }, - removeListener(listener) { + removeListener() { removed.tabUpdated += 1; }, }, @@ -137,6 +145,7 @@ function isAutoRunScheduledState() { } function getStep8CallbackUrlFromNavigation() { return ''; } function getStep8CallbackUrlFromTabUpdate() { return ''; } +function getStep8EffectLabel() { return 'no_effect'; } async function completeStepFromBackground() {} async function getTabId() { return await new Promise((resolve) => { @@ -149,18 +158,86 @@ async function reuseOrCreateTab() { async function isTabAlive() { return true; } -async function sendToContentScript(source, message) { - sentMessages.push({ source, type: message.type }); +async function ensureStep8SignupPageReady() {} +async function prepareStep8DebuggerClick() { + sentMessages.push({ source: 'signup-page', type: 'STEP8_FIND_AND_CLICK' }); return { rect: { centerX: 10, centerY: 20 } }; } +async function triggerStep8ContentStrategy() { + sentMessages.push({ source: 'signup-page', type: 'STEP8_TRIGGER_CONTINUE' }); +} +async function waitForStep8ClickEffect() { + return { progressed: false, reason: 'no_effect' }; +} +async function waitForStep8Ready() { + return { consentReady: true, url: 'https://example.com/consent' }; +} +async function reloadStep8ConsentPage() {} +async function sleepWithStop() {} async function clickWithDebugger() { clickCount += 1; } -${bundle} +function setWebNavListener(listener) { + webNavListener = listener; +} +function getWebNavListener() { + return webNavListener; +} +function setWebNavCommittedListener(listener) { + webNavCommittedListener = listener; +} +function getWebNavCommittedListener() { + return webNavCommittedListener; +} +function setStep8TabUpdatedListener(listener) { + step8TabUpdatedListener = listener; +} +function getStep8TabUpdatedListener() { + return step8TabUpdatedListener; +} +function setStep8PendingReject(handler) { + step8PendingReject = handler; +} + +${helperBundle} +${step8ModuleSource} + +const executor = self.MultiPageBackgroundStep8.createStep8Executor({ + addLog, + chrome, + cleanupStep8NavigationListeners, + clickWithDebugger, + completeStepFromBackground, + ensureStep8SignupPageReady, + getStep8CallbackUrlFromNavigation, + getStep8CallbackUrlFromTabUpdate, + getStep8EffectLabel, + getTabId, + getWebNavCommittedListener, + getWebNavListener, + getStep8TabUpdatedListener, + isTabAlive, + prepareStep8DebuggerClick, + reloadStep8ConsentPage, + reuseOrCreateTab, + setStep8PendingReject, + setStep8TabUpdatedListener, + setWebNavCommittedListener, + setWebNavListener, + sleepWithStop, + STEP8_CLICK_RETRY_DELAY_MS, + STEP8_MAX_ROUNDS, + STEP8_READY_WAIT_TIMEOUT_MS, + STEP8_STRATEGIES, + throwIfStep8SettledOrStopped, + triggerStep8ContentStrategy, + waitForStep8ClickEffect, + waitForStep8Ready, +}); return { - executeStep8, + executeStep8: executor.executeStep8, requestStop, resolveTabId(tabId) { if (!resolveTabId) { @@ -183,7 +260,7 @@ return { }; }, }; -`)(); +`)(step8ModuleSource); (async () => { const step8Promise = api.executeStep8({ oauthUrl: 'https://example.com/oauth' }); @@ -205,7 +282,7 @@ return { { beforeNavigate: 0, committed: 0, tabUpdated: 0 }, 'Stop 先发生时,不应再注册 Step 8 监听' ); - assert.strictEqual(state.sentMessages.length, 0, 'Stop 后不应再发送 STEP8_FIND_AND_CLICK 命令'); + assert.strictEqual(state.sentMessages.length, 0, 'Stop 后不应再发送 Step 8 执行动作'); assert.strictEqual(state.clickCount, 0, 'Stop 后不应再触发 debugger 点击'); assert.strictEqual(state.webNavListener, null, 'Stop 后 onBeforeNavigate 引用应为空'); assert.strictEqual(state.webNavCommittedListener, null, 'Stop 后 onCommitted 引用应为空');