Procházet zdrojové kódy

Update signup checkout automation flow

chendeben před 2 měsíci
rodič
revize
ce1b6660a2
36 změnil soubory, kde provedl 4467 přidání a 139 odebrání
  1. 290 45
      background.js
  2. 201 0
      background/checkout-api-utils.js
  3. 414 0
      background/cpa-api.js
  4. 1 1
      background/generated-email-helpers.js
  5. 2 0
      background/logging-status.js
  6. 37 19
      background/message-router.js
  7. 8 0
      background/navigation-utils.js
  8. 48 5
      background/signup-flow-helpers.js
  9. 32 0
      background/steps/fill-paypal-login.js
  10. 123 0
      background/steps/fill-paypal-payment.js
  11. 451 0
      background/steps/fill-stripe-checkout.js
  12. 248 0
      background/steps/get-plus-link.js
  13. 238 0
      background/steps/sync-cpa-session.js
  14. 47 0
      background/tab-runtime.js
  15. 2 3
      content/activation-utils.js
  16. 513 0
      content/checkout-paypal.js
  17. 1130 0
      content/checkout-stripe.js
  18. 57 0
      content/signup-page.js
  19. 2 0
      content/utils.js
  20. 5 5
      data/step-definitions.js
  21. 24 0
      manifest.json
  22. 8 0
      sidepanel/sidepanel.html
  23. 45 6
      sidepanel/sidepanel.js
  24. 4 4
      tests/activation-utils.test.js
  25. 9 9
      tests/auto-run-step4-restart.test.js
  26. 26 34
      tests/auto-run-step6-restart.test.js
  27. 129 0
      tests/background-cpa-api.test.js
  28. 1 1
      tests/background-generated-email-module.test.js
  29. 18 0
      tests/background-message-router-step2-skip.test.js
  30. 2 0
      tests/background-step-modules.test.js
  31. 119 0
      tests/background-step10-cpa-sync.test.js
  32. 105 0
      tests/background-step11-payurl.test.js
  33. 1 1
      tests/background-step4-a4sky-no-prefetch.test.js
  34. 61 0
      tests/background-step9-paypal-sms.test.js
  35. 56 0
      tests/checkout-api-utils.test.js
  36. 10 6
      tests/step-definitions-module.test.js

+ 290 - 45
background.js

@@ -24,6 +24,13 @@ importScripts(
   'background/steps/fetch-login-code.js',
   'background/steps/confirm-oauth.js',
   'background/steps/platform-verify.js',
+  'background/steps/get-plus-link.js',
+  'background/steps/fill-stripe-checkout.js',
+  'background/steps/fill-paypal-login.js',
+  'background/steps/fill-paypal-payment.js',
+  'background/cpa-api.js',
+  'background/steps/sync-cpa-session.js',
+  'background/checkout-api-utils.js',
   'data/names.js',
   'hotmail-utils.js',
   'microsoft-email.js',
@@ -39,7 +46,7 @@ const STEP_IDS = SHARED_STEP_DEFINITIONS
   .filter(Number.isFinite)
   .sort((left, right) => left - right);
 const LAST_STEP_ID = STEP_IDS[STEP_IDS.length - 1] || 10;
-const FINAL_OAUTH_CHAIN_START_STEP = 7;
+const FINAL_OAUTH_CHAIN_START_STEP = null;
 
 const {
   extractVerificationCodeFromMessage,
@@ -171,6 +178,8 @@ const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
 const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
 const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373';
 const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = DEFAULT_HOTMAIL_LOCAL_BASE_URL;
+const DEFAULT_PAYPAL_PHONE = '+15822201173';
+const DEFAULT_PAYPAL_SMS_API_URL = 'http://a.62-us.com/api/get_sms?key=a5d3262e05efaba982aba7cfae20b8bc';
 const HOTMAIL_LOCAL_HELPER_TIMEOUT_MS = 45000;
 const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
 const DISPLAY_TIMEZONE = 'Asia/Shanghai';
@@ -229,6 +238,8 @@ const PERSISTED_SETTING_DEFAULTS = {
   autoRunDelayEnabled: false,
   autoRunDelayMinutes: 30,
   autoStepDelaySeconds: null,
+  paypalPhone: DEFAULT_PAYPAL_PHONE,
+  paypalSmsApiUrl: DEFAULT_PAYPAL_SMS_API_URL,
   verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
   mailProvider: '163',
   mail2925Mode: DEFAULT_MAIL_2925_MODE,
@@ -391,6 +402,24 @@ function normalizeAutoStepDelaySeconds(value, fallback = null) {
   );
 }
 
+function normalizePaypalPhone(value, fallback = DEFAULT_PAYPAL_PHONE) {
+  const normalized = String(value || '').trim();
+  return normalized || fallback;
+}
+
+function normalizePaypalSmsApiUrl(value, fallback = DEFAULT_PAYPAL_SMS_API_URL) {
+  const normalized = String(value || '').trim();
+  if (!normalized) {
+    return fallback;
+  }
+  try {
+    const parsed = new URL(normalized);
+    return /^https?:$/i.test(parsed.protocol) ? parsed.toString() : fallback;
+  } catch {
+    return normalized;
+  }
+}
+
 function normalizeVerificationResendCount(value, fallback) {
   const rawValue = String(value ?? '').trim();
   if (!rawValue) {
@@ -872,6 +901,10 @@ function normalizePersistentSettingValue(key, value) {
       return normalizeAutoRunDelayMinutes(value);
     case 'autoStepDelaySeconds':
       return normalizeAutoStepDelaySeconds(value, PERSISTED_SETTING_DEFAULTS.autoStepDelaySeconds);
+    case 'paypalPhone':
+      return normalizePaypalPhone(value);
+    case 'paypalSmsApiUrl':
+      return normalizePaypalSmsApiUrl(value);
     case 'verificationResendCount':
       return normalizeVerificationResendCount(value, DEFAULT_VERIFICATION_RESEND_COUNT);
     case 'mailProvider':
@@ -3653,6 +3686,16 @@ function isSignupEmailVerificationPageUrl(rawUrl) {
     && /\/email-verification(?:[/?#]|$)/i.test(parsed.pathname || '');
 }
 
+function isSignupProfilePageUrl(rawUrl) {
+  if (typeof navigationUtils !== 'undefined' && navigationUtils?.isSignupProfilePageUrl) {
+    return navigationUtils.isSignupProfilePageUrl(rawUrl);
+  }
+  const parsed = parseUrlSafely(rawUrl);
+  if (!parsed) return false;
+  return isSignupPageHost(parsed.hostname)
+    && /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile|about-you)(?:[/?#]|$)/i.test(parsed.pathname || '');
+}
+
 function is163MailHost(hostname = '') {
   if (typeof navigationUtils !== 'undefined' && navigationUtils?.is163MailHost) {
     return navigationUtils.is163MailHost(hostname);
@@ -3770,6 +3813,10 @@ async function waitForTabComplete(tabId, options = {}) {
   return tabRuntime.waitForTabComplete(tabId, options);
 }
 
+async function waitForTabStableComplete(tabId, options = {}) {
+  return tabRuntime.waitForTabStableComplete(tabId, options);
+}
+
 async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
   return tabRuntime.ensureContentScriptReadyOnTab(source, tabId, options);
 }
@@ -4024,6 +4071,16 @@ function getFirstUnfinishedStep(statuses = {}) {
   return null;
 }
 
+function getNextActiveStep(step) {
+  const normalizedStep = Number(step);
+  for (const candidate of STEP_IDS) {
+    if (candidate > normalizedStep) {
+      return candidate;
+    }
+  }
+  return null;
+}
+
 function hasSavedProgress(statuses = {}) {
   if (typeof loggingStatus !== 'undefined' && loggingStatus?.hasSavedProgress) {
     return loggingStatus.hasSavedProgress(statuses);
@@ -4095,7 +4152,10 @@ async function invalidateDownstreamAfterStepRestart(step, options = {}) {
   const statuses = { ...(state.stepStatuses || {}) };
   const changedSteps = [];
 
-  for (let downstream = step + 1; downstream <= LAST_STEP_ID; downstream++) {
+  for (const downstream of STEP_IDS) {
+    if (downstream <= step) {
+      continue;
+    }
     if (statuses[downstream] !== 'pending') {
       statuses[downstream] = 'pending';
       changedSteps.push(downstream);
@@ -4581,10 +4641,12 @@ async function skipStep(step) {
     throw new Error(`步骤 ${step} 已完成,无需再跳过。`);
   }
 
-  if (step > 1) {
-    const prevStatus = statuses[step - 1];
+  const currentStepIndex = STEP_IDS.indexOf(step);
+  if (currentStepIndex > 0) {
+    const prevStep = STEP_IDS[currentStepIndex - 1];
+    const prevStatus = statuses[prevStep];
     if (!isStepDoneStatus(prevStatus)) {
-      throw new Error(`请先完成步骤 ${step - 1},再跳过步骤 ${step}。`);
+      throw new Error(`请先完成步骤 ${prevStep},再跳过步骤 ${step}。`);
     }
   }
 
@@ -4767,34 +4829,13 @@ async function handleStepData(step, payload) {
         await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
       }
       break;
-    case 7:
-      if (payload.loginVerificationRequestedAt) {
-        await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
-      }
-      break;
     case 4:
       await setState({
         lastEmailTimestamp: payload.emailTimestamp || null,
         signupVerificationRequestedAt: null,
       });
       break;
-    case 8:
-      await setState({
-        lastEmailTimestamp: payload.emailTimestamp || null,
-        loginVerificationRequestedAt: null,
-      });
-      break;
     case 9:
-      if (payload.localhostUrl) {
-        if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) {
-          throw new Error('步骤 9 返回了无效的 localhost OAuth 回调地址。');
-        }
-        await setState({
-          localhostUrl: payload.localhostUrl,
-          oauthFlowDeadlineAt: null,
-        });
-        broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
-      }
       break;
     case 10: {
       if (payload.localhostUrl) {
@@ -4841,8 +4882,8 @@ async function handleStepData(step, payload) {
 const stepWaiters = new Map();
 let resumeWaiter = null;
 const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000;
-const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 7, 8, 9]);
-const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 10]);
+const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 10]);
+const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 7, 8, 9]);
 
 function waitForStepComplete(step, timeoutMs = 120000) {
   return new Promise((resolve, reject) => {
@@ -5122,15 +5163,6 @@ async function executeStep(step, options = {}) {
 async function executeStepAndWait(step, delayAfter = 2000) {
   throwIfStopped();
 
-  const delaySeconds = normalizeAutoStepDelaySeconds((await getState()).autoStepDelaySeconds, null);
-  if (delaySeconds > 0) {
-    await addLog(
-      `自动运行:步骤 ${step} 执行前额外等待 ${delaySeconds} 秒,避免节奏过快。`,
-      'info'
-    );
-    await sleepWithStop(delaySeconds * 1000);
-  }
-
   if (AUTO_RUN_BACKGROUND_COMPLETED_STEPS.has(step)) {
     await addLog(`自动运行:步骤 ${step} 由后台流程负责收尾,执行函数返回后将直接进入下一步。`, 'info');
     await executeStep(step);
@@ -5244,9 +5276,10 @@ const AUTO_STEP_DELAYS = {
   4: 2000,
   5: 0,
   6: 3000,
-  7: 2000,
-  8: 2000,
-  9: 1000,
+  7: 3000,
+  8: 3000,
+  9: 2000,
+  10: 1000,
 };
 const accountRunHistoryHelpers = self.MultiPageBackgroundAccountRunHistory?.createAccountRunHistoryHelpers({
   ACCOUNT_RUN_HISTORY_STORAGE_KEY,
@@ -5614,12 +5647,11 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
   }
 
   let restartFromStep1WithCurrentEmail = false;
-  let step = Math.max(currentStartStep, 4);
-  while (step <= LAST_STEP_ID) {
+  let step = STEP_IDS.find((stepId) => stepId >= Math.max(currentStartStep, 4)) || null;
+  while (step !== null && step <= LAST_STEP_ID) {
     try {
       await executeStepAndWait(step, AUTO_STEP_DELAYS[step]);
-      const latestState = await getState();
-      step += 1;
+      step = getNextActiveStep(step);
     } catch (err) {
       if (isStopError(err)) {
         throw err;
@@ -5795,6 +5827,100 @@ async function resumeAutoRun() {
 
 const SIGNUP_ENTRY_URL = 'https://chatgpt.com/';
 const SIGNUP_PAGE_INJECT_FILES = ['content/utils.js', 'content/auth-page-recovery.js', 'content/signup-page.js'];
+const CHECKOUT_STRIPE_SOURCE = 'checkout-stripe';
+const CHECKOUT_STRIPE_INJECT_FILES = ['content/activation-utils.js', 'content/utils.js', 'content/checkout-stripe.js'];
+
+function isCheckoutStripeAutocompleteFrameUrl(url = '') {
+  return /elements-inner-autocompl|componentName=autocomplete/i.test(String(url || ''));
+}
+
+async function pingCheckoutStripeFrame(tabId, frameId) {
+  try {
+    const pong = await chrome.tabs.sendMessage(tabId, {
+      type: 'PING',
+      source: 'background',
+      payload: {},
+    }, {
+      frameId: Number.isInteger(frameId) ? frameId : 0,
+    });
+    return Boolean(pong?.ok && (!pong.source || pong.source === CHECKOUT_STRIPE_SOURCE));
+  } catch {
+    return false;
+  }
+}
+
+async function ensureCheckoutStripeFrameReady(tabId, frameId) {
+  if (await pingCheckoutStripeFrame(tabId, frameId)) {
+    return true;
+  }
+  if (!chrome?.scripting?.executeScript) {
+    return false;
+  }
+
+  try {
+    await chrome.scripting.executeScript({
+      target: { tabId, frameIds: [frameId] },
+      func: (injectedSource) => {
+        window.__MULTIPAGE_SOURCE = injectedSource;
+      },
+      args: [CHECKOUT_STRIPE_SOURCE],
+    });
+    await chrome.scripting.executeScript({
+      target: { tabId, frameIds: [frameId] },
+      files: CHECKOUT_STRIPE_INJECT_FILES,
+    });
+  } catch (error) {
+    console.warn(LOG_PREFIX, `Stripe autocomplete iframe 注入失败 frame=${frameId}: ${error?.message || error}`);
+  }
+
+  await sleepWithStop(300);
+  return pingCheckoutStripeFrame(tabId, frameId);
+}
+
+async function selectCheckoutStripeAutocompleteFrame(tabId, payload = {}) {
+  if (!chrome?.webNavigation?.getAllFrames) {
+    return { ok: false, error: '当前浏览器不支持枚举 checkout iframe。' };
+  }
+
+  const frames = await chrome.webNavigation.getAllFrames({ tabId }).catch(() => null);
+  const autocompleteFrames = (Array.isArray(frames) ? frames : [])
+    .filter((frame) => Number.isInteger(frame?.frameId) && isCheckoutStripeAutocompleteFrameUrl(frame.url));
+
+  if (!autocompleteFrames.length) {
+    return { ok: false, error: '未发现 Stripe/Google 地址 autocomplete iframe。' };
+  }
+
+  let lastError = '';
+  for (const frame of autocompleteFrames) {
+    const ready = await ensureCheckoutStripeFrameReady(tabId, frame.frameId);
+    if (!ready) {
+      lastError = `autocomplete iframe ${frame.frameId} 内容脚本未就绪`;
+      continue;
+    }
+
+    try {
+      const result = await chrome.tabs.sendMessage(tabId, {
+        type: 'CHECKOUT_STRIPE_SELECT_ADDRESS_SUGGESTION',
+        source: 'background',
+        payload,
+      }, {
+        frameId: frame.frameId,
+      });
+      if (result?.ok) {
+        return result;
+      }
+      lastError = result?.error || `autocomplete iframe ${frame.frameId} 未返回可用地址建议`;
+    } catch (error) {
+      lastError = error?.message || String(error || 'autocomplete iframe 选择失败');
+    }
+  }
+
+  return {
+    ok: false,
+    error: lastError || '未能在 autocomplete iframe 中选择 Google 地址建议。',
+  };
+}
+
 const panelBridge = self.MultiPageBackgroundPanelBridge?.createPanelBridge({
   chrome,
   addLog,
@@ -5820,6 +5946,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
   isGeneratedAliasProvider,
   isReusableGeneratedAliasEmail,
   isSignupEmailVerificationPageUrl,
+  isSignupProfilePageUrl,
   isHotmailProvider,
   isLuckmailProvider,
   isSignupPasswordPageUrl,
@@ -5829,6 +5956,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
   setEmailState,
   SIGNUP_ENTRY_URL,
   SIGNUP_PAGE_INJECT_FILES,
+  waitForTabStableComplete,
   waitForTabUrlMatch,
 });
 const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.createVerificationFlowHelpers({
@@ -5846,6 +5974,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create
   getState,
   getTabId,
   HOTMAIL_PROVIDER,
+  isRetryableContentScriptTransportError,
   isStopError,
   LUCKMAIL_PROVIDER,
   MAIL_2925_VERIFICATION_INTERVAL_MS,
@@ -5873,6 +6002,7 @@ const step2Executor = self.MultiPageBackgroundStep2?.createStep2Executor({
   chrome,
   completeStepFromBackground,
   ensureContentScriptReadyOnTab,
+  ensureSignupAuthEntryPageReady,
   ensureSignupEntryPageReady,
   ensureSignupPostEmailPageReadyInTab,
   getTabId,
@@ -5880,6 +6010,7 @@ const step2Executor = self.MultiPageBackgroundStep2?.createStep2Executor({
   resolveSignupEmailForFlow,
   sendToContentScriptResilient,
   SIGNUP_PAGE_INJECT_FILES,
+  waitForTabStableComplete,
 });
 const step3Executor = self.MultiPageBackgroundStep3?.createStep3Executor({
   addLog,
@@ -5911,6 +6042,7 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({
   shouldUseCustomRegistrationEmail,
   STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
   throwIfStopped,
+  waitForTabStableComplete,
 });
 const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({
   addLog,
@@ -5985,6 +6117,70 @@ const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({
   shouldBypassStep9ForLocalCpa,
   SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
 });
+const {
+  fetchPaypalSmsCode: fetchPaypalSmsCodeFromApi,
+  fetchRandomAddress: fetchCheckoutAddress,
+  generateCheckoutLink: generateCheckoutLink,
+} = self.MultiPageCheckoutApiUtils || {};
+
+async function fetchPaypalSmsCode(options = {}) {
+  if (typeof fetchPaypalSmsCodeFromApi !== 'function') {
+    throw new Error('PayPal 短信收码模块未加载。');
+  }
+  return fetchPaypalSmsCodeFromApi(options, {
+    addLog,
+    fetchImpl: (...args) => fetch(...args),
+    sleep: sleepWithStop,
+    throwIfStopped,
+  });
+}
+
+const step11Executor = self.MultiPageBackgroundStep11?.createStep11Executor({
+  addLog,
+  chrome,
+  completeStepFromBackground,
+  getState,
+  getTabId,
+  reuseOrCreateTab,
+  waitForTabStableComplete,
+});
+const step12Executor = self.MultiPageBackgroundStep12?.createStep12Executor({
+  addLog,
+  chrome,
+  completeStepFromBackground,
+  fetchCheckoutAddress,
+  getTabId,
+  sendToContentScriptResilient,
+});
+const step13Executor = self.MultiPageBackgroundStep13?.createStep13Executor({
+  addLog,
+  generateRandomEmail: () => generateRandomEmailForCheckout(),
+  sendToContentScriptResilient,
+});
+const step14Executor = self.MultiPageBackgroundStep14?.createStep14Executor({
+  addLog,
+  fetchCheckoutAddress,
+  generateRandomEmail: () => generateRandomEmailForCheckout(),
+  sendToContentScriptResilient,
+});
+const cpaSessionSyncExecutor = self.MultiPageBackgroundCpaSessionSync?.createCpaSessionSyncExecutor({
+  addLog,
+  chrome,
+  completeStepFromBackground,
+  createCpaApi: self.MultiPageBackgroundCpaApi?.createCpaApi,
+  fetchImpl: (...args) => fetch(...args),
+  getPanelMode,
+  getTabId,
+  sleepWithStop,
+  throwIfStopped,
+  waitForTabComplete,
+});
+function generateRandomEmailForCheckout() {
+  const c = 'abcdefghijklmnopqrstuvwxyz0123456789';
+  let e = '';
+  for (let i = 0; i < 16; i++) e += c[Math.floor(Math.random() * c.length)];
+  return e + '@gmail.com';
+}
 const stepDefinitions = SHARED_STEP_DEFINITIONS;
 const stepExecutorsByKey = {
   'open-chatgpt': () => step1Executor.executeStep1(),
@@ -5997,6 +6193,11 @@ const stepExecutorsByKey = {
   'fetch-login-code': (state) => step8Executor.executeStep8(state),
   'confirm-oauth': (state) => step9Executor.executeStep9(state),
   'platform-verify': (state) => step10Executor.executeStep10(state),
+  'get-plus-link': () => step11Executor.executeStep11(),
+  'fill-stripe-checkout': (state) => step12Executor.executeStep12(state),
+  'fill-paypal-login': (state) => step13Executor.executeStep13(state),
+  'fill-paypal-payment': (state) => step14Executor.executeStep14(state),
+  'sync-cpa-session': (state) => cpaSessionSyncExecutor.executeStep10(state),
 };
 const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({
   addLog,
@@ -6025,6 +6226,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
   executeStepViaCompletionSignal,
   exportSettingsBundle,
   fetchGeneratedEmail,
+  fetchPaypalSmsCode,
   finalizeStep3Completion: async () => {
     const currentState = await getState();
     const signupTabId = await getTabId('signup-page');
@@ -6065,6 +6267,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
   resumeAutoRun,
   scheduleAutoRun,
   selectLuckmailPurchase,
+  selectCheckoutStripeAutocompleteFrame,
   setCurrentHotmailAccount,
   setEmailState,
   setEmailStateSilently,
@@ -6111,6 +6314,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);
 }
@@ -6652,7 +6859,13 @@ function isAddPhoneAuthState(authState = {}) {
 async function getPostStep6AutoRestartDecision(step, error) {
   const normalizedStep = Number(step);
   const errorMessage = getErrorMessage(error);
-  if (!Number.isFinite(normalizedStep) || normalizedStep < 7 || normalizedStep > LAST_STEP_ID) {
+  if (
+    !Number.isFinite(normalizedStep)
+    || !Number.isFinite(Number(FINAL_OAUTH_CHAIN_START_STEP))
+    || normalizedStep < FINAL_OAUTH_CHAIN_START_STEP
+    || normalizedStep > LAST_STEP_ID
+    || !STEP_IDS.includes(FINAL_OAUTH_CHAIN_START_STEP)
+  ) {
     return {
       shouldRestart: false,
       blockedByAddPhone: false,
@@ -7159,6 +7372,38 @@ async function executeStep10(state) {
   return step10Executor.executeStep10(state);
 }
 
+// ============================================================
+// Step 6: 获取 Plus 订阅链接
+// ============================================================
+
+async function executeStep11() {
+  return step11Executor.executeStep11();
+}
+
+// ============================================================
+// Step 7: 填写 Stripe 结账表单
+// ============================================================
+
+async function executeStep12(state) {
+  return step12Executor.executeStep12(state);
+}
+
+// ============================================================
+// Step 8: 填写 PayPal 登录邮箱
+// ============================================================
+
+async function executeStep13(state) {
+  return step13Executor.executeStep13(state);
+}
+
+// ============================================================
+// Step 9: 填写 PayPal 付款信息
+// ============================================================
+
+async function executeStep14(state) {
+  return step14Executor.executeStep14(state);
+}
+
 // ============================================================
 // Open Side Panel on extension icon click
 // ============================================================

+ 201 - 0
background/checkout-api-utils.js

@@ -0,0 +1,201 @@
+// background/checkout-api-utils.js — API helpers for Stripe/PayPal checkout automation
+(function attachCheckoutApiUtils(root) {
+  root.MultiPageCheckoutApiUtils = (function createCheckoutApiUtilsModule() {
+
+    const ADDRESS_API_URL = 'https://www.meiguodizhi.com/api/v1/dz';
+    const CHATGPT_CHECKOUT_URL = 'https://chatgpt.com/backend-api/payments/checkout';
+    const DEFAULT_PAYPAL_SMS_MAX_ATTEMPTS = 18;
+    const DEFAULT_PAYPAL_SMS_INTERVAL_MS = 5000;
+
+    /**
+     * Fetch a random US address from meiguodizhi.com.
+     * @returns {Promise<{street: string, city: string, state: string, zip: string}>}
+     */
+    async function fetchRandomAddress() {
+      try {
+        const response = await fetch(ADDRESS_API_URL, {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({ path: '/', method: 'address' }),
+        });
+        const d = await response.json();
+        const a = d.address || d;
+        return {
+          street: a.Address || a.street || '123 Main St',
+          city: a.City || a.city || 'New York',
+          state: a.State_Full || a.State || a.state || 'New York',
+          zip: String(a.Zip_Code || a.zip || '10001').substring(0, 5),
+        };
+      } catch (e) {
+        console.error('[CheckoutAPI] 获取随机地址失败:', e.message);
+        return { street: '123 Main St', city: 'New York', state: 'New York', zip: '10001' };
+      }
+    }
+
+    /**
+     * Generate a ChatGPT Plus Stripe hosted checkout URL.
+     * @param {string} accessToken - ChatGPT session access token
+     * @returns {Promise<{hostedUrl: string, checkoutSessionId: string}|null>}
+     */
+    async function generateCheckoutLink(accessToken) {
+      try {
+        const payload = {
+          plan_name: 'chatgptplusplan',
+          billing_details: { country: 'US', currency: 'USD' },
+          cancel_url: 'https://chatgpt.com/#pricing',
+          promo_campaign: { promo_campaign_id: 'plus-1-month-free', is_coupon_from_query_param: false },
+          checkout_ui_mode: 'hosted',
+        };
+
+        const response = await fetch(CHATGPT_CHECKOUT_URL, {
+          method: 'POST',
+          headers: {
+            'Authorization': `Bearer ${accessToken}`,
+            'Content-Type': 'application/json',
+          },
+          body: JSON.stringify(payload),
+        });
+
+        const data = await response.json();
+
+        if (!response.ok) {
+          console.error('[CheckoutAPI] 请求失败 HTTP', response.status, data);
+          return null;
+        }
+
+        const hostedUrl = data?.url || data?.stripe_hosted_url || data?.checkout_url;
+        if (!hostedUrl) {
+          console.error('[CheckoutAPI] 未找到长链接', data);
+          return null;
+        }
+
+        return {
+          hostedUrl,
+          checkoutSessionId: data.checkout_session_id || '',
+        };
+      } catch (e) {
+        console.error('[CheckoutAPI] 生成 Plus 链接异常:', e.message);
+        return null;
+      }
+    }
+
+    function collectSmsTextValues(value, results = []) {
+      if (value === null || value === undefined) {
+        return results;
+      }
+      if (typeof value === 'string' || typeof value === 'number') {
+        results.push(String(value));
+        return results;
+      }
+      if (Array.isArray(value)) {
+        value.forEach((item) => collectSmsTextValues(item, results));
+        return results;
+      }
+      if (typeof value === 'object') {
+        const preferredKeys = [
+          'code',
+          'sms',
+          'message',
+          'msg',
+          'content',
+          'text',
+          'data',
+          'result',
+        ];
+        preferredKeys.forEach((key) => {
+          if (Object.prototype.hasOwnProperty.call(value, key)) {
+            collectSmsTextValues(value[key], results);
+          }
+        });
+        Object.keys(value)
+          .filter((key) => !preferredKeys.includes(key))
+          .forEach((key) => collectSmsTextValues(value[key], results));
+      }
+      return results;
+    }
+
+    function extractPaypalSmsCode(rawPayload, options = {}) {
+      const excluded = new Set((options.excludeCodes || []).map((code) => String(code || '').trim()).filter(Boolean));
+      const rawText = String(rawPayload || '').trim();
+      if (!rawText) return '';
+
+      let values = [rawText];
+      try {
+        values = collectSmsTextValues(JSON.parse(rawText));
+      } catch {
+        // Plain text API responses are expected.
+      }
+
+      const candidates = [];
+      values.forEach((value) => {
+        String(value || '').replace(/\b(\d{4,8})\b/g, (_, code) => {
+          if (!excluded.has(code)) {
+            candidates.push(code);
+          }
+          return _;
+        });
+      });
+
+      return candidates.find((code) => code.length === 6)
+        || candidates.find((code) => code.length >= 4 && code.length <= 8)
+        || '';
+    }
+
+    async function fetchPaypalSmsCode(options = {}, deps = {}) {
+      const fetchImpl = deps.fetchImpl || fetch;
+      const sleep = deps.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
+      const addLog = deps.addLog || (async () => {});
+      const throwIfStopped = deps.throwIfStopped || (() => {});
+      const smsApiUrl = String(options.smsApiUrl || '').trim();
+      if (!smsApiUrl) {
+        throw new Error('PayPal 短信收码 API 未配置。');
+      }
+
+      const maxAttempts = Math.max(1, Math.floor(Number(options.maxAttempts) || DEFAULT_PAYPAL_SMS_MAX_ATTEMPTS));
+      const intervalMs = Math.max(500, Math.floor(Number(options.intervalMs) || DEFAULT_PAYPAL_SMS_INTERVAL_MS));
+      let lastError = '';
+
+      for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
+        throwIfStopped();
+        try {
+          const response = await fetchImpl(smsApiUrl, {
+            method: 'GET',
+            cache: 'no-store',
+          });
+          const text = await response.text();
+          if (!response.ok) {
+            lastError = `HTTP ${response.status}: ${text.slice(0, 120)}`;
+          } else {
+            const code = extractPaypalSmsCode(text, {
+              excludeCodes: options.excludeCodes || [],
+            });
+            if (code) {
+              return {
+                code,
+                attempt,
+                raw: text,
+              };
+            }
+            lastError = text.slice(0, 160) || '接口未返回验证码';
+          }
+        } catch (error) {
+          lastError = error?.message || String(error || '请求失败');
+        }
+
+        if (attempt < maxAttempts) {
+          await addLog(`PayPal 短信验证码暂未获取到,${Math.round(intervalMs / 1000)} 秒后重试(${attempt}/${maxAttempts})。`, 'info');
+          await sleep(intervalMs);
+        }
+      }
+
+      throw new Error(`PayPal 短信验证码获取超时。最后响应:${lastError || '无响应'}`);
+    }
+
+    return {
+      extractPaypalSmsCode,
+      fetchRandomAddress,
+      fetchPaypalSmsCode,
+      generateCheckoutLink,
+    };
+  })();
+})(typeof self !== 'undefined' ? self : globalThis);

+ 414 - 0
background/cpa-api.js

@@ -0,0 +1,414 @@
+// background/cpa-api.js — CPA management API helpers for Codex auth JSON import
+(function attachBackgroundCpaApi(root, factory) {
+  root.MultiPageBackgroundCpaApi = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundCpaApiModule() {
+  function createCpaApi(deps = {}) {
+    const {
+      addLog = async () => {},
+      fetchImpl = (...args) => fetch(...args),
+    } = deps;
+
+    function normalizeString(value = '') {
+      return String(value || '').trim();
+    }
+
+    function isPlainObject(value) {
+      return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
+    }
+
+    function firstNonEmpty(...values) {
+      for (const value of values) {
+        const normalized = normalizeString(value);
+        if (normalized) return normalized;
+      }
+      return '';
+    }
+
+    function normalizeEmailValue(value = '') {
+      const email = normalizeString(value);
+      return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : '';
+    }
+
+    function deriveCpaManagementOrigin(vpsUrl) {
+      const normalizedUrl = normalizeString(vpsUrl);
+      if (!normalizedUrl) {
+        throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
+      }
+      try {
+        return new URL(normalizedUrl).origin;
+      } catch {
+        throw new Error('CPA 地址格式无效,请先在侧边栏检查。');
+      }
+    }
+
+    function getCpaApiErrorMessage(payload, responseStatus = 500) {
+      const candidates = [
+        payload?.error,
+        payload?.message,
+        payload?.detail,
+        payload?.reason,
+      ];
+      const message = candidates.map(normalizeString).find(Boolean);
+      return message || `CPA 管理接口请求失败(HTTP ${responseStatus})。`;
+    }
+
+    async function fetchCpaManagementJson(origin, path, options = {}) {
+      const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000));
+      const controller = new AbortController();
+      const timer = setTimeout(() => controller.abort(), timeoutMs);
+
+      try {
+        const managementKey = normalizeString(options.managementKey);
+        const headers = {
+          Accept: 'application/json',
+          'Content-Type': 'application/json',
+        };
+        if (managementKey) {
+          headers.Authorization = `Bearer ${managementKey}`;
+          headers['X-Management-Key'] = managementKey;
+        }
+
+        const response = await fetchImpl(`${origin}${path}`, {
+          method: options.method || 'POST',
+          headers,
+          body: options.body === undefined ? undefined : JSON.stringify(options.body),
+          signal: controller.signal,
+        });
+
+        let payload = {};
+        try {
+          payload = await response.json();
+        } catch {
+          payload = {};
+        }
+
+        if (!response.ok) {
+          throw new Error(getCpaApiErrorMessage(payload, response.status));
+        }
+
+        return payload;
+      } catch (error) {
+        if (error?.name === 'AbortError') {
+          throw new Error('CPA 管理接口请求超时,请稍后重试。');
+        }
+        throw error;
+      } finally {
+        clearTimeout(timer);
+      }
+    }
+
+    function decodeBase64UrlSegment(segment = '') {
+      const normalized = normalizeString(segment)
+        .replace(/-/g, '+')
+        .replace(/_/g, '/');
+      if (!normalized) return '';
+
+      const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
+      try {
+        if (typeof Buffer !== 'undefined') {
+          return Buffer.from(padded, 'base64').toString('utf8');
+        }
+        if (typeof atob === 'function') {
+          const binary = atob(padded);
+          const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
+          if (typeof TextDecoder !== 'undefined') {
+            return new TextDecoder().decode(bytes);
+          }
+          return binary;
+        }
+      } catch {
+        return '';
+      }
+      return '';
+    }
+
+    function encodeBase64UrlJson(value) {
+      const json = JSON.stringify(value);
+      if (typeof Buffer !== 'undefined') {
+        return Buffer.from(json, 'utf8')
+          .toString('base64')
+          .replace(/\+/g, '-')
+          .replace(/\//g, '_')
+          .replace(/=+$/g, '');
+      }
+
+      const bytes = new TextEncoder().encode(json);
+      let binary = '';
+      bytes.forEach((byte) => {
+        binary += String.fromCharCode(byte);
+      });
+      return btoa(binary)
+        .replace(/\+/g, '-')
+        .replace(/\//g, '_')
+        .replace(/=+$/g, '');
+    }
+
+    function parseJwtPayload(token = '') {
+      const normalized = normalizeString(token);
+      if (!normalized) return null;
+
+      const parts = normalized.split('.');
+      if (parts.length < 2) return null;
+
+      try {
+        return JSON.parse(decodeBase64UrlSegment(parts[1]));
+      } catch {
+        return null;
+      }
+    }
+
+    function getOpenAiAuthSection(payload) {
+      if (!isPlainObject(payload)) return {};
+      const auth = payload['https://api.openai.com/auth'];
+      return isPlainObject(auth) ? auth : {};
+    }
+
+    function getOpenAiProfileSection(payload) {
+      if (!isPlainObject(payload)) return {};
+      const profile = payload['https://api.openai.com/profile'];
+      return isPlainObject(profile) ? profile : {};
+    }
+
+    function normalizeTimestamp(value) {
+      if (value instanceof Date && !Number.isNaN(value.getTime())) {
+        return value.toISOString();
+      }
+      if (typeof value === 'number' && Number.isFinite(value)) {
+        const milliseconds = value > 1e11 ? value : value * 1000;
+        const date = new Date(milliseconds);
+        return Number.isNaN(date.getTime()) ? '' : date.toISOString();
+      }
+      if (typeof value !== 'string' || !value.trim()) return '';
+
+      const date = new Date(value);
+      return Number.isNaN(date.getTime()) ? '' : date.toISOString();
+    }
+
+    function timestampFromUnixSeconds(value) {
+      const numeric = Number(value);
+      if (!Number.isFinite(numeric)) return '';
+
+      const date = new Date(numeric * 1000);
+      return Number.isNaN(date.getTime()) ? '' : date.toISOString();
+    }
+
+    function epochSecondsFromValue(value) {
+      if (value === undefined || value === null || value === '') return 0;
+
+      const numeric = Number(value);
+      if (Number.isFinite(numeric)) {
+        return Math.trunc(numeric > 1e11 ? numeric / 1000 : numeric);
+      }
+
+      const parsed = Date.parse(String(value));
+      return Number.isFinite(parsed) ? Math.trunc(parsed / 1000) : 0;
+    }
+
+    function buildSyntheticCodexIdToken(email, accountId, planType, userId, expiresAt) {
+      const normalizedAccountId = normalizeString(accountId);
+      if (!normalizedAccountId) return '';
+
+      const now = Math.trunc(Date.now() / 1000);
+      const expires = epochSecondsFromValue(expiresAt) || now + 90 * 24 * 60 * 60;
+      const authInfo = { chatgpt_account_id: normalizedAccountId };
+
+      if (planType) authInfo.chatgpt_plan_type = normalizeString(planType);
+      if (userId) {
+        authInfo.chatgpt_user_id = normalizeString(userId);
+        authInfo.user_id = normalizeString(userId);
+      }
+
+      const payload = {
+        iat: now,
+        exp: expires,
+        'https://api.openai.com/auth': authInfo,
+      };
+      if (email) payload.email = normalizeString(email);
+
+      return `${encodeBase64UrlJson({ alg: 'none', typ: 'JWT', cpa_synthetic: true })}.${encodeBase64UrlJson(payload)}.synthetic`;
+    }
+
+    function normalizePlanTypeForFileName(planType = '') {
+      return normalizeString(planType)
+        .split(/[^a-zA-Z0-9]+/)
+        .map((part) => part.trim().toLowerCase())
+        .filter(Boolean)
+        .join('-');
+    }
+
+    function sanitizeFileSegment(value = '', fallback = 'chatgpt-session') {
+      const normalized = normalizeString(value)
+        .replace(/[\\/:*?"<>|]+/g, '-')
+        .replace(/\s+/g, '-')
+        .replace(/-+/g, '-')
+        .replace(/^-+|-+$/g, '');
+      return normalized || fallback;
+    }
+
+    function buildCpaAuthFileName(metadata = {}) {
+      const email = sanitizeFileSegment(metadata.email || '');
+      const planType = normalizePlanTypeForFileName(metadata.planType || '');
+      const accountId = sanitizeFileSegment(metadata.accountId || '');
+
+      if (email && planType) return `codex-${email}-${planType}.json`;
+      if (email) return `codex-${email}.json`;
+      if (accountId && planType) return `codex-${accountId}-${planType}.json`;
+      if (accountId) return `codex-${accountId}.json`;
+      return `codex-${Date.now()}.json`;
+    }
+
+    function buildCpaSessionAuthJson(state = {}, options = {}) {
+      const session = isPlainObject(state?.session) ? state.session : {};
+      const accessToken = normalizeString(state?.accessToken || session?.accessToken);
+      if (!accessToken) {
+        throw new Error('未读取到可导入的 ChatGPT accessToken。');
+      }
+
+      const inputIdToken = firstNonEmpty(
+        state?.idToken,
+        state?.id_token,
+        session?.idToken,
+        session?.id_token
+      );
+      const refreshToken = firstNonEmpty(
+        state?.refreshToken,
+        state?.refresh_token,
+        session?.refreshToken,
+        session?.refresh_token
+      );
+      const sessionToken = firstNonEmpty(
+        state?.sessionToken,
+        state?.session_token,
+        session?.sessionToken,
+        session?.session_token
+      );
+      const accessPayload = parseJwtPayload(accessToken);
+      const idPayload = parseJwtPayload(inputIdToken);
+      const accessAuth = getOpenAiAuthSection(accessPayload);
+      const idAuth = getOpenAiAuthSection(idPayload);
+      const profile = getOpenAiProfileSection(accessPayload);
+      const expiresAt = firstNonEmpty(
+        timestampFromUnixSeconds(accessPayload?.exp),
+        normalizeTimestamp(session?.expires),
+        normalizeTimestamp(session?.expiresAt),
+        normalizeTimestamp(session?.expired),
+        normalizeTimestamp(session?.expires_at)
+      );
+      const accountIdentifierEmail = normalizeString(state?.accountIdentifierType).toLowerCase() === 'email'
+        ? normalizeEmailValue(state?.accountIdentifier)
+        : '';
+      const email = firstNonEmpty(
+        normalizeEmailValue(session?.user?.email),
+        normalizeEmailValue(session?.email),
+        normalizeEmailValue(state?.email),
+        accountIdentifierEmail,
+        normalizeEmailValue(profile?.email),
+        normalizeEmailValue(idPayload?.email),
+        normalizeEmailValue(accessPayload?.email)
+      );
+      const accountId = firstNonEmpty(
+        session?.account?.id,
+        session?.account_id,
+        accessAuth?.chatgpt_account_id,
+        idAuth?.chatgpt_account_id
+      );
+      const userId = firstNonEmpty(
+        session?.user?.id,
+        session?.user_id,
+        accessAuth?.chatgpt_user_id,
+        accessAuth?.user_id,
+        idAuth?.chatgpt_user_id,
+        idAuth?.user_id
+      );
+      const planType = firstNonEmpty(
+        session?.account?.planType,
+        session?.account?.plan_type,
+        session?.planType,
+        session?.plan_type,
+        accessAuth?.chatgpt_plan_type,
+        idAuth?.chatgpt_plan_type
+      );
+      const exportedAt = normalizeTimestamp(options.now || new Date()) || new Date().toISOString();
+      const syntheticIdToken = inputIdToken
+        ? ''
+        : buildSyntheticCodexIdToken(email, accountId, planType, userId, expiresAt);
+      const idToken = inputIdToken || syntheticIdToken;
+      const authJson = Object.fromEntries(
+        Object.entries({
+          type: 'codex',
+          account_id: accountId,
+          chatgpt_account_id: accountId,
+          email,
+          name: firstNonEmpty(email, state?.email, 'ChatGPT Account'),
+          plan_type: planType,
+          chatgpt_plan_type: planType,
+          id_token: idToken,
+          id_token_synthetic: syntheticIdToken ? true : undefined,
+          access_token: accessToken,
+          refresh_token: refreshToken || '',
+          session_token: sessionToken,
+          last_refresh: exportedAt,
+          expired: expiresAt,
+          disabled: session?.disabled === true ? true : undefined,
+        }).filter(([, value]) => value !== undefined && value !== null && value !== '')
+      );
+
+      return {
+        authJson,
+        accountId,
+        email,
+        expiresAt,
+        fileName: buildCpaAuthFileName({ email, planType, accountId }),
+        hasRefreshToken: Boolean(refreshToken),
+      };
+    }
+
+    async function logWithOptions(message, level = 'info', options = {}) {
+      await addLog(message, level, options.logOptions || {});
+    }
+
+    async function importCurrentChatGptSession(state = {}, options = {}) {
+      const logLabel = normalizeString(options.logLabel) || 'CPA 会话导入';
+      const managementKey = normalizeString(state?.vpsPassword);
+      if (!managementKey) {
+        throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
+      }
+
+      const origin = deriveCpaManagementOrigin(state?.vpsUrl);
+      const sessionAuth = buildCpaSessionAuthJson(state, options);
+
+      await logWithOptions(`${logLabel}:正在通过 CPA 管理接口导入当前 ChatGPT 会话...`, 'info', options);
+      if (!sessionAuth.hasRefreshToken) {
+        await logWithOptions(`${logLabel}:未包含 refresh_token,access_token 过期后无法自动续期。`, 'warn', options);
+      }
+
+      await fetchCpaManagementJson(origin, `/v0/management/auth-files?name=${encodeURIComponent(sessionAuth.fileName)}`, {
+        method: 'POST',
+        managementKey,
+        timeoutMs: options.importTimeoutMs || options.timeoutMs,
+        body: sessionAuth.authJson,
+      });
+
+      const verifiedStatus = sessionAuth.email
+        ? `CPA 会话导入完成:${sessionAuth.email}`
+        : `CPA 会话导入完成:${sessionAuth.fileName}`;
+      await logWithOptions(verifiedStatus, 'ok', options);
+      return {
+        verifiedStatus,
+        cpaImportedFileName: sessionAuth.fileName,
+        cpaImportedEmail: sessionAuth.email || null,
+      };
+    }
+
+    return {
+      buildCpaSessionAuthJson,
+      deriveCpaManagementOrigin,
+      fetchCpaManagementJson,
+      importCurrentChatGptSession,
+    };
+  }
+
+  return {
+    createCpaApi,
+  };
+});

+ 1 - 1
background/generated-email-helpers.js

@@ -43,7 +43,7 @@
 
     async function fetchA4skyEmail() {
       throwIfStopped();
-      const email = `n${buildA4skyTimestamp()}@a4sky.com`;
+      const email = `n${buildA4skyTimestamp()}@edu.a4sky.com`;
       await setEmailState(email);
       await addLog(`A4Sky 邮箱:已生成 ${email}`, 'ok');
       return email;

+ 2 - 0
background/logging-status.js

@@ -27,6 +27,8 @@
         'hotmail-api': 'Hotmail(API对接/本地助手)',
         'luckmail-api': 'LuckMail(API 购邮)',
         'cloudflare-temp-email': 'Cloudflare Temp Email',
+        'checkout-stripe': 'Stripe 结账',
+        'checkout-paypal': 'PayPal 结账',
       };
       return labels[source] || source || '未知来源';
     }

+ 37 - 19
background/message-router.js

@@ -29,6 +29,7 @@
       executeStepViaCompletionSignal,
       exportSettingsBundle,
       fetchGeneratedEmail,
+      fetchPaypalSmsCode,
       finalizeStep3Completion,
       finalizeIcloudAliasAfterSuccessfulFlow,
       findHotmailAccount,
@@ -79,7 +80,17 @@
       testHotmailAccountMailAccess,
       upsertHotmailAccount,
       verifyHotmailAccount,
+      selectCheckoutStripeAutocompleteFrame,
     } = deps;
+    const contentScriptReadyLogCache = new Map();
+
+    function shouldLogContentScriptReady(source, tabId) {
+      const key = `${source}:${tabId}`;
+      const now = Date.now();
+      const previousAt = contentScriptReadyLogCache.get(key) || 0;
+      contentScriptReadyLogCache.set(key, now);
+      return !previousAt || now - previousAt > 5000;
+    }
 
     async function appendManualAccountRunRecordIfNeeded(status, stateOverride = null, reason = '') {
       if (typeof appendAccountRunRecord !== 'function') {
@@ -134,31 +145,13 @@
             await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
           }
           break;
-        case 7:
-          if (payload.loginVerificationRequestedAt) {
-            await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
-          }
-          break;
         case 4:
           await setState({
             lastEmailTimestamp: payload.emailTimestamp || null,
             signupVerificationRequestedAt: null,
           });
           break;
-        case 8:
-          await setState({
-            lastEmailTimestamp: payload.emailTimestamp || null,
-            loginVerificationRequestedAt: null,
-          });
-          break;
         case 9:
-          if (payload.localhostUrl) {
-            if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) {
-              throw new Error('步骤 9 返回了无效的 localhost OAuth 回调地址。');
-            }
-            await setState({ localhostUrl: payload.localhostUrl });
-            broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
-          }
           break;
         case 10: {
           if (payload.localhostUrl) {
@@ -203,11 +196,36 @@
           if (tabId && message.source) {
             await registerTab(message.source, tabId);
             flushCommand(message.source, tabId);
-            await addLog(`内容脚本已就绪:${getSourceLabel(message.source)}(标签页 ${tabId})`);
+            if (shouldLogContentScriptReady(message.source, tabId)) {
+              await addLog(`内容脚本已就绪:${getSourceLabel(message.source)}(标签页 ${tabId})`);
+            }
           }
           return { ok: true };
         }
 
+        case 'CHECKOUT_STRIPE_SELECT_AUTOCOMPLETE_FRAME': {
+          if (typeof selectCheckoutStripeAutocompleteFrame !== 'function') {
+            return { ok: false, error: '后台未启用 Stripe 地址 iframe 选择能力。' };
+          }
+          const tabId = sender.tab?.id;
+          if (!Number.isInteger(tabId)) {
+            return { ok: false, error: '缺少 checkout 标签页 ID,无法选择 Google 地址建议。' };
+          }
+          return selectCheckoutStripeAutocompleteFrame(tabId, message.payload || {});
+        }
+
+        case 'PAYPAL_FETCH_SMS_CODE': {
+          if (typeof fetchPaypalSmsCode !== 'function') {
+            return { ok: false, error: '后台未启用 PayPal 短信收码能力。' };
+          }
+          const result = await fetchPaypalSmsCode(message.payload || {});
+          return {
+            ok: true,
+            code: result.code,
+            attempt: result.attempt,
+          };
+        }
+
         case 'LOG': {
           const { message: msg, level } = message.payload;
           await addLog(`[${getSourceLabel(message.source)}] ${msg}`, level);

+ 8 - 0
background/navigation-utils.js

@@ -58,6 +58,13 @@
         && /\/email-verification(?:[/?#]|$)/i.test(parsed.pathname || '');
     }
 
+    function isSignupProfilePageUrl(rawUrl) {
+      const parsed = parseUrlSafely(rawUrl);
+      if (!parsed) return false;
+      return isSignupPageHost(parsed.hostname)
+        && /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile|about-you)(?:[/?#]|$)/i.test(parsed.pathname || '');
+    }
+
     function is163MailHost(hostname = '') {
       return hostname === 'mail.163.com'
         || hostname.endsWith('.mail.163.com')
@@ -159,6 +166,7 @@
       isSignupEntryHost,
       isSignupPageHost,
       isSignupPasswordPageUrl,
+      isSignupProfilePageUrl,
       matchesSourceUrlFamily,
       normalizeSub2ApiUrl,
       parseUrlSafely,

+ 48 - 5
background/signup-flow-helpers.js

@@ -3,6 +3,7 @@
 })(typeof self !== 'undefined' ? self : globalThis, function createSignupFlowHelpersModule() {
   function createSignupFlowHelpers(deps = {}) {
     const {
+      addLog,
       buildGeneratedAliasEmail,
       chrome,
       ensureContentScriptReadyOnTab,
@@ -14,14 +15,36 @@
       isLuckmailProvider,
       isSignupEmailVerificationPageUrl,
       isSignupPasswordPageUrl,
+      isSignupProfilePageUrl = null,
       reuseOrCreateTab,
       sendToContentScriptResilient,
       setEmailState,
       SIGNUP_ENTRY_URL,
       SIGNUP_PAGE_INJECT_FILES,
+      waitForTabStableComplete = null,
       waitForTabUrlMatch,
     } = deps;
 
+    async function waitForSignupEntryTabToSettle(tabId, step = 1) {
+      if (step !== 2 || !Number.isInteger(tabId) || typeof waitForTabStableComplete !== 'function') {
+        return null;
+      }
+
+      if (typeof addLog === 'function') {
+        await addLog(
+          `步骤 ${step}:注册页已打开,正在等待页面加载完成并额外稳定 3 秒...`,
+          'info'
+        );
+      }
+
+      return waitForTabStableComplete(tabId, {
+        timeoutMs: 45000,
+        retryDelayMs: 300,
+        stableMs: 3000,
+        initialDelayMs: 300,
+      });
+    }
+
     async function openSignupEntryTab(step = 1, options = {}) {
       const { reloadIfSameUrl = false } = options;
       const tabId = await reuseOrCreateTab('signup-page', SIGNUP_ENTRY_URL, {
@@ -30,6 +53,8 @@
         reloadIfSameUrl,
       });
 
+      await waitForSignupEntryTabToSettle(tabId, step);
+
       await ensureContentScriptReadyOnTab('signup-page', tabId, {
         inject: SIGNUP_PAGE_INJECT_FILES,
         injectSource: 'signup-page',
@@ -68,6 +93,9 @@
       if (isSignupEmailVerificationPageUrl(rawUrl)) {
         return 'verification_page';
       }
+      if (typeof isSignupProfilePageUrl === 'function' && isSignupProfilePageUrl(rawUrl)) {
+        return 'profile_page';
+      }
       return '';
     }
 
@@ -100,7 +128,7 @@
       }
 
       if (!landingState) {
-        throw new Error(`邮箱提交后未能识别当前页面,既不是密码页也不是邮箱验证码页。URL: ${landingUrl || 'unknown'}`);
+        throw new Error(`邮箱提交后未能识别当前页面,既不是密码页、邮箱验证码页,也不是资料页。URL: ${landingUrl || 'unknown'}`);
       }
 
       await ensureContentScriptReadyOnTab('signup-page', tabId, {
@@ -108,12 +136,27 @@
         injectSource: 'signup-page',
         timeoutMs: 45000,
         retryDelayMs: 900,
-        logMessage: landingState === 'verification_page'
-          ? `步骤 ${step}:邮箱验证码页仍在加载,正在等待页面恢复...`
-          : `步骤 ${step}:密码页仍在加载,正在重试连接内容脚本...`,
+        logMessage: landingState === 'password_page'
+          ? `步骤 ${step}:密码页仍在加载,正在重试连接内容脚本...`
+          : `步骤 ${step}:注册后续页面仍在加载,正在等待页面恢复...`,
       });
 
-      if (landingState === 'verification_page') {
+      if (landingState !== 'password_page') {
+        if (typeof waitForTabStableComplete === 'function') {
+          const stableTab = await waitForTabStableComplete(tabId, {
+            timeoutMs: 45000,
+            retryDelayMs: 300,
+            stableMs: 800,
+            initialDelayMs: 300,
+          });
+          if (stableTab?.url) {
+            const stableState = resolveSignupPostEmailState(stableTab.url);
+            if (stableState) {
+              landingUrl = stableTab.url;
+              landingState = stableState;
+            }
+          }
+        }
         return {
           ready: true,
           state: landingState,

+ 32 - 0
background/steps/fill-paypal-login.js

@@ -0,0 +1,32 @@
+// background/steps/fill-paypal-login.js — Step 8: Fill PayPal login email
+(function attachBackgroundStep13(root, factory) {
+  root.MultiPageBackgroundStep13 = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep13Module() {
+  function createStep13Executor(deps = {}) {
+    const {
+      addLog,
+      generateRandomEmail,
+      sendToContentScriptResilient,
+    } = deps;
+
+    async function executeStep13(state) {
+      await addLog('步骤 8:正在填写 PayPal 登录邮箱...');
+
+      const email = generateRandomEmail();
+      await addLog(`已生成随机邮箱: ${email}`);
+
+      // The checkout-paypal content script auto-injects on paypal.com via manifest.
+      // After Stripe form submission, the tab has redirected to paypal.com.
+      await sendToContentScriptResilient('checkout-paypal', {
+        type: 'EXECUTE_STEP',
+        step: 8,
+        source: 'background',
+        payload: { email },
+      });
+    }
+
+    return { executeStep13 };
+  }
+
+  return { createStep13Executor };
+});

+ 123 - 0
background/steps/fill-paypal-payment.js

@@ -0,0 +1,123 @@
+// background/steps/fill-paypal-payment.js — Step 9: Fill PayPal checkout payment form
+(function attachBackgroundStep14(root, factory) {
+  root.MultiPageBackgroundStep14 = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep14Module() {
+  function createStep14Executor(deps = {}) {
+    const {
+      addLog,
+      fetchCheckoutAddress,
+      generateRandomEmail,
+      sendToContentScriptResilient,
+    } = deps;
+
+    async function executeStep14(state = {}) {
+      await addLog('步骤 9:正在准备 PayPal 付款信息...');
+
+      const card = generateLuhnVisaTestCard();
+      await addLog(`已生成测试 VISA 卡号(Luhn):尾号 ${card.Credit_Card_Number.slice(-4)} 有效期 ${card.Expires}`);
+
+      // Fetch random US address
+      const addr = await fetchCheckoutAddress();
+      await addLog(`已获取地址: ${addr.street}, ${addr.city}, ${addr.state} ${addr.zip}`);
+
+      // Generate random credentials
+      const email = generateRandomEmail();
+      const password = generateRandomPassword();
+
+      // The checkout-paypal content script is still active after step 8's PayPal login.
+      // After PayPal login form submission, the tab has redirected to the checkout page.
+      await addLog('正在向 PayPal 页面发送填表指令...');
+
+      await sendToContentScriptResilient('checkout-paypal', {
+        type: 'EXECUTE_STEP',
+        step: 9,
+        source: 'background',
+        payload: {
+          card,
+          address: addr,
+          email,
+          password,
+          phone: normalizePaypalPhoneForInput(state.paypalPhone || state.checkoutPhone || '+15822201173'),
+          paypalPhone: state.paypalPhone || state.checkoutPhone || '+15822201173',
+          paypalSmsApiUrl: state.paypalSmsApiUrl || 'http://a.62-us.com/api/get_sms?key=a5d3262e05efaba982aba7cfae20b8bc',
+        },
+      });
+    }
+
+    return { executeStep14 };
+  }
+
+  function generateRandomPassword() {
+    const L = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+    const D = '0123456789';
+    const S = '!@#$%^';
+    const A = L + D + S;
+    let p = L[Math.floor(Math.random() * 26)]
+      + L[26 + Math.floor(Math.random() * 26)]
+      + D[Math.floor(Math.random() * 10)]
+      + S[Math.floor(Math.random() * 6)];
+    for (let i = 4; i < 14; i++) p += A[Math.floor(Math.random() * A.length)];
+    return p.split('').sort(() => Math.random() - 0.5).join('');
+  }
+
+  function normalizePaypalPhoneForInput(value = '') {
+    const raw = String(value || '').trim();
+    const digits = raw.replace(/\D+/g, '');
+    if (digits.length === 11 && digits.startsWith('1')) {
+      return digits.slice(1);
+    }
+    return digits || raw;
+  }
+
+  function generateLuhnVisaTestCard() {
+    return {
+      Credit_Card_Type: 'Visa',
+      Credit_Card_Number: generateVisaLuhnCardNumber(),
+      Expires: generateFutureExpiry(),
+      CVV2: generateNumericString(3),
+    };
+  }
+
+  function generateVisaLuhnCardNumber() {
+    const body = `4${generateNumericString(14)}`;
+    return `${body}${computeLuhnCheckDigit(body)}`;
+  }
+
+  function computeLuhnCheckDigit(body) {
+    const sumWithZero = luhnSum(`${body}0`);
+    return String((10 - (sumWithZero % 10)) % 10);
+  }
+
+  function luhnSum(value) {
+    let sum = 0;
+    let shouldDouble = false;
+    for (let index = String(value).length - 1; index >= 0; index -= 1) {
+      let digit = Number(String(value)[index]);
+      if (!Number.isFinite(digit)) digit = 0;
+      if (shouldDouble) {
+        digit *= 2;
+        if (digit > 9) digit -= 9;
+      }
+      sum += digit;
+      shouldDouble = !shouldDouble;
+    }
+    return sum;
+  }
+
+  function generateFutureExpiry() {
+    const now = new Date();
+    const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
+    const year = String((now.getFullYear() + 3 + Math.floor(Math.random() * 4)) % 100).padStart(2, '0');
+    return `${month}/${year}`;
+  }
+
+  function generateNumericString(length) {
+    let value = '';
+    for (let index = 0; index < length; index += 1) {
+      value += String(Math.floor(Math.random() * 10));
+    }
+    return value;
+  }
+
+  return { createStep14Executor };
+});

+ 451 - 0
background/steps/fill-stripe-checkout.js

@@ -0,0 +1,451 @@
+// background/steps/fill-stripe-checkout.js — Step 7: Fill Stripe checkout form
+(function attachBackgroundStep12(root, factory) {
+  root.MultiPageBackgroundStep12 = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep12Module() {
+  const CHECKOUT_STRIPE_SOURCE = 'checkout-stripe';
+  const CHECKOUT_STRIPE_INJECT_FILES = ['content/activation-utils.js', 'content/utils.js', 'content/checkout-stripe.js'];
+  const CHECKOUT_URL_PATTERNS = [
+    'https://chatgpt.com/checkout/*',
+    'https://pay.openai.com/*',
+    'https://checkout.stripe.com/*',
+  ];
+  const SUBMIT_MAX_ATTEMPTS = 3;
+  const PAYPAL_REDIRECT_TIMEOUT_MS = 20000;
+
+  function createStep12Executor(deps = {}) {
+    const {
+      addLog,
+      chrome,
+      completeStepFromBackground,
+      fetchCheckoutAddress,
+      getTabId,
+      sendToContentScriptResilient,
+    } = deps;
+
+    async function executeStep12(state) {
+      await addLog('步骤 7:正在填写 Stripe 结账表单...');
+
+      // Fetch a random US address
+      const addr = await fetchCheckoutAddress();
+      await addLog(`已获取美国地址: ${addr.street}, ${addr.city}, ${addr.state} ${addr.zip}`);
+
+      if (!chrome?.tabs?.sendMessage || !chrome?.webNavigation?.getAllFrames || !chrome?.scripting?.executeScript) {
+        await addLog('步骤 7:当前环境不支持 checkout iframe 编排,回退到旧版主 frame 填写方式。', 'warn');
+        await sendToContentScriptResilient(CHECKOUT_STRIPE_SOURCE, {
+          type: 'EXECUTE_STEP',
+          step: 7,
+          source: 'background',
+          payload: { address: addr },
+        });
+        return;
+      }
+
+      const tabId = await resolveCheckoutTabId();
+      if (!tabId) {
+        throw new Error('步骤 7:未找到 Stripe/Plus Checkout 标签页,请先完成步骤 6。');
+      }
+
+      await addLog('步骤 7:正在等待 Checkout 页面加载完成...');
+      await waitForTabComplete(tabId);
+      await sleep(800);
+
+      const readyFrames = await getReadyCheckoutFrames(tabId);
+      const paymentFrame = await resolvePaymentFrame(tabId, readyFrames);
+      if (paymentFrame.frameId === null) {
+        throw new Error(`步骤 7:未在主页面或 iframe 中发现 PayPal 付款方式。frame 摘要:${buildFrameSummary(paymentFrame.inspections)}`);
+      }
+      if (paymentFrame.frameId !== 0) {
+        await addLog(`步骤 7:PayPal 位于 checkout iframe(frameId=${paymentFrame.frameId}),将改为在该 frame 内操作。`, 'info');
+      }
+
+      await addLog('步骤 7:正在切换 PayPal 付款方式...');
+      const paymentResult = await sendFrameMessage(tabId, paymentFrame.frameId, {
+        type: 'CHECKOUT_STRIPE_SELECT_PAYPAL',
+        source: 'background',
+        payload: { relaxedActivation: true },
+      });
+      if (paymentResult?.error) {
+        throw new Error(paymentResult.error);
+      }
+
+      const billingFrame = await waitForBillingOrHostedSubmitFrameAfterPaymentSelection(tabId, paymentFrame);
+      let billingResult = {
+        countryText: '',
+        structuredAddress: null,
+      };
+      if (billingFrame.frameId !== paymentFrame.frameId) {
+        await addLog(`步骤 7:账单地址位于 checkout iframe(frameId=${billingFrame.frameId}),将改为在该 frame 内填写。`, 'info');
+      }
+
+      await addLog(
+        billingFrame.autoJsDirectSelectors
+          ? '步骤 7:未发现独立账单地址 iframe,参考 auto.js 在当前 checkout frame 内直接填写固定账单字段...'
+          : '步骤 7:正在填写账单地址...'
+      );
+      billingResult = await sendFrameMessage(tabId, billingFrame.frameId, {
+        type: 'CHECKOUT_STRIPE_FILL_BILLING_ADDRESS',
+        source: 'background',
+        payload: {
+          address: addr,
+          autoJsDirectSelectors: Boolean(billingFrame.autoJsDirectSelectors),
+        },
+      });
+      if (billingResult?.error) {
+        throw new Error(billingResult.error);
+      }
+
+      let redirectedToPayPal = false;
+      let lastSubmitError = '';
+      for (let attempt = 1; attempt <= SUBMIT_MAX_ATTEMPTS; attempt += 1) {
+        await addLog(
+          attempt === 1
+            ? (billingFrame.autoJsDirectSelectors
+              ? '步骤 7:账单地址已按 auto.js 方式处理,等待 1.5 秒后提交...'
+              : '步骤 7:账单地址已填写完成,等待 3 秒让 checkout 完成校验...')
+            : `步骤 7:准备第 ${attempt}/${SUBMIT_MAX_ATTEMPTS} 次重新提交...`,
+          attempt === 1 ? 'info' : 'warn'
+        );
+        await sleep(billingFrame.autoJsDirectSelectors ? 1500 : 3000);
+
+        const submitFrame = await waitForSubmitFrame(tabId, [
+          { frameId: 0, url: '' },
+          { frameId: paymentFrame.frameId, url: paymentFrame.frameUrl || '' },
+          { frameId: billingFrame.frameId, url: billingFrame.frameUrl || '' },
+        ]);
+        const submitResult = await sendFrameMessage(tabId, submitFrame.frameId, {
+          type: 'CHECKOUT_STRIPE_CLICK_SUBMIT',
+          source: 'background',
+          payload: {
+            beforeClickDelayMs: attempt === 1 ? 700 : 1200,
+          },
+        });
+        if (submitResult?.error) {
+          lastSubmitError = submitResult.error;
+          await addLog(`步骤 7:点击订阅失败(${attempt}/${SUBMIT_MAX_ATTEMPTS}):${lastSubmitError}`, 'warn');
+          continue;
+        }
+
+        await addLog(`步骤 7:已提交 checkout,正在等待跳转到 PayPal(${attempt}/${SUBMIT_MAX_ATTEMPTS})...`, 'info');
+        redirectedToPayPal = await waitForPayPalRedirectAfterSubmit(tabId);
+        if (redirectedToPayPal) break;
+
+        lastSubmitError = `提交后 ${Math.round(PAYPAL_REDIRECT_TIMEOUT_MS / 1000)} 秒内未跳转到 PayPal`;
+        await addLog(`步骤 7:${lastSubmitError},将重试提交。`, 'warn');
+      }
+
+      if (!redirectedToPayPal) {
+        throw new Error(`步骤 7:多次提交 checkout 后仍未跳转到 PayPal。${lastSubmitError}`);
+      }
+
+      if (typeof completeStepFromBackground === 'function') {
+        await completeStepFromBackground(7, {
+          plusBillingCountryText: billingResult?.countryText || '',
+          plusBillingAddress: billingResult?.structuredAddress || null,
+        });
+      }
+    }
+
+    async function resolveCheckoutTabId() {
+      const registered = await getTabId(CHECKOUT_STRIPE_SOURCE);
+      if (registered && await isCheckoutTab(registered)) {
+        return registered;
+      }
+
+      const signupTab = await getTabId('signup-page');
+      if (signupTab && await isCheckoutTab(signupTab)) {
+        return signupTab;
+      }
+
+      const tabs = await chrome.tabs.query({ url: CHECKOUT_URL_PATTERNS }).catch(() => []);
+      const tab = tabs.find((item) => Number.isInteger(item?.id) && isCheckoutUrl(item.url));
+      return tab?.id || null;
+    }
+
+    async function isCheckoutTab(tabId) {
+      const tab = await chrome.tabs.get(tabId).catch(() => null);
+      return Boolean(tab && isCheckoutUrl(tab.url));
+    }
+
+    function isCheckoutUrl(url = '') {
+      return /^https:\/\/(?:chatgpt\.com\/checkout|pay\.openai\.com|checkout\.stripe\.com)(?:\/|$)/i.test(String(url || ''));
+    }
+
+    async function waitForTabComplete(tabId, timeoutMs = 30000) {
+      const startedAt = Date.now();
+      while (Date.now() - startedAt < timeoutMs) {
+        const tab = await chrome.tabs.get(tabId).catch(() => null);
+        if (!tab) throw new Error('步骤 7:Checkout 标签页已关闭。');
+        if (tab.status === 'complete') return tab;
+        await sleep(300);
+      }
+      return chrome.tabs.get(tabId).catch(() => null);
+    }
+
+    async function getCheckoutFrames(tabId) {
+      const frames = await chrome.webNavigation.getAllFrames({ tabId }).catch(() => null);
+      if (!Array.isArray(frames) || !frames.length) {
+        return [{ frameId: 0, url: '' }];
+      }
+      return frames
+        .filter((frame) => Number.isInteger(frame?.frameId))
+        .sort((left, right) => Number(left.frameId) - Number(right.frameId));
+    }
+
+    async function pingCheckoutFrame(tabId, frameId) {
+      try {
+        const pong = await chrome.tabs.sendMessage(tabId, {
+          type: 'PING',
+          source: 'background',
+          payload: {},
+        }, {
+          frameId: Number.isInteger(frameId) ? frameId : 0,
+        });
+        return Boolean(pong?.ok && (!pong.source || pong.source === CHECKOUT_STRIPE_SOURCE));
+      } catch {
+        return false;
+      }
+    }
+
+    async function ensureCheckoutFrameReady(tabId, frameId) {
+      if (await pingCheckoutFrame(tabId, frameId)) {
+        return true;
+      }
+
+      try {
+        await chrome.scripting.executeScript({
+          target: { tabId, frameIds: [frameId] },
+          func: (injectedSource) => {
+            window.__MULTIPAGE_SOURCE = injectedSource;
+          },
+          args: [CHECKOUT_STRIPE_SOURCE],
+        });
+        await chrome.scripting.executeScript({
+          target: { tabId, frameIds: [frameId] },
+          files: CHECKOUT_STRIPE_INJECT_FILES,
+        });
+      } catch {
+        // Some Stripe helper frames are intentionally not scriptable. They are skipped below.
+      }
+
+      await sleep(500);
+      return pingCheckoutFrame(tabId, frameId);
+    }
+
+    async function getReadyCheckoutFrames(tabId) {
+      const frames = await getCheckoutFrames(tabId);
+      const readyFrames = [];
+      for (const frame of frames) {
+        const ready = await ensureCheckoutFrameReady(tabId, frame.frameId);
+        readyFrames.push({ ...frame, ready });
+      }
+      return readyFrames;
+    }
+
+    async function sendFrameMessage(tabId, frameId, message) {
+      return chrome.tabs.sendMessage(tabId, message, {
+        frameId: Number.isInteger(frameId) ? frameId : 0,
+      });
+    }
+
+    async function inspectCheckoutFrame(tabId, frame) {
+      if (frame.ready === false) {
+        return { frame, error: 'content-script-not-ready' };
+      }
+      try {
+        const result = await sendFrameMessage(tabId, frame.frameId, {
+          type: 'CHECKOUT_STRIPE_GET_STATE',
+          source: 'background',
+          payload: {},
+        });
+        return { frame, result: result || {} };
+      } catch (error) {
+        return { frame, error: error?.message || String(error || '') };
+      }
+    }
+
+    async function inspectCheckoutFrames(tabId, frames) {
+      const inspections = [];
+      for (const frame of frames) {
+        inspections.push(await inspectCheckoutFrame(tabId, frame));
+      }
+      return inspections;
+    }
+
+    async function resolvePaymentFrame(tabId, frames) {
+      const inspections = await inspectCheckoutFrames(tabId, frames);
+      const picked = pickPaymentFrame(inspections);
+      if (!picked) {
+        return { frameId: null, frameUrl: '', inspections };
+      }
+      return {
+        frameId: picked.frame.frameId,
+        frameUrl: picked.frame.url || '',
+        inspections,
+      };
+    }
+
+    function isPaymentFrameUrl(url = '') {
+      return /elements-inner-payment|componentName=payment/i.test(String(url || ''));
+    }
+
+    function isAddressFrameUrl(url = '') {
+      return /elements-inner-address|componentName=address/i.test(String(url || ''));
+    }
+
+    function pickPaymentFrame(inspections = []) {
+      return inspections.find((item) => item.result?.hasPayPal || item.result?.paypalCandidates?.length)
+        || inspections.find((item) => isPaymentFrameUrl(item.frame.url))
+        || null;
+    }
+
+    function pickBillingFrame(inspections = []) {
+      return inspections.find((item) => item.result?.billingFieldsVisible)
+        || inspections.find((item) => isAddressFrameUrl(item.frame.url))
+        || null;
+    }
+
+    function pickHostedPayPalSubmitFrame(inspections = []) {
+      return inspections.find((item) => item.result?.hasPayPal && item.result?.hasSubmitButton)
+        || null;
+    }
+
+    async function getHostedPayPalSubmitFrame(tabId) {
+      const frames = await getReadyCheckoutFrames(tabId);
+      const inspections = await inspectCheckoutFrames(tabId, frames);
+      const picked = pickHostedPayPalSubmitFrame(inspections);
+      if (!picked) {
+        return null;
+      }
+      return {
+        frameId: picked.frame.frameId,
+        frameUrl: picked.frame.url || '',
+        inspections,
+        skipBillingAddress: true,
+      };
+    }
+
+    async function waitForBillingOrHostedSubmitFrameAfterPaymentSelection(tabId, paymentFrame) {
+      try {
+        return await waitForBillingFrame(tabId, 6500);
+      } catch (firstError) {
+        const hostedSubmitFrame = await getHostedPayPalSubmitFrame(tabId);
+        if (hostedSubmitFrame) {
+          await addLog(`步骤 7:未等到账单地址 iframe,但当前 frame 已有 PayPal 与提交按钮,将参考 auto.js 直接填写当前页面字段。${firstError.message}`, 'warn');
+          return hostedSubmitFrame;
+        }
+        await addLog(`步骤 7:首次等待账单地址未出现,准备按 auto.js 方式重新点击 PayPal。${firstError.message}`, 'warn');
+      }
+
+      const latestFrames = await getReadyCheckoutFrames(tabId);
+      const latestPaymentFrame = await resolvePaymentFrame(tabId, latestFrames);
+      const retryFrameId = latestPaymentFrame.frameId ?? paymentFrame.frameId;
+      if (Number.isInteger(retryFrameId)) {
+        const retryResult = await sendFrameMessage(tabId, retryFrameId, {
+          type: 'CHECKOUT_STRIPE_SELECT_PAYPAL',
+          source: 'background',
+          payload: { relaxedActivation: true },
+        });
+        if (retryResult?.error) {
+          await addLog(`步骤 7:再次点击 PayPal 失败:${retryResult.error}`, 'warn');
+        } else {
+          await addLog('步骤 7:已再次点击 PayPal,继续检测账单地址或 hosted checkout 提交区...', 'info');
+        }
+      }
+
+      try {
+        return await waitForBillingFrame(tabId, 6500);
+      } catch (secondError) {
+        const hostedSubmitFrame = await getHostedPayPalSubmitFrame(tabId);
+        if (hostedSubmitFrame) {
+          await addLog(`步骤 7:二次等待账单地址仍未出现,将参考 auto.js 在当前页面直接填固定账单字段后提交。${secondError.message}`, 'warn');
+          return hostedSubmitFrame;
+        }
+        throw secondError;
+      }
+    }
+
+    async function waitForBillingFrame(tabId, timeoutMs = 30000) {
+      const startedAt = Date.now();
+      let lastInspections = [];
+      while (Date.now() - startedAt < timeoutMs) {
+        const frames = await getReadyCheckoutFrames(tabId);
+        const inspections = await inspectCheckoutFrames(tabId, frames);
+        lastInspections = inspections;
+        const picked = pickBillingFrame(inspections);
+        if (picked) {
+          return {
+            frameId: picked.frame.frameId,
+            frameUrl: picked.frame.url || '',
+            inspections,
+          };
+        }
+        await sleep(250);
+      }
+      throw new Error(`步骤 7:等待账单地址 iframe 超时。frame 摘要:${buildFrameSummary(lastInspections) || '无可检测 frame'}`);
+    }
+
+    async function waitForSubmitFrame(tabId, candidateFrames = [], timeoutMs = 15000) {
+      const startedAt = Date.now();
+      while (Date.now() - startedAt < timeoutMs) {
+        const frameMap = new Map();
+        for (const item of candidateFrames) {
+          if (Number.isInteger(item?.frameId)) frameMap.set(item.frameId, item);
+        }
+        const readyFrames = await getReadyCheckoutFrames(tabId);
+        for (const frame of readyFrames) {
+          if (!frameMap.has(frame.frameId)) frameMap.set(frame.frameId, frame);
+        }
+        const inspections = await inspectCheckoutFrames(tabId, Array.from(frameMap.values()));
+        const picked = inspections.find((item) => item.result?.hasSubmitButton);
+        if (picked) {
+          return picked.frame;
+        }
+        await sleep(250);
+      }
+      throw new Error('步骤 7:等待订阅按钮超时。');
+    }
+
+    async function waitForPayPalRedirectAfterSubmit(tabId) {
+      const startedAt = Date.now();
+      while (Date.now() - startedAt < PAYPAL_REDIRECT_TIMEOUT_MS) {
+        const tab = await chrome.tabs.get(tabId).catch(() => null);
+        if (!tab) throw new Error('步骤 7:Checkout 标签页已关闭,无法继续等待 PayPal 跳转。');
+        const url = String(tab.url || '');
+        if (/paypal\./i.test(url) && !isCheckoutUrl(url)) {
+          await waitForTabComplete(tabId, 15000);
+          await sleep(1000);
+          return true;
+        }
+        if (url && !isCheckoutUrl(url)) {
+          await addLog(`步骤 7:点击订阅后页面跳转到非 PayPal 识别地址:${url}`, 'warn');
+          return false;
+        }
+        await sleep(500);
+      }
+      return false;
+    }
+
+    function buildFrameSummary(inspections = []) {
+      return inspections
+        .map((item) => {
+          const flags = [];
+          if (item.result?.hasPayPal) flags.push('paypal');
+          if (item.result?.billingFieldsVisible) flags.push('billing');
+          if (item.result?.hasSubmitButton) flags.push('submit');
+          if (!flags.length && item.error) flags.push(item.error);
+          if (!flags.length) flags.push('no-match');
+          return `${item.frame.frameId}:${item.frame.url || 'about:blank'}:${flags.join(',')}`;
+        })
+        .slice(0, 8)
+        .join(' | ');
+    }
+
+    return { executeStep12 };
+  }
+
+  function sleep(ms) {
+    return new Promise((resolve) => setTimeout(resolve, ms));
+  }
+
+  return { createStep12Executor };
+});

+ 248 - 0
background/steps/get-plus-link.js

@@ -0,0 +1,248 @@
+// background/steps/get-plus-link.js — Step 6: Get ChatGPT Plus Stripe checkout link
+(function attachBackgroundStep11(root, factory) {
+  root.MultiPageBackgroundStep11 = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep11Module() {
+  const PAYURL_CHECKOUT_ENDPOINT = 'https://payurl.ark2.cn/api/checkout';
+  const PAYURL_CHECKOUT_MAX_ATTEMPTS = 5;
+  const PAYURL_CHECKOUT_RETRY_DELAY_MS = 1200;
+
+  function createStep11Executor(deps = {}) {
+    const {
+      addLog,
+      chrome,
+      completeStepFromBackground,
+      getTabId,
+      reuseOrCreateTab,
+      waitForTabStableComplete = null,
+    } = deps;
+
+    async function executeStep11() {
+      await addLog('步骤 6:正在获取 ChatGPT Plus 订阅链接...');
+
+      const tabId = await getTabId('signup-page');
+
+      if (!tabId) {
+        throw new Error('未找到 ChatGPT 标签页,请先打开 chatgpt.com');
+      }
+
+      await waitForChatGptLoginPageStable(tabId, {
+        addLog,
+        waitForTabStableComplete,
+      });
+
+      const accessToken = await getChatGptAccessToken(tabId, chrome, addLog);
+      if (!accessToken) {
+        throw new Error('未获取到 accessToken,请确认已登录 ChatGPT');
+      }
+
+      await addLog('已获取 ChatGPT Session Token,正在请求 Plus 长链...');
+      const result = await requestPayurlCheckoutWithRetry(accessToken, addLog);
+
+      if (!result.hostedUrl) {
+        throw new Error('未从 payurl.ark2.cn 响应中解析到 Plus 长链');
+      }
+
+      await addLog(`已获取 Plus 链接,正在跳转到 Stripe 结账页面...`);
+      await addLog(`Session ID: ${result.checkoutSessionId || '未知'}`);
+
+      // Navigate to the Stripe checkout URL
+      await reuseOrCreateTab('signup-page', result.hostedUrl, {
+        reloadIfSameUrl: false,
+      });
+
+      await completeStepFromBackground(6, {
+        checkoutUrl: result.hostedUrl,
+        checkoutSessionId: result.checkoutSessionId,
+        chatgptCheckoutUrl: result.chatgptCheckoutUrl,
+        openaiPayUrl: result.openaiPayUrl,
+      });
+    }
+
+    return { executeStep11 };
+  }
+
+  async function waitForChatGptLoginPageStable(tabId, deps = {}) {
+    const {
+      addLog,
+      waitForTabStableComplete,
+    } = deps;
+    if (typeof waitForTabStableComplete !== 'function') {
+      return null;
+    }
+
+    await addLog('步骤 6:正在等待步骤 5 登录后的 ChatGPT 页面稳定,再读取 Session...', 'info');
+    const startedAt = Date.now();
+    let lastTab = null;
+    while (Date.now() - startedAt < 45000) {
+      lastTab = await waitForTabStableComplete(tabId, {
+        timeoutMs: 6000,
+        retryDelayMs: 300,
+        stableMs: 1500,
+        initialDelayMs: 600,
+      });
+      if (!lastTab) {
+        throw new Error('步骤 6:ChatGPT 标签页已关闭,无法获取 Session。');
+      }
+      const url = String(lastTab.url || '');
+      if (isChatGptWebUrl(url)) {
+        await addLog('步骤 6:ChatGPT 页面已稳定,开始读取 Session。', 'info');
+        return lastTab;
+      }
+      await sleep(500);
+    }
+
+    const lastUrl = String(lastTab?.url || '');
+    await addLog(
+      `步骤 6:等待 ChatGPT 页面稳定超时,最后页面为 ${lastUrl || 'unknown'},将继续尝试读取 Session。`,
+      'warn'
+    );
+    return lastTab;
+  }
+
+  function isChatGptWebUrl(rawUrl = '') {
+    try {
+      const parsed = new URL(String(rawUrl || ''));
+      return /(?:^|\.)chatgpt\.com$/i.test(parsed.hostname);
+    } catch {
+      return false;
+    }
+  }
+
+  async function getChatGptAccessToken(tabId, chrome, addLog) {
+    try {
+      return await fetchAccessTokenFromSessionEndpoint();
+    } catch (error) {
+      await addLog(`后台获取 ChatGPT Session Token 失败,尝试在页面上下文重试:${error.message}`, 'warn');
+    }
+
+    const results = await chrome.scripting.executeScript({
+      target: { tabId },
+      func: fetchAccessTokenInPage,
+    });
+    const tokenResult = results?.[0]?.result;
+    if (!tokenResult || tokenResult.error) {
+      throw new Error(tokenResult?.error || '获取 ChatGPT Session Token 失败,请确认已登录 ChatGPT');
+    }
+    return String(tokenResult.accessToken || '').trim();
+  }
+
+  async function fetchAccessTokenFromSessionEndpoint() {
+    const sessionResp = await fetch('https://chatgpt.com/api/auth/session', {
+      credentials: 'include',
+      headers: {
+        'Accept': 'application/json',
+      },
+    });
+    const text = await sessionResp.text();
+    let session = null;
+    try {
+      session = text ? JSON.parse(text) : null;
+    } catch (error) {
+      throw new Error(`Session 响应不是有效 JSON:${text.slice(0, 300)}`);
+    }
+    if (!sessionResp.ok) {
+      throw new Error(`Session 请求失败,HTTP ${sessionResp.status}: ${JSON.stringify(session)}`);
+    }
+    const accessToken = String(session?.accessToken || '').trim();
+    if (!accessToken) {
+      throw new Error('Session 响应中没有 accessToken');
+    }
+    return accessToken;
+  }
+
+  /**
+   * This function is executed in the page context via chrome.scripting.executeScript.
+   * It has access to the page's cookies and can make same-origin requests.
+   */
+  async function fetchAccessTokenInPage() {
+    try {
+      const sessionResp = await fetch('https://chatgpt.com/api/auth/session', {
+        credentials: 'include',
+      });
+      const session = await sessionResp.json();
+      const accessToken = session?.accessToken;
+      if (!accessToken) {
+        return { error: '未获取到 accessToken,请确认已登录 ChatGPT' };
+      }
+      return { accessToken };
+    } catch (e) {
+      return { error: e.message };
+    }
+  }
+
+  async function requestPayurlCheckoutWithRetry(accessToken, addLog) {
+    let lastError = null;
+    for (let attempt = 1; attempt <= PAYURL_CHECKOUT_MAX_ATTEMPTS; attempt += 1) {
+      try {
+        if (attempt > 1) {
+          await addLog(`Plus 长链获取失败,正在重试 ${attempt}/${PAYURL_CHECKOUT_MAX_ATTEMPTS}...`, 'warn');
+          await sleep(PAYURL_CHECKOUT_RETRY_DELAY_MS);
+        }
+        return await requestPayurlCheckout(accessToken);
+      } catch (error) {
+        lastError = error;
+        await addLog(`Plus 长链获取失败(${attempt}/${PAYURL_CHECKOUT_MAX_ATTEMPTS}):${error.message}`, 'warn');
+      }
+    }
+    throw new Error(`Plus 长链获取连续失败 ${PAYURL_CHECKOUT_MAX_ATTEMPTS} 次:${lastError?.message || '未知错误'}`);
+  }
+
+  async function requestPayurlCheckout(accessToken) {
+    const payload = {
+      token: accessToken,
+      plan: 'plus',
+      checkout_ui_mode: 'hosted',
+      ui_language: 'en',
+      country: 'US',
+      currency: 'USD',
+      proxy: '',
+      use_promo: true,
+      promo_code: 'STRIPEATLASGPT4BIZ050126',
+      workspace_name: 'linux-do',
+      seat_quantity: 2,
+    };
+
+    const response = await fetch(PAYURL_CHECKOUT_ENDPOINT, {
+      method: 'POST',
+      headers: {
+        'Accept': '*/*',
+        'Accept-Language': 'zh-CN,zh;q=0.9',
+        'Content-Type': 'application/json',
+        'DNT': '1',
+        'Origin': 'https://payurl.ark2.cn',
+        'Referer': 'https://payurl.ark2.cn/',
+      },
+      body: JSON.stringify(payload),
+    });
+
+    const responseText = await response.text();
+    let data = null;
+    try {
+      data = responseText ? JSON.parse(responseText) : null;
+    } catch (error) {
+      throw new Error(`payurl 响应不是有效 JSON:${responseText.slice(0, 300)}`);
+    }
+
+    if (!response.ok) {
+      throw new Error(`payurl 请求失败,HTTP ${response.status}: ${JSON.stringify(data)}`);
+    }
+
+    const hostedUrl = data?.url || data?.openai_payurl || data?.chatgpt_checkout_url || '';
+    if (!hostedUrl) {
+      throw new Error(`payurl 响应中未找到 url/openai_payurl/chatgpt_checkout_url:${JSON.stringify(data)}`);
+    }
+
+    return {
+      hostedUrl,
+      checkoutSessionId: data.checkout_session_id || '',
+      chatgptCheckoutUrl: data.chatgpt_checkout_url || '',
+      openaiPayUrl: data.openai_payurl || data.url || '',
+    };
+  }
+
+  function sleep(ms) {
+    return new Promise((resolve) => setTimeout(resolve, ms));
+  }
+
+  return { createStep11Executor };
+});

+ 238 - 0
background/steps/sync-cpa-session.js

@@ -0,0 +1,238 @@
+// background/steps/sync-cpa-session.js — Step 10: Sync current ChatGPT session to CPA
+(function attachBackgroundCpaSessionSync(root, factory) {
+  root.MultiPageBackgroundCpaSessionSync = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundCpaSessionSyncModule() {
+  const CHATGPT_SESSION_URL = 'https://chatgpt.com/';
+  const CHATGPT_SESSION_ENDPOINT = 'https://chatgpt.com/api/auth/session';
+
+  function createCpaSessionSyncExecutor(deps = {}) {
+    const {
+      addLog = async () => {},
+      chrome = null,
+      completeStepFromBackground = async () => {},
+      createCpaApi = null,
+      fetchImpl = (...args) => fetch(...args),
+      getPanelMode = (state) => (state?.panelMode === 'sub2api' ? 'sub2api' : 'cpa'),
+      getTabId = async () => null,
+      sleepWithStop = sleep,
+      throwIfStopped = () => {},
+      waitForTabComplete = async () => null,
+    } = deps;
+
+    let cpaApi = null;
+
+    function getApi() {
+      if (cpaApi) return cpaApi;
+
+      const factory = createCpaApi || self.MultiPageBackgroundCpaApi?.createCpaApi;
+      if (typeof factory !== 'function') {
+        throw new Error('CPA 接口模块未加载,无法同步当前 ChatGPT 会话。');
+      }
+
+      cpaApi = factory({ addLog, fetchImpl });
+      return cpaApi;
+    }
+
+    async function executeStep10(state = {}) {
+      throwIfStopped();
+      await addLog('步骤 10:正在准备同步 ChatGPT session 到 CPA...');
+
+      const mode = getPanelMode(state);
+      if (mode === 'sub2api') {
+        await addLog('步骤 10:当前为 SUB2API 模式,跳过 CPA session 同步。', 'warn');
+        await completeStepFromBackground(10, {
+          cpaSyncSkipped: true,
+          cpaSyncSkipReason: 'sub2api-mode',
+        });
+        return;
+      }
+
+      if (!normalizeString(state.vpsUrl)) {
+        throw new Error('步骤 10:尚未配置 CPA 地址,请先在侧边栏填写。');
+      }
+      if (!normalizeString(state.vpsPassword)) {
+        throw new Error('步骤 10:尚未配置 CPA 管理密钥,请先在侧边栏填写。');
+      }
+
+      await addLog('步骤 10:正在读取 ChatGPT 当前登录 session...');
+      const sessionState = await readCurrentChatGptSession();
+      throwIfStopped();
+
+      await addLog('步骤 10:已读取 ChatGPT session,正在生成 CPA auth JSON 并同步...');
+      const result = await getApi().importCurrentChatGptSession({
+        ...state,
+        session: sessionState.session,
+        accessToken: sessionState.accessToken,
+      }, {
+        logLabel: '步骤 10',
+        timeoutMs: 120000,
+        importTimeoutMs: 120000,
+      });
+
+      await completeStepFromBackground(10, result);
+    }
+
+    async function readCurrentChatGptSession() {
+      try {
+        return await fetchChatGptSessionFromBackground();
+      } catch (error) {
+        await addLog(`步骤 10:后台读取 ChatGPT session 失败,准备打开页面上下文重试:${error.message}`, 'warn');
+      }
+
+      return fetchChatGptSessionFromPage();
+    }
+
+    async function fetchChatGptSessionFromBackground() {
+      const response = await fetchImpl(CHATGPT_SESSION_ENDPOINT, {
+        credentials: 'include',
+        headers: {
+          Accept: 'application/json',
+        },
+      });
+
+      const text = await response.text();
+      let session = null;
+      try {
+        session = text ? JSON.parse(text) : null;
+      } catch {
+        throw new Error(`Session 响应不是有效 JSON:${text.slice(0, 300)}`);
+      }
+
+      if (!response.ok) {
+        throw new Error(`Session 请求失败,HTTP ${response.status}: ${JSON.stringify(session)}`);
+      }
+
+      return normalizeSessionResult({ session, accessToken: session?.accessToken }, '后台');
+    }
+
+    async function fetchChatGptSessionFromPage() {
+      if (!chrome?.tabs || !chrome?.scripting?.executeScript) {
+        throw new Error('当前环境无法在 ChatGPT 页面上下文读取 session。');
+      }
+
+      const tabId = await resolveChatGptSessionTabId();
+      if (!tabId) {
+        throw new Error('未找到可读取 ChatGPT session 的标签页。');
+      }
+
+      await waitForTabComplete(tabId, { timeoutMs: 30000, retryDelayMs: 300 });
+      await sleepWithStop(1000);
+
+      const results = await chrome.scripting.executeScript({
+        target: { tabId },
+        func: fetchSessionInChatGptPage,
+      });
+      const result = results?.[0]?.result;
+      if (!result || result.error) {
+        throw new Error(result?.error || '页面上下文读取 ChatGPT session 失败。');
+      }
+
+      return normalizeSessionResult(result, '页面');
+    }
+
+    async function resolveChatGptSessionTabId() {
+      const registeredSignupTabId = await getTabId('signup-page').catch(() => null);
+      const registeredSignupTab = await getChatGptTabIfReadable(registeredSignupTabId);
+      if (registeredSignupTab?.id) return registeredSignupTab.id;
+
+      const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true }).catch(() => []);
+      const activeMatch = pickPreferredChatGptTab(activeTabs);
+      if (activeMatch?.id) return activeMatch.id;
+
+      const chatGptTabs = await chrome.tabs.query({ url: ['https://chatgpt.com/*'] }).catch(() => []);
+      const existingMatch = pickPreferredChatGptTab(chatGptTabs);
+      if (existingMatch?.id) return existingMatch.id;
+
+      await addLog('步骤 10:未找到 ChatGPT 页面,正在打开 chatgpt.com 以读取 session...', 'info');
+      const created = await chrome.tabs.create({ url: CHATGPT_SESSION_URL, active: true });
+      return created?.id || null;
+    }
+
+    async function getChatGptTabIfReadable(tabId) {
+      const numericTabId = Number(tabId) || 0;
+      if (!numericTabId || !chrome?.tabs?.get) return null;
+
+      const tab = await chrome.tabs.get(numericTabId).catch(() => null);
+      return tab?.id && isChatGptSessionUrl(tab.url) ? tab : null;
+    }
+
+    function pickPreferredChatGptTab(tabs = []) {
+      return (Array.isArray(tabs) ? tabs : [])
+        .filter((tab) => Number.isInteger(tab?.id) && isChatGptSessionUrl(tab.url))
+        .sort((left, right) => {
+          const activeDiff = Number(Boolean(right.active)) - Number(Boolean(left.active));
+          if (activeDiff) return activeDiff;
+          return (Number(right.lastAccessed) || 0) - (Number(left.lastAccessed) || 0);
+        })[0] || null;
+    }
+
+    function isChatGptSessionUrl(url = '') {
+      try {
+        const parsed = new URL(String(url || ''));
+        return /^https?:$/i.test(parsed.protocol)
+          && parsed.hostname.toLowerCase() === 'chatgpt.com';
+      } catch {
+        return false;
+      }
+    }
+
+    function normalizeSessionResult(result = {}, label = 'ChatGPT') {
+      const session = result?.session && typeof result.session === 'object' && !Array.isArray(result.session)
+        ? result.session
+        : null;
+      const accessToken = normalizeString(result?.accessToken || session?.accessToken);
+      if (!session && !accessToken) {
+        throw new Error(`${label} 未返回有效的 ChatGPT session 或 accessToken。`);
+      }
+      if (!accessToken) {
+        throw new Error(`${label} Session 响应中没有 accessToken。`);
+      }
+      return { session, accessToken };
+    }
+
+    return {
+      executeStep10,
+      fetchChatGptSessionFromBackground,
+      isChatGptSessionUrl,
+    };
+  }
+
+  async function fetchSessionInChatGptPage() {
+    try {
+      const response = await fetch('/api/auth/session', {
+        credentials: 'include',
+        headers: {
+          Accept: 'application/json',
+        },
+      });
+      const text = await response.text();
+      let session = null;
+      try {
+        session = text ? JSON.parse(text) : null;
+      } catch {
+        return { error: `Session 响应不是有效 JSON:${text.slice(0, 300)}` };
+      }
+      if (!response.ok) {
+        return { error: `Session 请求失败,HTTP ${response.status}: ${JSON.stringify(session)}` };
+      }
+      return {
+        session,
+        accessToken: session?.accessToken || '',
+      };
+    } catch (error) {
+      return { error: error?.message || String(error || '页面上下文读取 ChatGPT session 失败。') };
+    }
+  }
+
+  function normalizeString(value = '') {
+    return String(value || '').trim();
+  }
+
+  function sleep(ms) {
+    return new Promise((resolve) => setTimeout(resolve, ms));
+  }
+
+  return {
+    createCpaSessionSyncExecutor,
+  };
+});

+ 47 - 0
background/tab-runtime.js

@@ -310,6 +310,52 @@
       }
     }
 
+    async function waitForTabStableComplete(tabId, options = {}) {
+      const {
+        timeoutMs = 30000,
+        retryDelayMs = 300,
+        stableMs = 1000,
+        initialDelayMs = 0,
+      } = options;
+      const start = Date.now();
+      let lastUrl = '';
+      let lastStatus = '';
+      let stableStartedAt = 0;
+      let lastTab = null;
+
+      if (initialDelayMs > 0) {
+        await sleepOrStop(initialDelayMs);
+      }
+
+      while (Date.now() - start < timeoutMs) {
+        throwIfStopped();
+        try {
+          lastTab = await chrome.tabs.get(tabId);
+        } catch {
+          return null;
+        }
+
+        const currentUrl = String(lastTab?.url || '');
+        const currentStatus = String(lastTab?.status || '');
+        if (currentStatus === 'complete') {
+          if (currentUrl !== lastUrl || currentStatus !== lastStatus || !stableStartedAt) {
+            stableStartedAt = Date.now();
+          }
+          if (Date.now() - stableStartedAt >= stableMs) {
+            return lastTab;
+          }
+        } else {
+          stableStartedAt = 0;
+        }
+
+        lastUrl = currentUrl;
+        lastStatus = currentStatus;
+        await sleepOrStop(retryDelayMs);
+      }
+
+      return lastTab;
+    }
+
     async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
       const {
         inject = null,
@@ -787,6 +833,7 @@
       sendToMailContentScriptResilient,
       summarizeMessageResultForDebug,
       waitForTabComplete,
+      waitForTabStableComplete,
       waitForTabUrlFamily,
       waitForTabUrlMatch,
     };

+ 2 - 3
content/activation-utils.js

@@ -23,15 +23,14 @@
     const type = normalizeType(target.type);
     const pathname = normalizePathname(target.pathname);
     const hasForm = Boolean(target.hasForm);
-    const isEmailVerificationRoute = /\/email-verification(?:[/?#]|$)/i.test(pathname);
     const isSubmitButton = hasForm
       && (
         (tagName === 'button' && (!type || type === 'submit'))
         || (tagName === 'input' && type === 'submit')
       );
 
-    if (isSubmitButton && isEmailVerificationRoute) {
-      return { method: 'requestSubmit' };
+    if (isSubmitButton) {
+      return { method: 'click' };
     }
 
     return { method: 'click' };

+ 513 - 0
content/checkout-paypal.js

@@ -0,0 +1,513 @@
+// content/checkout-paypal.js — PayPal page automation for login and checkout
+(function attachCheckoutPaypal() {
+  if (document.documentElement.hasAttribute('data-multipage-checkout-paypal-listener')) return;
+  document.documentElement.setAttribute('data-multipage-checkout-paypal-listener', '');
+
+  let _cleaned = false;
+
+  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+    if (message.type === 'EXECUTE_STEP') {
+      resetStopState();
+      const payload = message.payload || {};
+      if (message.step === 8) {
+        runPaypalLogin(payload).then(
+          (result) => sendResponse(result),
+          (err) => sendResponse({ error: err.message })
+        );
+        return true;
+      }
+      if (message.step === 9) {
+        runPaypalPayment(payload).then(
+          (result) => sendResponse(result),
+          (err) => sendResponse({ error: err.message })
+        );
+        return true;
+      }
+    }
+  });
+
+  // ========== PayPal Login (Step 8) ==========
+  async function runPaypalLogin(payload) {
+    try {
+      throwIfStopped();
+      log('开始执行 PayPal 登录页面自动化...');
+      clearPaypalSession();
+      await sleep(2000);
+
+      const email = payload.email || randEmail();
+      log(`正在填写邮箱: ${email}`);
+      fillById('email', email);
+
+      await sleep(1200);
+      await clickNextButton();
+
+      log('PayPal 登录邮箱已提交');
+      reportComplete(8, { email });
+      return { ok: true };
+    } catch (e) {
+      if (isStopError(e)) throw e;
+      log('PayPal 登录流程出错: ' + e.message, 'error');
+      reportError(8, e.message);
+      return { error: e.message };
+    }
+  }
+
+  // ========== PayPal Checkout (Step 9) ==========
+  async function runPaypalPayment(payload) {
+    try {
+      throwIfStopped();
+      log('开始执行 PayPal 结账页面自动化...');
+      await sleep(2000);
+
+      if (isPaypalHostedReviewPage()) {
+        await clickHostedReviewConsent();
+        log('PayPal 二次确认页已提交');
+        reportComplete(9, { reviewSubmitted: true });
+        return { ok: true, reviewSubmitted: true };
+      }
+
+      // Switch country to US
+      const country = document.getElementById('country');
+      if (country && country.value !== 'US') {
+        log('检测到国家非 US,正在切换...');
+        fillSelect(country, 'US');
+        country.dispatchEvent(new Event('change', { bubbles: true }));
+        await sleep(3000);
+      } else {
+        log('国家已是 US');
+      }
+
+      // Fill card info from payload
+      const card = payload.card || {};
+      const addr = payload.address || {};
+      const email = payload.email || randEmail();
+      const password = payload.password || randPass();
+      const phone = payload.phone || normalizePaypalPhoneForInput(payload.paypalPhone || '+15822201173');
+
+      fillById('email', email);
+      fillById('phone', phone);
+      fillById('cardNumber', card.Credit_Card_Number || '');
+      fillById('cardExpiry', normalizeExpiry(card.Expires || ''));
+      fillById('cardCvv', card.CVV2 || '');
+      fillById('password', password);
+      fillById('firstName', 'James');
+      fillById('lastName', 'Smith');
+      fillById('billingLine1', addr.street || '123 Main St');
+      fillById('billingCity', addr.city || 'New York');
+      fillById('billingPostalCode', (addr.zip || '10001').substring(0, 5));
+      fillSelectById('billingState', addr.state || 'New York');
+
+      log('PayPal 表单已填充完毕');
+
+      await sleep(1200);
+      await clickPaypalSubmit();
+      const postSubmit = await waitForPaypalPostSubmitDecision(payload);
+
+      log('PayPal 结账表单已提交');
+      reportComplete(9, postSubmit);
+      return { ok: true, ...postSubmit };
+    } catch (e) {
+      if (isStopError(e)) throw e;
+      log('PayPal 结账流程出错: ' + e.message, 'error');
+      reportError(9, e.message);
+      return { error: e.message };
+    }
+  }
+
+  // ========== Helpers ==========
+  function fillById(id, val) {
+    const el = document.getElementById(id);
+    if (el) {
+      fillInput(el, val);
+    }
+  }
+
+  function fillSelectById(id, text) {
+    const el = document.getElementById(id);
+    if (!el) return;
+    for (let i = 0; i < el.options.length; i++) {
+      const opt = el.options[i];
+      if (opt.text.toLowerCase().includes(text.toLowerCase()) || opt.value.toLowerCase().includes(text.toLowerCase())) {
+        fillSelect(el, opt.value);
+        return;
+      }
+    }
+  }
+
+  function clearPaypalSession() {
+    if (_cleaned) return;
+    _cleaned = true;
+    try { localStorage.clear(); } catch (e) {}
+    try { sessionStorage.clear(); } catch (e) {}
+    try {
+      const host = window.location.hostname;
+      const domains = [host, '.' + host];
+      const parts = host.split('.');
+      for (let i = 1; i < parts.length - 1; i++) {
+        domains.push('.' + parts.slice(i).join('.'));
+      }
+      const paths = ['/', window.location.pathname];
+      const cookies = document.cookie ? document.cookie.split(';') : [];
+      cookies.forEach((c) => {
+        const name = c.split('=')[0].trim();
+        if (!name) return;
+        paths.forEach((p) => {
+          domains.forEach((d) => {
+            document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=' + p + '; domain=' + d;
+          });
+          document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=' + p;
+        });
+      });
+      log('已清理 PayPal 前端 cookie / storage');
+    } catch (e) {
+      log('清理 cookie 时出错: ' + e.message, 'warn');
+    }
+  }
+
+  function randEmail() {
+    const c = 'abcdefghijklmnopqrstuvwxyz0123456789';
+    let e = '';
+    for (let i = 0; i < 16; i++) e += c[Math.floor(Math.random() * c.length)];
+    return e + '@gmail.com';
+  }
+
+  function randPass() {
+    const L = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+    const D = '0123456789';
+    const S = '!@#$%^';
+    const A = L + D + S;
+    let p = L[Math.floor(Math.random() * 26)] + L[26 + Math.floor(Math.random() * 26)] + D[Math.floor(Math.random() * 10)] + S[Math.floor(Math.random() * 6)];
+    for (let i = 4; i < 14; i++) p += A[Math.floor(Math.random() * A.length)];
+    return p.split('').sort(() => Math.random() - 0.5).join('');
+  }
+
+  function normalizePaypalPhoneForInput(value = '') {
+    const raw = String(value || '').trim();
+    const digits = raw.replace(/\D+/g, '');
+    if (digits.length === 11 && digits.startsWith('1')) {
+      return digits.slice(1);
+    }
+    return digits || raw;
+  }
+
+  function normalizeExpiry(exp) {
+    if (!exp) return '';
+    const m = String(exp).match(/^(\d{1,2})\s*\/\s*(\d{2,4})$/);
+    if (!m) return exp;
+    const mm = m[1].padStart(2, '0');
+    const yy = m[2].length === 4 ? m[2].slice(2) : m[2];
+    return mm + ' / ' + yy;
+  }
+
+  function isPaypalHostedReviewPage() {
+    const path = String(location.pathname || '');
+    const text = document.body?.innerText || '';
+    return /\/webapps\/hermes/i.test(path)
+      || Boolean(document.getElementById('consentButton'))
+      || /set up once\. pay faster next time|agree and continue/i.test(text);
+  }
+
+  async function waitForPaypalPostSubmitDecision(payload = {}, timeoutMs = 60000) {
+    const startedAt = Date.now();
+    const startUrl = location.href;
+    while (Date.now() - startedAt < timeoutMs) {
+      throwIfStopped();
+      if (isPaypalHostedReviewPage()) {
+        await clickHostedReviewConsent();
+        return { reviewSubmitted: true };
+      }
+      if (isPaypalSmsVerificationPage()) {
+        const result = await completePaypalSmsVerification(payload);
+        return { smsVerified: true, ...result };
+      }
+      if (!/paypal\./i.test(String(location.host || ''))) {
+        return { leftPayPal: true };
+      }
+      if (location.href !== startUrl && /return|success|complete/i.test(location.href)) {
+        return { redirected: true };
+      }
+      await sleep(1000);
+    }
+    log('提交后未检测到 PayPal 二次确认页或外部跳转,按已提交继续。', 'warn');
+    return { postSubmitTimeout: true };
+  }
+
+  function isPaypalSmsVerificationPage() {
+    return Boolean(findPaypalSmsCodeInput())
+      || Boolean(findPaypalSplitSmsCodeInputs().length >= 4)
+      || /verification\s*code|confirm.{0,40}phone|verify.{0,40}phone|sent.{0,40}code|text\s*message|短信|验证码|安全代码/i.test(document.body?.innerText || '');
+  }
+
+  async function completePaypalSmsVerification(payload = {}) {
+    await clickPaypalSmsSendCodeButtonIfPresent();
+    const inputState = await waitForPaypalSmsCodeInput(20000);
+    if (!inputState) {
+      throw new Error('PayPal 短信验证码页面已出现,但未找到验证码输入框。');
+    }
+
+    const code = await requestPaypalSmsCode(payload);
+    fillPaypalSmsCodeInput(inputState, code);
+    await sleep(500);
+    const submitButton = findPaypalSmsSubmitButton();
+    if (!submitButton) {
+      throw new Error('PayPal 短信验证码已填写,但未找到继续/验证按钮。');
+    }
+    submitButton.click();
+    log('PayPal 短信验证码已填写并提交。');
+    await sleep(2500);
+    return { smsCodeSubmitted: true };
+  }
+
+  async function requestPaypalSmsCode(payload = {}) {
+    const response = await chrome.runtime.sendMessage({
+      type: 'PAYPAL_FETCH_SMS_CODE',
+      source: 'checkout-paypal',
+      payload: {
+        phone: payload.paypalPhone || payload.phone || '+15822201173',
+        smsApiUrl: payload.paypalSmsApiUrl || '',
+      },
+    });
+    if (response?.error) {
+      throw new Error(response.error);
+    }
+    if (!response?.code) {
+      throw new Error('PayPal 短信收码接口未返回验证码。');
+    }
+    log(`PayPal 短信验证码已获取(第 ${response.attempt || 1} 次轮询)。`);
+    return String(response.code);
+  }
+
+  async function clickPaypalSmsSendCodeButtonIfPresent() {
+    const button = findPaypalButtonByPatterns([
+      /send\s*(?:me\s*)?(?:a\s*)?code/i,
+      /text\s*(?:me|code)/i,
+      /resend/i,
+      /发送.*验证码|短信|重新发送/i,
+    ]);
+    if (button) {
+      button.click();
+      log('已点击 PayPal 发送/重新发送短信验证码按钮。');
+      await sleep(1200);
+    }
+  }
+
+  async function waitForPaypalSmsCodeInput(timeoutMs = 20000) {
+    const startedAt = Date.now();
+    while (Date.now() - startedAt < timeoutMs) {
+      throwIfStopped();
+      const input = findPaypalSmsCodeInput();
+      if (input) {
+        return { input, splitInputs: [] };
+      }
+      const splitInputs = findPaypalSplitSmsCodeInputs();
+      if (splitInputs.length >= 4) {
+        return { input: null, splitInputs };
+      }
+      await sleep(400);
+    }
+    return null;
+  }
+
+  function fillPaypalSmsCodeInput(inputState, code) {
+    const normalizedCode = String(code || '').replace(/\D+/g, '');
+    if (!normalizedCode) {
+      throw new Error('PayPal 短信验证码为空。');
+    }
+    if (inputState.input) {
+      fillInput(inputState.input, normalizedCode);
+      return;
+    }
+    inputState.splitInputs.forEach((input, index) => {
+      if (normalizedCode[index]) {
+        fillInput(input, normalizedCode[index]);
+      }
+    });
+  }
+
+  function findPaypalSmsCodeInput() {
+    const selectors = [
+      'input[autocomplete="one-time-code"]',
+      'input[name*="code" i]',
+      'input[id*="code" i]',
+      'input[aria-label*="code" i]',
+      'input[placeholder*="code" i]',
+      'input[inputmode="numeric"]',
+      'input[maxlength="6"]',
+    ];
+    const candidates = selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector)));
+    return candidates.find((input) => {
+      if (!isClickable(input)) return false;
+      const text = getPaypalFieldText(input);
+      if (/card|cvv|cvc|postal|zip|phone|tel|amount|卡|邮编|电话/i.test(text)) {
+        return false;
+      }
+      return /code|otp|security|verification|验证码|安全/i.test(text)
+        || String(input.getAttribute('maxlength') || '') === '6'
+        || String(input.getAttribute('autocomplete') || '').toLowerCase() === 'one-time-code';
+    }) || null;
+  }
+
+  function findPaypalSplitSmsCodeInputs() {
+    return Array.from(document.querySelectorAll('input'))
+      .filter((input) => {
+        if (!isClickable(input)) return false;
+        const maxLength = Number(input.getAttribute('maxlength') || input.maxLength || 0);
+        const text = getPaypalFieldText(input);
+        return maxLength === 1
+          && /code|otp|security|verification|验证码|安全/i.test(text);
+      })
+      .slice(0, 8);
+  }
+
+  function findPaypalSmsSubmitButton() {
+    return findPaypalButtonByPatterns([
+      /continue/i,
+      /verify/i,
+      /confirm/i,
+      /submit/i,
+      /next/i,
+      /继续|验证|确认|提交|下一步/i,
+    ]);
+  }
+
+  function findPaypalButtonByPatterns(patterns) {
+    return Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"]')).find((button) => {
+      if (!isClickable(button)) return false;
+      const text = String(button.textContent || button.value || button.getAttribute?.('aria-label') || '').trim();
+      return patterns.some((pattern) => pattern.test(text));
+    }) || null;
+  }
+
+  function getPaypalFieldText(el) {
+    if (!el) return '';
+    const id = el.id ? String(el.id) : '';
+    const labelText = id
+      ? Array.from(document.querySelectorAll(`label[for="${cssEscape(id)}"]`)).map((label) => label.textContent || '').join(' ')
+      : '';
+    return [
+      id,
+      el.name,
+      el.getAttribute?.('autocomplete'),
+      el.getAttribute?.('aria-label'),
+      el.getAttribute?.('placeholder'),
+      labelText,
+      el.closest?.('label')?.textContent || '',
+      el.closest?.('[data-testid], [class], div, section, fieldset')?.textContent || '',
+    ].filter(Boolean).join(' ');
+  }
+
+  function cssEscape(value) {
+    if (window.CSS?.escape) return window.CSS.escape(value);
+    return String(value || '').replace(/["\\]/g, '\\$&');
+  }
+
+  async function clickHostedReviewConsent() {
+    log(`PayPal Hermes:开始等待账单确认按钮。当前 URL:${location.href}`, 'info');
+    let waited = 0;
+    while (waited < 30) {
+      throwIfStopped();
+      waited += 1;
+      const button = findHostedReviewConsentButton();
+      if (button) {
+        log('PayPal Hermes:已找到确认按钮,准备点击 Agree and Continue。', 'info');
+        button.click();
+        await sleep(1000);
+        return true;
+      }
+      if (waited === 1 || waited % 5 === 0) {
+        log(`PayPal Hermes:尚未找到确认按钮,继续等待(${waited}/30)。`, 'info');
+      }
+      await sleep(1000);
+    }
+    throw new Error('PayPal hosted checkout 二次确认页超时,未找到确认按钮。');
+  }
+
+  function findHostedReviewConsentButton() {
+    const direct = document.getElementById('consentButton')
+      || document.querySelector('button[data-testid="consentButton"]');
+    if (direct && isClickable(direct)) return direct;
+
+    const patterns = [
+      /agree\s*(?:and|&)\s*continue/i,
+      /continue/i,
+      /pay\s*now/i,
+      /同意|继续|付款/i,
+    ];
+    return Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"]')).find((button) => {
+      if (!isClickable(button)) return false;
+      const text = String(button.textContent || button.value || button.getAttribute?.('aria-label') || '').trim();
+      return patterns.some((pattern) => pattern.test(text));
+    }) || null;
+  }
+
+  function isClickable(el) {
+    if (!el || el.disabled) return false;
+    const rect = el.getBoundingClientRect();
+    const style = window.getComputedStyle(el);
+    return rect.width > 0
+      && rect.height > 0
+      && style.visibility !== 'hidden'
+      && style.display !== 'none';
+  }
+
+  async function clickPaypalSubmit(retries = 0) {
+    throwIfStopped();
+    if (retries >= 12) throw new Error('未找到 PayPal 提交按钮,已超时');
+
+    const btn = document.querySelector('button[data-testid="submit-button"]')
+      || document.querySelector('button[data-testid="hosted-payment-submit-button"]');
+    if (btn) {
+      const rect = btn.getBoundingClientRect();
+      if (btn.disabled || rect.height === 0) {
+        log('PayPal 提交按钮被禁用或不可见,等待中...');
+        await sleep(800);
+        return clickPaypalSubmit(retries + 1);
+      }
+      log(`正在点击 PayPal 提交: ${btn.textContent.trim()}`);
+      btn.click();
+    } else {
+      const all = document.querySelectorAll('button');
+      for (let i = 0; i < all.length; i++) {
+        const t = all[i].textContent.trim();
+        if (['Agree & Create Account', 'Agree and Pay', 'Continue', 'Pay Now'].includes(t) || t.includes('同意')) {
+          all[i].click();
+          log(`已点击: ${t}`);
+          return;
+        }
+      }
+      log(`未找到提交按钮,重试中... (${retries + 1})`);
+      await sleep(800);
+      return clickPaypalSubmit(retries + 1);
+    }
+  }
+
+  async function clickNextButton(retries = 0) {
+    throwIfStopped();
+    if (retries >= 12) throw new Error('未找到 PayPal 下一步按钮,已超时');
+
+    const all = document.querySelectorAll('button');
+    for (let i = 0; i < all.length; i++) {
+      const t = all[i].textContent.trim();
+      if (['下一页', '下一步', 'Next', 'Continue'].includes(t)) {
+        if (!all[i].disabled && all[i].getBoundingClientRect().height > 0) {
+          all[i].click();
+          log(`已点击: ${t}`);
+          return;
+        }
+      }
+    }
+    log(`未找到"下一步"按钮,重试中... (${retries + 1})`);
+    await sleep(800);
+    return clickNextButton(retries + 1);
+  }
+
+  if (isPaypalHostedReviewPage()) {
+    setTimeout(() => {
+      clickHostedReviewConsent().catch((error) => {
+        log(`PayPal Hermes 自动确认失败: ${error?.message || error}`, 'warn');
+      });
+    }, 0);
+  }
+
+  document.documentElement.setAttribute('data-multipage-checkout-paypal-ready', '');
+})();

+ 1130 - 0
content/checkout-stripe.js

@@ -0,0 +1,1130 @@
+// content/checkout-stripe.js — Stripe checkout page automation for ChatGPT Plus subscription
+(function attachCheckoutStripe() {
+  if (document.documentElement.hasAttribute('data-multipage-checkout-stripe-listener')) return;
+  document.documentElement.setAttribute('data-multipage-checkout-stripe-listener', '');
+
+  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+    if (message.type === 'EXECUTE_STEP' && message.step === 7) {
+      resetStopState();
+      runStripeCheckout(message.payload || {}).then(
+        (result) => sendResponse(result),
+        (err) => sendResponse({ error: err.message })
+      );
+      return true;
+    }
+
+    if (message.type === 'CHECKOUT_STRIPE_SELECT_ADDRESS_SUGGESTION') {
+      resetStopState();
+      selectGoogleAddressSuggestionOnly(message.payload || {}).then(
+        (result) => sendResponse(result),
+        (err) => sendResponse({ error: err.message })
+      );
+      return true;
+    }
+
+    if (message.type === 'CHECKOUT_STRIPE_GET_STATE') {
+      sendResponse(inspectCheckoutStripeState());
+      return false;
+    }
+
+    if (message.type === 'CHECKOUT_STRIPE_SELECT_PAYPAL') {
+      resetStopState();
+      selectPayPalPaymentMethod(message.payload || {}).then(
+        (result) => sendResponse(result),
+        (err) => sendResponse({ error: err.message })
+      );
+      return true;
+    }
+
+    if (message.type === 'CHECKOUT_STRIPE_FILL_BILLING_ADDRESS') {
+      resetStopState();
+      fillStripeBillingAddress(message.payload || {}).then(
+        (result) => sendResponse(result),
+        (err) => sendResponse({ error: err.message })
+      );
+      return true;
+    }
+
+    if (message.type === 'CHECKOUT_STRIPE_CLICK_SUBMIT') {
+      resetStopState();
+      clickStripeCheckoutSubmit(message.payload || {}).then(
+        (result) => sendResponse(result),
+        (err) => sendResponse({ error: err.message })
+      );
+      return true;
+    }
+  });
+
+  async function runStripeCheckout(payload) {
+    try {
+      throwIfStopped();
+      log('开始执行 Stripe 结账页面自动化...');
+      await sleep(1800);
+
+      // 1. Click PayPal option
+      await selectPayPalPaymentMethod({ relaxedActivation: true });
+
+      await sleep(1000);
+
+      // 2. Fill billing address
+      await fillStripeBillingAddress(payload);
+
+      // 3. Click submit
+      await clickStripeCheckoutSubmit({ beforeClickDelayMs: 1500 });
+
+      log('Stripe 结账表单已提交');
+      reportComplete(7, {});
+      return { ok: true };
+    } catch (e) {
+      if (isStopError(e)) throw e;
+      log('Stripe 结账流程出错: ' + e.message, 'error');
+      reportError(7, e.message);
+      return { error: e.message };
+    }
+  }
+
+  function inspectCheckoutStripeState() {
+    const addressFields = getStructuredAddressFields();
+    return {
+      url: location.href,
+      readyState: document.readyState,
+      hasPayPal: Boolean(findPayPalPaymentMethodTarget()),
+      paypalCandidates: getPayPalCandidateSummaries(),
+      billingFieldsVisible: hasBillingAddressFields(addressFields),
+      hasSubmitButton: Boolean(findSubmitButton()),
+      addressFieldValues: {
+        address1: addressFields.address1?.value || '',
+        city: addressFields.city?.value || '',
+        region: addressFields.region?.value || getSelectText(addressFields.regionSelect) || '',
+        postalCode: addressFields.postalCode?.value || '',
+      },
+    };
+  }
+
+  async function selectPayPalPaymentMethod(options = {}) {
+    log('正在寻找 PayPal 选项...');
+    const autoJsTarget = findAutoJsPayPalTarget();
+    if (autoJsTarget) {
+      await clickPayPalLikeAutoJs(autoJsTarget);
+      await sleep(450);
+      await clickPayPalLikeAutoJs(autoJsTarget);
+      const autoJsActive = await waitForPayPalPaymentMethodActive(2500);
+      if (autoJsActive) {
+        log('已按 auto.js 方式确认 PayPal 选项生效');
+      } else {
+        log('auto.js 方式点击 PayPal 后未观察到标准选中标记,继续按 hosted checkout 宽松模式执行。', 'warn');
+      }
+      return {
+        paymentSelected: autoJsActive,
+        relaxed: !autoJsActive,
+      };
+    }
+
+    const target = await waitForPayPalPaymentMethodTarget(10000);
+    if (!target) {
+      if (options.relaxedActivation) {
+        log('未找到 PayPal 选项按钮,当前为宽松模式,继续执行...', 'warn');
+        return { paymentSelected: false, relaxed: true };
+      }
+      throw new Error('未找到 PayPal 付款方式,无法切换。');
+    }
+
+    const clickTargets = getPayPalActivationTargets(target);
+    for (let attempt = 0; attempt < 2; attempt += 1) {
+      for (const candidate of clickTargets) {
+        dispatchRobustActivation(candidate);
+        await sleep(220);
+        if (hasSelectedPayPalControl() || hasBillingAddressFields()) {
+          break;
+        }
+      }
+      await sleep(450);
+    }
+
+    const active = await waitForPayPalPaymentMethodActive(3500);
+    if (!active) {
+      log('点击 PayPal 后未观察到标准选中标记,继续按 hosted checkout 宽松模式执行。', 'warn');
+    } else {
+      log('已确认 PayPal 选项生效');
+    }
+    return {
+      paymentSelected: active,
+      relaxed: !active,
+    };
+  }
+
+  async function fillStripeBillingAddress(payload = {}) {
+    const addr = normalizeCheckoutAddress(payload.address || {});
+    if (payload.autoJsDirectSelectors) {
+      log('参考 auto.js 直接填写账单字段...');
+      fillBillingAddressDirectSelectors(addr);
+      await sleep(800);
+      hideAutocompleteDropdowns();
+      await ensureTermsCheckbox();
+
+      const latest = getStructuredAddressFields();
+      return {
+        countryText: readCountryText(),
+        selectedAutocompleteAddress: false,
+        structuredAddress: {
+          address1: latest.address1?.value || '',
+          city: latest.city?.value || '',
+          region: latest.region?.value || getSelectText(latest.regionSelect) || '',
+          postalCode: latest.postalCode?.value || '',
+        },
+      };
+    }
+
+    log('正在填写账单地址...');
+    const selectedAutocompleteAddress = await fillBillingAddressLine1FromGoogle(addr);
+    if (!selectedAutocompleteAddress) {
+      log('未能选择 Google 地址下拉项,改用手动填写地址兜底。', 'warn');
+      const fields = getStructuredAddressFields();
+      const address1Input = fields.address1 || findAddressSearchInput();
+      if (address1Input) {
+        fillInput(address1Input, addr.street);
+      } else {
+        fillBySelector('#billingAddressLine1', addr.street);
+      }
+    }
+    await sleep(900);
+    const fields = getStructuredAddressFields();
+    fillMissingControl(fields.city || document.querySelector('#billingLocality'), addr.city);
+    fillMissingControl(fields.postalCode || document.querySelector('#billingPostalCode'), addr.zip.substring(0, 5));
+    fillRegionControlByText(fields.regionSelect || fields.region || document.querySelector('#billingAdministrativeArea'), addr.state);
+    await sleep(800);
+    hideAutocompleteDropdowns();
+    await ensureTermsCheckbox();
+
+    const latest = getStructuredAddressFields();
+    return {
+      countryText: readCountryText(),
+      selectedAutocompleteAddress,
+      structuredAddress: {
+        address1: latest.address1?.value || '',
+        city: latest.city?.value || '',
+        region: latest.region?.value || getSelectText(latest.regionSelect) || '',
+        postalCode: latest.postalCode?.value || '',
+      },
+    };
+  }
+
+  async function clickStripeCheckoutSubmit(payload = {}) {
+    await ensureTermsCheckbox();
+    await sleep(Math.max(0, Math.floor(Number(payload.beforeClickDelayMs) || 0)));
+    await clickSubmitButton();
+    return { clicked: true };
+  }
+
+  function normalizeCheckoutAddress(addr = {}) {
+    return {
+      street: normalizeText(addr.street || addr.address1 || '123 Main St'),
+      city: normalizeText(addr.city || 'New York'),
+      state: normalizeText(addr.state || addr.region || 'New York'),
+      zip: normalizeText(addr.zip || addr.postalCode || '10001').substring(0, 5),
+      query: normalizeText(addr.query || addr.autocompleteQuery || ''),
+    };
+  }
+
+  function getStructuredAddressFields() {
+    const address1 = getVisibleElementById('billingAddressLine1') || findInputByFieldText([
+      /address\s*(?:line)?\s*1|address[_-]?line[_-]?1|address\[(?:address_)?line1\]|line\s*1|street|street[_-]?address/i,
+      /地址\s*1|街道|详细地址|住所/i,
+    ], {
+      exclude: (input) => isNonAddressSearchInput(input),
+    }) || findAddressSearchInput();
+    const city = getVisibleElementById('billingLocality') || findInputByFieldText([
+      /city|town|suburb|locality|address[_-]?level[_-]?2|address\[city\]/i,
+      /城市|市区|区市町村|市区町村|市町村/i,
+    ]);
+    const postalCode = getVisibleElementById('billingPostalCode') || findInputByFieldText([
+      /postal|zip|postcode|postal[_-]?code|zip[_-]?code|address\[postal_code\]/i,
+      /邮编|邮政|郵便番号/i,
+    ]);
+    const regionSelect = getVisibleElementById('billingAdministrativeArea');
+    const region = regionSelect || findInputByFieldText([
+      /state|province|region|county|prefecture|administrative|administrative[_-]?area|address[_-]?level[_-]?1|address\[state\]/i,
+      /省|州|地区|辖区|都道府县|都道府県/i,
+    ]);
+
+    return {
+      address1,
+      city,
+      postalCode,
+      region,
+      regionSelect: regionSelect && regionSelect.tagName === 'SELECT' ? regionSelect : null,
+    };
+  }
+
+  function getVisibleElementById(id) {
+    const el = document.getElementById(id);
+    return el && isVisibleNode(el) ? el : null;
+  }
+
+  function hasBillingAddressFields(fields = getStructuredAddressFields()) {
+    if (fields?.address1 || fields?.city || fields?.postalCode) {
+      return true;
+    }
+    return getVisibleTextInputs().some((input) => {
+      const text = getFieldText(input);
+      return /address|street|billing|line\s*1|地址|街道|账单/i.test(text)
+        && !/card\s*number|card|expiry|expiration|security|cvc|cvv|name|email|e-mail|phone|tel|country|region|postal|zip|city|state|province|银行卡|卡号|有效期|安全码|姓名|邮箱|电话|国家|地区|邮编|城市|省|州/i.test(text);
+    }) || Boolean(findAddressSearchInput());
+  }
+
+  function readCountryText() {
+    const country = document.getElementById('billingCountry')
+      || findInputByFieldText([/country|region/i, /国家|地区/i])
+      || Array.from(document.querySelectorAll('select')).find((select) => (
+        isVisibleNode(select) && /country|region|国家|地区/i.test(getFieldText(select))
+      ));
+    if (!country) return '';
+    if (country.tagName === 'SELECT') {
+      return getSelectText(country) || country.value || '';
+    }
+    return country.value || country.textContent || '';
+  }
+
+  function getSelectText(select) {
+    if (!select || select.tagName !== 'SELECT') return '';
+    return normalizeText(select.selectedOptions?.[0]?.textContent || select.value || '');
+  }
+
+  function findInputByFieldText(patterns = [], options = {}) {
+    const excluded = options.exclude || (() => false);
+    return getVisibleTextInputs().find((input) => {
+      if (excluded(input)) return false;
+      return patterns.some((pattern) => pattern.test(getFieldText(input)));
+    }) || null;
+  }
+
+  function getVisibleControls(selector) {
+    return Array.from(document.querySelectorAll(selector)).filter((el) => isVisibleNode(el));
+  }
+
+  function getVisibleTextInputs() {
+    return getVisibleControls('input, textarea')
+      .filter((el) => {
+        const type = String(el.getAttribute('type') || el.type || '').trim().toLowerCase();
+        return !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
+      });
+  }
+
+  function getVisibleFormControls() {
+    return Array.from(document.querySelectorAll('input, textarea, select'))
+      .filter((el) => {
+        const type = String(el.getAttribute('type') || el.type || '').toLowerCase();
+        return type !== 'hidden' && isVisibleNode(el);
+      });
+  }
+
+  function getFieldText(el) {
+    if (!el) return '';
+    const id = el.id ? String(el.id) : '';
+    const labelText = id
+      ? Array.from(document.querySelectorAll(`label[for="${cssEscape(id)}"]`)).map((label) => label.textContent || '').join(' ')
+      : '';
+    const wrappingLabel = el.closest?.('label')?.textContent || '';
+    const container = el.closest?.('[data-testid], [class], div, section, fieldset');
+    return normalizeText([
+      id,
+      el.name,
+      el.getAttribute?.('autocomplete'),
+      el.getAttribute?.('aria-label'),
+      el.getAttribute?.('placeholder'),
+      labelText,
+      wrappingLabel,
+      container && !isDocumentLevelContainer(container) ? container.textContent || '' : '',
+    ].filter(Boolean).join(' '));
+  }
+
+  function getDirectFieldHintText(el) {
+    if (!el) return '';
+    const id = el.id ? String(el.id) : '';
+    const labelText = id
+      ? Array.from(document.querySelectorAll(`label[for="${cssEscape(id)}"]`)).map((label) => label.textContent || '').join(' ')
+      : '';
+    const wrappingLabel = el.closest?.('label')?.textContent || '';
+    return normalizeText([
+      id,
+      el.name,
+      el.getAttribute?.('autocomplete'),
+      el.getAttribute?.('aria-label'),
+      el.getAttribute?.('placeholder'),
+      labelText,
+      wrappingLabel,
+    ].filter(Boolean).join(' '));
+  }
+
+  function isNonAddressSearchInput(input) {
+    const directText = getDirectFieldHintText(input);
+    const type = String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase();
+    return /name|email|e-mail|phone|tel|password|coupon|promo|country|region|postal|zip|city|state|province|card|card\s*number|expiry|expiration|security|cvc|cvv|cc-|全名|姓名|邮箱|电话|密码|国家|地区|邮编|城市|省|州|银行卡|卡号|有效期|安全码/i.test(directText)
+      || ['email', 'tel', 'password'].includes(type);
+  }
+
+  function isLikelyAddressSearchInput(input) {
+    const text = getFieldText(input);
+    if (isNonAddressSearchInput(input)) {
+      return false;
+    }
+    if (/name|email|e-mail|phone|tel|password|coupon|promo|country|region|postal|zip|city|state|province|card|card\s*number|expiry|expiration|security|cvc|cvv|cc-|全名|姓名|邮箱|电话|密码|国家|地区|邮编|城市|省|州|银行卡|卡号|有效期|安全码/i.test(text)) {
+      return false;
+    }
+    return /address|street|billing|search|line\s*1|地址|街道|账单/i.test(text);
+  }
+
+  function findAddressSearchInput() {
+    const direct = findInputByFieldText([
+      /address|street|billing|search|line\s*1/i,
+      /地址|街道|账单/i,
+    ], {
+      exclude: (input) => isNonAddressSearchInput(input)
+        || /city|state|province|postal|zip|country|region|城市|省|州|邮编|国家|地区/i.test(getFieldText(input)),
+    });
+    if (direct) return direct;
+    return getVisibleTextInputs().filter(isLikelyAddressSearchInput)[0] || null;
+  }
+
+  function isDocumentLevelContainer(el) {
+    return !el
+      || el === document.documentElement
+      || el === document.body
+      || ['HTML', 'BODY', 'MAIN'].includes(el.tagName);
+  }
+
+  function cssEscape(value) {
+    if (window.CSS?.escape) return window.CSS.escape(value);
+    return String(value || '').replace(/["\\]/g, '\\$&');
+  }
+
+  function findPayPalPaymentMethodTarget() {
+    const directSelectors = [
+      '[data-testid="paypal-accordion-item-button"]',
+      '[data-testid*="paypal" i]',
+      '.paypal-accordion-item button',
+      'button[aria-label*="PayPal" i]',
+      '[aria-label*="PayPal" i]',
+      '[title*="PayPal" i]',
+      '[role="radio"][aria-label*="PayPal" i]',
+      'input[type="radio"][value*="paypal" i]',
+      'button[value*="paypal" i]',
+    ];
+
+    for (const selector of directSelectors) {
+      const target = Array.from(document.querySelectorAll(selector)).find((el) => isVisibleNode(el));
+      if (target) return target;
+    }
+
+    const directClickable = findClickableByText([/paypal/i]);
+    if (directClickable) return directClickable;
+
+    const radios = getVisibleControls('input[type="radio"], [role="radio"]');
+    const matchedRadio = radios.find((el) => /paypal/i.test(getCombinedSearchText(el)));
+    if (matchedRadio) return matchedRadio;
+
+    for (const candidate of getPayPalSearchCandidates()) {
+      const interactive = findInteractiveAncestor(candidate);
+      if (interactive && /paypal/i.test(getCombinedSearchText(interactive))) {
+        return interactive;
+      }
+      const card = findPaymentCardAncestor(candidate, /paypal/i);
+      if (card) {
+        return card;
+      }
+    }
+
+    return null;
+  }
+
+  function getPayPalCandidateSummaries() {
+    return getPayPalSearchCandidates()
+      .slice(0, 8)
+      .map((el) => ({
+        tag: el.tagName,
+        id: el.id || '',
+        role: el.getAttribute?.('role') || '',
+        text: normalizeText(getCombinedSearchText(el)).slice(0, 120),
+        visible: isVisibleNode(el),
+        checked: el.checked === true || el.getAttribute?.('aria-checked') === 'true',
+      }));
+  }
+
+  function findClickableByText(patterns = []) {
+    const candidates = getVisibleControls('button, a, [role="button"], [role="radio"], [role="tab"], input[type="button"], input[type="submit"], input[type="radio"], label, [tabindex]');
+    return candidates.find((el) => {
+      const text = getCombinedSearchText(el);
+      return patterns.some((pattern) => pattern.test(text));
+    }) || null;
+  }
+
+  function getPayPalActivationTargets(target) {
+    const candidates = [];
+    const push = (el) => {
+      if (!el || !isVisibleNode(el) || isDocumentLevelContainer(el)) return;
+      if (!candidates.includes(el)) candidates.push(el);
+    };
+
+    push(target);
+    push(target?.querySelector?.('input[type="radio"], [role="radio"], button, [role="button"]'));
+    push(target?.closest?.('input[type="radio"], [role="radio"], button, [role="button"], label, [tabindex]'));
+    push(findInteractiveAncestor(target));
+    push(findPaymentCardAncestor(target, /paypal/i));
+
+    let current = target;
+    for (let depth = 0; current && depth < 7; depth += 1, current = current.parentElement) {
+      if (isDocumentLevelContainer(current)) break;
+      if (/paypal/i.test(getCombinedSearchText(current))) {
+        push(current.querySelector?.('input[type="radio"], [role="radio"], button, [role="button"]'));
+        if (isPaymentCardSized(current)) push(current);
+        if (current.matches?.('button, [role="button"], [role="radio"], label, [tabindex]')) push(current);
+      }
+    }
+
+    for (const candidate of getPayPalSearchCandidates().slice(0, 8)) {
+      push(candidate.closest?.('button, [role="button"], [role="radio"], label, [tabindex]'));
+      push(findInteractiveAncestor(candidate));
+      push(findPaymentCardAncestor(candidate, /paypal/i));
+    }
+
+    return candidates.slice(0, 10);
+  }
+
+  function getPayPalSearchCandidates() {
+    const selector = [
+      'button',
+      'a',
+      'label',
+      '[role="button"]',
+      '[role="radio"]',
+      '[role="tab"]',
+      'input[type="radio"]',
+      '[tabindex]',
+      '[data-testid]',
+      '[aria-label]',
+      '[title]',
+      'img',
+      'svg',
+      'span',
+      'div',
+    ].join(', ');
+
+    return getVisibleControls(selector)
+      .filter((el) => /paypal/i.test(getCombinedSearchText(el)))
+      .sort((left, right) => {
+        const leftRect = left.getBoundingClientRect();
+        const rightRect = right.getBoundingClientRect();
+        return (leftRect.width * leftRect.height) - (rightRect.width * rightRect.height);
+      });
+  }
+
+  function findInteractiveAncestor(el) {
+    let current = el;
+    for (let depth = 0; current && depth < 8; depth += 1, current = current.parentElement) {
+      if (!isVisibleNode(current) || isDocumentLevelContainer(current)) continue;
+      if (current.matches?.('button, a, label, [role="button"], [role="radio"], [role="tab"], input[type="radio"], [tabindex]')) {
+        return current;
+      }
+    }
+    return null;
+  }
+
+  function isPaymentCardSized(el) {
+    if (!isVisibleNode(el) || isDocumentLevelContainer(el)) return false;
+    const rect = el.getBoundingClientRect();
+    const maxWidth = Math.max(320, Math.min(window.innerWidth * 0.95, 900));
+    const maxHeight = Math.max(140, Math.min(window.innerHeight * 0.45, 340));
+    return rect.width >= 64
+      && rect.height >= 28
+      && rect.width <= maxWidth
+      && rect.height <= maxHeight;
+  }
+
+  function findPaymentCardAncestor(el, pattern) {
+    let current = el;
+    for (let depth = 0; current && depth < 8; depth += 1, current = current.parentElement) {
+      if (!isVisibleNode(current)) continue;
+      if (isDocumentLevelContainer(current)) break;
+      const text = getCombinedSearchText(current);
+      if (pattern.test(text) && isPaymentCardSized(current)) {
+        return current;
+      }
+    }
+    return null;
+  }
+
+  function getCombinedSearchText(el) {
+    if (!el) return '';
+    return [
+      el.textContent,
+      el.getAttribute?.('aria-label'),
+      el.getAttribute?.('data-testid'),
+      el.id,
+      el.name,
+      el.value,
+      el.className && typeof el.className === 'string' ? el.className : '',
+    ].filter(Boolean).join(' ');
+  }
+
+  async function waitForPayPalPaymentMethodTarget(timeoutMs = 10000) {
+    const startedAt = Date.now();
+    while (Date.now() - startedAt < timeoutMs) {
+      throwIfStopped();
+      const target = findPayPalPaymentMethodTarget();
+      if (target) return target;
+      await sleep(250);
+    }
+    return null;
+  }
+
+  async function waitForPayPalPaymentMethodActive(timeoutMs = 3500) {
+    const startedAt = Date.now();
+    while (Date.now() - startedAt < timeoutMs) {
+      throwIfStopped();
+      if (hasSelectedPayPalControl()) return true;
+      await sleep(250);
+    }
+    return false;
+  }
+
+  function hasSelectedPayPalControl() {
+    const target = findPayPalPaymentMethodTarget();
+    let current = target;
+    for (let depth = 0; current && depth < 6; depth += 1, current = current.parentElement) {
+      if (isDocumentLevelContainer(current)) break;
+      if (/paypal/i.test(getCombinedSearchText(current)) && hasPaymentMethodSelectionMarker(current)) {
+        return true;
+      }
+
+      const radio = current.querySelector?.('input[type="radio"], [role="radio"]');
+      if (
+        radio
+        && /paypal/i.test(getCombinedSearchText(current) || getCombinedSearchText(radio))
+        && hasPaymentMethodSelectionMarker(radio)
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  function hasPaymentMethodSelectionMarker(el) {
+    if (!el) return false;
+    const className = typeof el.className === 'string' ? el.className : el.getAttribute?.('class') || '';
+    return el.checked === true
+      || el.getAttribute?.('aria-checked') === 'true'
+      || el.getAttribute?.('aria-selected') === 'true'
+      || el.getAttribute?.('data-state') === 'checked'
+      || el.getAttribute?.('data-selected') === 'true'
+      || /\b(selected|checked|active)\b/i.test(className);
+  }
+
+  async function ensureTermsCheckbox() {
+    const cb = document.getElementById('termsOfServiceConsentCheckbox')
+      || Array.from(document.querySelectorAll('input[type="checkbox"]')).find((input) => /terms|service|agree|consent/i.test(getFieldText(input)));
+    if (cb && !cb.checked) {
+      dispatchPointerMouseClick(cb);
+      log('已勾选服务条款');
+      await sleep(300);
+    }
+  }
+
+  function findSubmitButton() {
+    const direct = document.querySelector('button[data-testid="submit-button"]')
+      || document.querySelector('button[data-testid="hosted-payment-submit-button"]')
+      || document.querySelector('button[data-atomic-wait-intent="Submit_Email"]')
+      || document.querySelector('button.SubmitButton--complete');
+    if (direct && isVisibleNode(direct)) return direct;
+
+    const patterns = [/^下一页$/, /^下一步$/, /^next$/i, /subscribe|pay|continue|agree/i, /訂閱|处理中|同意|付款|继续/];
+    return Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"]')).find((el) => {
+      if (!isVisibleNode(el) || el.disabled) return false;
+      const text = normalizeText(el.textContent || el.value || el.getAttribute?.('aria-label') || '');
+      return patterns.some((pattern) => pattern.test(text));
+    }) || null;
+  }
+
+  function dispatchRobustActivation(el) {
+    dispatchPointerMouseClick(el);
+    if (typeof el.focus === 'function') {
+      el.focus({ preventScroll: true });
+    }
+    [' ', 'Enter'].forEach((key) => {
+      try {
+        el.dispatchEvent(new KeyboardEvent('keydown', {
+          key,
+          code: key === ' ' ? 'Space' : 'Enter',
+          bubbles: true,
+          cancelable: true,
+        }));
+        el.dispatchEvent(new KeyboardEvent('keyup', {
+          key,
+          code: key === ' ' ? 'Space' : 'Enter',
+          bubbles: true,
+          cancelable: true,
+        }));
+      } catch {
+        // Some synthetic keyboard events can be rejected on hardened checkout nodes.
+      }
+    });
+  }
+
+  function dispatchPointerMouseClick(el) {
+    if (!el) throw new Error('无法点击空元素。');
+    el.scrollIntoView?.({ block: 'center', inline: 'nearest' });
+    const rect = el.getBoundingClientRect();
+    const clientX = Math.max(0, Math.floor(rect.left + rect.width / 2));
+    const clientY = Math.max(0, Math.floor(rect.top + rect.height / 2));
+    ['pointerdown', 'mouseover', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((type) => {
+      const EventCtor = type.startsWith('pointer') && typeof PointerEvent === 'function' ? PointerEvent : MouseEvent;
+      el.dispatchEvent(new EventCtor(type, {
+        bubbles: true,
+        cancelable: true,
+        view: window,
+        button: 0,
+        buttons: type === 'pointerup' || type === 'mouseup' || type === 'click' ? 0 : 1,
+        clientX,
+        clientY,
+        pointerId: 1,
+        pointerType: 'mouse',
+        isPrimary: true,
+      }));
+    });
+    if (typeof el.click === 'function') {
+      el.click();
+    }
+  }
+
+  function fillBySelector(selector, value) {
+    const el = document.querySelector(selector);
+    if (el) {
+      fillInput(el, value);
+    } else {
+      log(`未找到元素: ${selector}`, 'warn');
+    }
+  }
+
+  function fillMissingBySelector(selector, value) {
+    const el = document.querySelector(selector);
+    if (!el) {
+      log(`未找到元素: ${selector}`, 'warn');
+      return;
+    }
+
+    if (String(el.value || '').trim()) {
+      return;
+    }
+
+    fillInput(el, value);
+  }
+
+  function fillMissingControl(el, value) {
+    if (!el) return false;
+    if (String(el.value || '').trim()) {
+      return false;
+    }
+    fillInput(el, value);
+    return true;
+  }
+
+  function fillSelectByText(selector, text) {
+    const el = document.querySelector(selector);
+    if (!el) {
+      log(`未找到 select: ${selector}`, 'warn');
+      return;
+    }
+    for (let i = 0; i < el.options.length; i++) {
+      const opt = el.options[i];
+      if (opt.text.toLowerCase().includes(text.toLowerCase()) || opt.value.toLowerCase().includes(text.toLowerCase())) {
+        fillSelect(el, opt.value);
+        return;
+      }
+    }
+    log(`未在 select 中找到匹配项: ${text}`, 'warn');
+  }
+
+  function fillRegionControlByText(el, text) {
+    if (!el) return false;
+    if (el.tagName === 'SELECT') {
+      for (let i = 0; i < el.options.length; i++) {
+        const opt = el.options[i];
+        if (opt.text.toLowerCase().includes(text.toLowerCase()) || opt.value.toLowerCase().includes(text.toLowerCase())) {
+          fillSelect(el, opt.value);
+          return true;
+        }
+      }
+      log(`未在地区 select 中找到匹配项: ${text}`, 'warn');
+      return false;
+    }
+    return fillMissingControl(el, text);
+  }
+
+  function fillBillingAddressDirectSelectors(addr = {}) {
+    fillBySelector('#billingAddressLine1', addr.street);
+    fillBySelector('#billingLocality', addr.city);
+    fillBySelector('#billingPostalCode', addr.zip.substring(0, 5));
+    fillSelectByText('#billingAdministrativeArea', addr.state);
+  }
+
+  async function fillBillingAddressLine1FromGoogle(addr = {}) {
+    const input = getStructuredAddressFields().address1 || findAddressSearchInput();
+    if (!input) {
+      log('未找到地址栏 1 输入框,无法触发 Google 地址下拉。', 'warn');
+      return false;
+    }
+
+    const query = buildGoogleAddressQuery(addr);
+    log(`正在地址栏 1 输入完整地址以触发 Google 下拉: ${query}`);
+    await typeAddressQueryForAutocomplete(input, query);
+
+    const item = await waitForGoogleAddressSuggestion(query, 6500);
+    if (item) {
+      const itemText = getSuggestionText(item) || '首个地址建议';
+      log(`正在选择 Google 地址建议: ${itemText}`);
+      clickAutocompleteSuggestion(item);
+      await waitForAddressAutofill(input, query, 2200);
+      dispatchInputBlur(input);
+      return true;
+    }
+
+    const externalFrameSelected = await selectAddressSuggestionInExternalFrame(query);
+    if (externalFrameSelected) {
+      await waitForAddressAutofill(input, query, 2200);
+      dispatchInputBlur(input);
+      return true;
+    }
+
+    log('未检测到可点击的 Google 地址建议,尝试使用键盘选择首个建议...', 'warn');
+    const keyboardSelected = await chooseAutocompleteWithKeyboard(input, query);
+    if (keyboardSelected) {
+      dispatchInputBlur(input);
+      return true;
+    }
+
+    return false;
+  }
+
+  function findAutoJsPayPalTarget() {
+    return document.querySelector('[data-testid="paypal-accordion-item-button"]')
+      || document.querySelector('.paypal-accordion-item button');
+  }
+
+  async function clickPayPalLikeAutoJs(target) {
+    if (!target) return;
+    target.scrollIntoView?.({ block: 'center', inline: 'nearest' });
+    if (typeof target.click === 'function') {
+      target.click();
+    }
+    await sleep(600);
+  }
+
+  async function selectGoogleAddressSuggestionOnly(payload = {}) {
+    const query = normalizeText(payload.query || '');
+    const item = await waitForGoogleAddressSuggestion(query, 5500);
+    if (!item) {
+      return { error: '未找到 Google 地址建议项' };
+    }
+
+    const itemText = getSuggestionText(item) || '首个地址建议';
+    log(`正在独立 autocomplete iframe 中选择 Google 地址建议: ${itemText}`);
+    clickAutocompleteSuggestion(item);
+    await sleep(900);
+    return {
+      ok: true,
+      selectedAddressText: itemText,
+    };
+  }
+
+  async function selectAddressSuggestionInExternalFrame(query) {
+    if (!chrome?.runtime?.sendMessage) {
+      return false;
+    }
+
+    try {
+      const response = await chrome.runtime.sendMessage({
+        type: 'CHECKOUT_STRIPE_SELECT_AUTOCOMPLETE_FRAME',
+        source: 'checkout-stripe',
+        payload: { query },
+      });
+      if (response?.ok) {
+        log(`已在独立 Google 地址 iframe 中选择建议: ${response.selectedAddressText || '首个地址建议'}`);
+        return true;
+      }
+      if (response?.error) {
+        log(`独立 Google 地址 iframe 未完成选择: ${response.error}`, 'warn');
+      }
+    } catch (error) {
+      log(`尝试选择独立 Google 地址 iframe 失败: ${error?.message || error}`, 'warn');
+    }
+
+    return false;
+  }
+
+  function buildGoogleAddressQuery(addr = {}) {
+    const explicitQuery = normalizeText(addr.query || addr.autocompleteQuery || '');
+    if (explicitQuery) return explicitQuery;
+
+    const street = normalizeText(addr.street || addr.address1 || '123 Main St');
+    const city = normalizeText(addr.city || 'New York');
+    const state = normalizeText(addr.state || addr.region || 'New York');
+    const zip = normalizeText(addr.zip || addr.postalCode || '10001').substring(0, 5);
+    return [street, city, state, zip].filter(Boolean).join(', ');
+  }
+
+  async function typeAddressQueryForAutocomplete(input, query) {
+    input.scrollIntoView?.({ block: 'center', inline: 'nearest' });
+    input.focus();
+    await sleep(150);
+
+    setNativeInputValue(input, '');
+    input.dispatchEvent(new Event('input', { bubbles: true }));
+    input.dispatchEvent(new Event('change', { bubbles: true }));
+    await sleep(120);
+
+    for (const char of String(query || '')) {
+      throwIfStopped();
+      input.dispatchEvent(new KeyboardEvent('keydown', {
+        key: char,
+        bubbles: true,
+        cancelable: true,
+      }));
+      setNativeInputValue(input, `${input.value || ''}${char}`);
+      dispatchAutocompleteInputEvent(input, char);
+      input.dispatchEvent(new KeyboardEvent('keyup', {
+        key: char,
+        bubbles: true,
+        cancelable: true,
+      }));
+      await sleep(18);
+    }
+
+    input.dispatchEvent(new Event('change', { bubbles: true }));
+  }
+
+  function setNativeInputValue(input, value) {
+    const descriptor = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value');
+    if (descriptor?.set) {
+      descriptor.set.call(input, value);
+    } else {
+      input.value = value;
+    }
+  }
+
+  function dispatchAutocompleteInputEvent(input, data) {
+    try {
+      input.dispatchEvent(new InputEvent('input', {
+        bubbles: true,
+        cancelable: true,
+        data,
+        inputType: 'insertText',
+      }));
+    } catch {
+      input.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
+    }
+  }
+
+  function dispatchInputBlur(input) {
+    input.dispatchEvent(new Event('change', { bubbles: true }));
+    input.dispatchEvent(new Event('blur', { bubbles: true }));
+  }
+
+  async function waitForGoogleAddressSuggestion(query, timeoutMs = 5000) {
+    const start = Date.now();
+    while (Date.now() - start < timeoutMs) {
+      throwIfStopped();
+      const items = getVisibleAutocompleteSuggestions(query);
+      if (items.length) {
+        const matching = findBestAddressSuggestion(items, query);
+        return matching || items[0];
+      }
+      await sleep(250);
+    }
+    return null;
+  }
+
+  function getVisibleAutocompleteSuggestions(query = '') {
+    const selectors = [
+      { selector: '.pac-container .pac-item', generic: false },
+      { selector: '#billing-address-autocomplete-results [role="option"]', generic: false },
+      { selector: '.AddressAutocomplete-results [role="option"]', generic: false },
+      { selector: '[class*="AddressAutocomplete"] [role="option"]', generic: false },
+      { selector: '[data-testid*="address" i] [role="option"]', generic: false },
+      { selector: '[role="listbox"] [role="option"]', generic: false },
+      { selector: '[role="option"]', generic: false },
+      { selector: '[role="listbox"] li', generic: true },
+      { selector: '.autocomplete-dropdown [role="option"]', generic: false },
+      { selector: '.autocomplete-dropdown li', generic: true },
+      { selector: 'li', generic: true },
+    ];
+    const seen = new Set();
+    const items = [];
+
+    for (const config of selectors) {
+      document.querySelectorAll(config.selector).forEach((item) => {
+        if (seen.has(item) || !isVisibleNode(item)) return;
+        if (!isLikelyAddressSuggestion(item, query, config.generic)) return;
+        seen.add(item);
+        items.push(item);
+      });
+    }
+
+    return items;
+  }
+
+  function findBestAddressSuggestion(items, query) {
+    let best = null;
+    let bestScore = 0;
+
+    for (const item of items) {
+      const score = scoreAddressSuggestion(item, query);
+      if (score > bestScore) {
+        best = item;
+        bestScore = score;
+      }
+    }
+
+    return best || null;
+  }
+
+  function getSuggestionText(item) {
+    return normalizeText(item?.textContent || item?.getAttribute?.('aria-label') || '');
+  }
+
+  function normalizeText(value = '') {
+    return String(value || '').replace(/\s+/g, ' ').trim();
+  }
+
+  function getAddressQueryTokens(query = '') {
+    return normalizeText(query)
+      .toLowerCase()
+      .split(/[^a-z0-9]+/i)
+      .map((part) => part.trim())
+      .filter((part) => part.length >= 3);
+  }
+
+  function scoreAddressSuggestion(item, query = '') {
+    const text = getSuggestionText(item).toLowerCase();
+    if (!text) return 0;
+
+    const tokens = getAddressQueryTokens(query);
+    let score = 0;
+    tokens.forEach((token) => {
+      if (text.includes(token)) score += 1;
+    });
+    if (/\d/.test(text)) score += 2;
+    if (/street|st\.?|avenue|ave\.?|road|rd\.?|drive|dr\.?|boulevard|blvd\.?|lane|ln\.?|way|court|ct\.?/i.test(text)) {
+      score += 2;
+    }
+    return score;
+  }
+
+  function isLikelyAddressSuggestion(item, query = '', generic = false) {
+    const text = getSuggestionText(item);
+    if (!text || text.length < 3) return false;
+
+    if (!generic) return true;
+
+    const lowered = text.toLowerCase();
+    if (/terms|privacy|subscribe|paypal|card|payment|email|phone|下一步|提交|付款|订阅/i.test(lowered)) {
+      return false;
+    }
+
+    return scoreAddressSuggestion(item, query) > 0;
+  }
+
+  function isVisibleNode(node) {
+    const rect = node.getBoundingClientRect();
+    const style = window.getComputedStyle(node);
+    return rect.width > 0
+      && rect.height > 0
+      && style.visibility !== 'hidden'
+      && style.display !== 'none';
+  }
+
+  function clickAutocompleteSuggestion(item) {
+    item.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
+    ['pointerdown', 'mouseover', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((type) => {
+      const EventCtor = type.startsWith('pointer') && typeof PointerEvent === 'function' ? PointerEvent : MouseEvent;
+      item.dispatchEvent(new EventCtor(type, {
+        bubbles: true,
+        cancelable: true,
+        view: window,
+      }));
+    });
+    if (typeof item.click === 'function') {
+      item.click();
+    }
+  }
+
+  async function chooseAutocompleteWithKeyboard(input, query = '') {
+    input.focus();
+    input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', code: 'ArrowDown', bubbles: true, cancelable: true }));
+    input.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown', code: 'ArrowDown', bubbles: true, cancelable: true }));
+    await sleep(250);
+    input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true }));
+    input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true }));
+    return waitForAddressAutofill(input, query, 2200);
+  }
+
+  async function waitForAddressAutofill(input, query = '', timeoutMs = 1800) {
+    const startedAt = Date.now();
+    const originalQuery = normalizeText(query);
+    while (Date.now() - startedAt < timeoutMs) {
+      throwIfStopped();
+      const line1 = normalizeText(input?.value || '');
+      const fields = getStructuredAddressFields();
+      const city = normalizeText(fields.city?.value || document.querySelector('#billingLocality')?.value || '');
+      const zip = normalizeText(fields.postalCode?.value || document.querySelector('#billingPostalCode')?.value || '');
+      if (city || zip || (line1 && line1 !== originalQuery)) {
+        return true;
+      }
+      await sleep(200);
+    }
+    return false;
+  }
+
+  function hideAutocompleteDropdowns() {
+    log('正在隐藏剩余 Google 地址补全框...');
+    document.querySelectorAll([
+      '.pac-container',
+      '.pac-item',
+      'div[role="listbox"]',
+      '.AddressAutocomplete-results',
+      '[class*="AddressAutocomplete"]',
+      '#billing-address-autocomplete-results',
+    ].join(', ')).forEach((el) => {
+      el.style.setProperty('display', 'none', 'important');
+      el.style.setProperty('visibility', 'hidden', 'important');
+      el.style.setProperty('height', '0', 'important');
+      el.style.setProperty('overflow', 'hidden', 'important');
+    });
+  }
+
+  async function clickSubmitButton(retries = 0) {
+    throwIfStopped();
+    const btn = findSubmitButton();
+
+    if (btn) {
+      const rect = btn.getBoundingClientRect();
+      if (btn.disabled || rect.height === 0) {
+        log('提交按钮被禁用或不可见,等待中...');
+        if (retries < 12) {
+          await sleep(800);
+          return clickSubmitButton(retries + 1);
+        }
+        throw new Error('提交按钮一直不可用,已超时');
+      }
+      log(`正在点击提交按钮: ${btn.textContent.trim()}`);
+      dispatchPointerMouseClick(btn);
+    } else {
+      if (retries < 12) {
+        log(`未找到提交按钮,重试中... (${retries + 1})`);
+        await sleep(800);
+        return clickSubmitButton(retries + 1);
+      }
+      throw new Error('在 Stripe 页面上未找到提交按钮');
+    }
+  }
+
+  document.documentElement.setAttribute('data-multipage-checkout-stripe-ready', '');
+})();

+ 57 - 0
content/signup-page.js

@@ -257,6 +257,13 @@ function is405MethodNotAllowedPage() {
     || /Route\s+Error.*405/i.test(pageText);
 }
 
+function isVerificationRouteErrorPage() {
+  const pageText = String(document.body?.textContent || '').replace(/\s+/g, ' ').trim();
+  return /Oops,\s*an\s*error\s*occurred/i.test(pageText)
+    || /Route\s+Error.*Invalid\s+content\s+type/i.test(pageText)
+    || /Invalid\s+content\s+type:\s*text\/html/i.test(pageText);
+}
+
 async function handle405ResendError(step, remainingTimeout = 30000) {
   const start = Date.now();
   let retryCount = 0;
@@ -1428,6 +1435,13 @@ async function waitForVerificationSubmitOutcome(step, timeout) {
   while (Date.now() - start < resolvedTimeout) {
     throwIfStopped();
 
+    if (isVerificationRouteErrorPage()) {
+      return {
+        invalidCode: true,
+        errorText: getPageTextSnapshot().slice(0, 240) || '验证码提交后进入认证错误页。',
+      };
+    }
+
     const errorText = getVerificationErrorText();
     if (errorText) {
       return { invalidCode: true, errorText };
@@ -1458,6 +1472,48 @@ async function waitForVerificationSubmitOutcome(step, timeout) {
   return { success: true, assumed: true };
 }
 
+async function waitForVerificationPageStableBeforeSubmit(step, codeInput, submitBtn, timeout = 3500) {
+  const start = Date.now();
+  let stableSince = 0;
+  let loggedWaiting = false;
+
+  while (Date.now() - start < timeout) {
+    throwIfStopped();
+
+    if (is405MethodNotAllowedPage()) {
+      log(`步骤 ${step}:提交验证码前检测到 405 错误页面,正在恢复...`, 'warn');
+      await handle405ResendError(step, Math.max(1000, timeout - (Date.now() - start)));
+      stableSince = 0;
+      continue;
+    }
+
+    if (isVerificationRouteErrorPage()) {
+      throw new Error(`步骤 ${step}:提交验证码前认证页面已进入错误页,请重新执行当前步骤。URL: ${location.href}`);
+    }
+
+    const inputReady = codeInput?.isConnected && isVisibleElement(codeInput);
+    const submitReady = !submitBtn || (submitBtn.isConnected && isVisibleElement(submitBtn) && isActionEnabled(submitBtn));
+    const pageReady = step === 8 ? isVerificationPageStillVisible() : isEmailVerificationPage();
+
+    if (inputReady && submitReady && pageReady) {
+      if (!stableSince) stableSince = Date.now();
+      if (Date.now() - stableSince >= 700) {
+        return;
+      }
+    } else {
+      stableSince = 0;
+      if (!loggedWaiting) {
+        loggedWaiting = true;
+        log(`步骤 ${step}:提交验证码前等待页面和按钮稳定...`, 'info');
+      }
+    }
+
+    await sleep(150);
+  }
+
+  throw new Error(`步骤 ${step}:提交验证码前页面未稳定,请稍后重试。URL: ${location.href}`);
+}
+
 async function fillVerificationCode(step, payload) {
   const { code } = payload;
   if (!code) throw new Error('未提供验证码。');
@@ -1532,6 +1588,7 @@ async function fillVerificationCode(step, payload) {
     || await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证/i, 5000).catch(() => null);
 
   if (submitBtn) {
+    await waitForVerificationPageStableBeforeSubmit(step, codeInput, submitBtn);
     await humanPause(450, 1200);
     simulateClick(submitBtn);
     log(`步骤 ${step}:验证码已提交`);

+ 2 - 0
content/utils.js

@@ -14,6 +14,8 @@ const SCRIPT_SOURCE = (() => {
   if (url.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
   if (url.includes('chatgpt.com')) return 'chatgpt';
   if (url.includes("2925.com")) return "mail-2925";
+  if (hostname === 'checkout.stripe.com' || hostname === 'pay.openai.com') return 'checkout-stripe';
+  if (hostname.includes('paypal.com')) return 'checkout-paypal';
   // VPS panel — detected dynamically since URL is configurable
   return 'vps-panel';
 })();

+ 5 - 5
data/step-definitions.js

@@ -7,11 +7,11 @@
     { id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' },
     { id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码' },
     { id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日' },
-    { id: 6, order: 60, key: 'clear-login-cookies', title: '清理登录 Cookies' },
-    { id: 7, order: 70, key: 'oauth-login', title: '刷新 OAuth 并登录' },
-    { id: 8, order: 80, key: 'fetch-login-code', title: '获取登录验证码' },
-    { id: 9, order: 90, key: 'confirm-oauth', title: '自动确认 OAuth' },
-    { id: 10, order: 100, key: 'platform-verify', title: '平台回调验证' },
+    { id: 6, order: 60, key: 'get-plus-link', title: '获取 Plus 订阅链接' },
+    { id: 7, order: 70, key: 'fill-stripe-checkout', title: '填写 Stripe 结账表单' },
+    { id: 8, order: 80, key: 'fill-paypal-login', title: '填写 PayPal 登录邮箱' },
+    { id: 9, order: 90, key: 'fill-paypal-payment', title: '填写 PayPal 付款信息' },
+    { id: 10, order: 100, key: 'sync-cpa-session', title: '同步 CPA 会话' },
   ];
 
   function getSteps() {

+ 24 - 0
manifest.json

@@ -102,6 +102,30 @@
         "content/duck-mail.js"
       ],
       "run_at": "document_idle"
+    },
+    {
+      "matches": [
+        "https://checkout.stripe.com/*",
+        "https://pay.openai.com/*"
+      ],
+      "js": [
+        "content/activation-utils.js",
+        "content/utils.js",
+        "content/checkout-stripe.js"
+      ],
+      "run_at": "document_idle"
+    },
+    {
+      "matches": [
+        "https://*.paypal.com/*",
+        "https://paypal.com/*"
+      ],
+      "js": [
+        "content/activation-utils.js",
+        "content/utils.js",
+        "content/checkout-paypal.js"
+      ],
+      "run_at": "document_idle"
     }
   ],
   "action": {

+ 8 - 0
sidepanel/sidepanel.html

@@ -242,6 +242,14 @@
           <button id="btn-fetch-email" class="btn btn-outline btn-sm data-inline-btn" type="button">获取</button>
         </div>
       </div>
+      <div class="data-row">
+        <span class="data-label">PayPal手机</span>
+        <input type="text" id="input-paypal-phone" class="data-input" placeholder="+15822201173" />
+      </div>
+      <div class="data-row">
+        <span class="data-label">短信接口</span>
+        <input type="text" id="input-paypal-sms-api-url" class="data-input mono" placeholder="http://a.62-us.com/api/get_sms?key=..." />
+      </div>
       <div class="data-row">
         <span class="data-label">延迟</span>
         <div class="data-inline auto-delay-inline">

+ 45 - 6
sidepanel/sidepanel.js

@@ -166,6 +166,8 @@ const rowCfDomain = document.getElementById('row-cf-domain');
 const selectCfDomain = document.getElementById('select-cf-domain');
 const inputCfDomain = document.getElementById('input-cf-domain');
 const btnCfDomainMode = document.getElementById('btn-cf-domain-mode');
+const inputPaypalPhone = document.getElementById('input-paypal-phone');
+const inputPaypalSmsApiUrl = document.getElementById('input-paypal-sms-api-url');
 const inputRunCount = document.getElementById('input-run-count');
 const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
 const inputAutoSkipFailuresThreadIntervalMinutes = document.getElementById('input-auto-skip-failures-thread-interval-minutes');
@@ -209,6 +211,8 @@ const AUTO_STEP_DELAY_MAX_SECONDS = 600;
 const VERIFICATION_RESEND_COUNT_MIN = 0;
 const VERIFICATION_RESEND_COUNT_MAX = 20;
 const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
+const DEFAULT_PAYPAL_PHONE = '+15822201173';
+const DEFAULT_PAYPAL_SMS_API_URL = 'http://a.62-us.com/api/get_sms?key=a5d3262e05efaba982aba7cfae20b8bc';
 const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
 const DEFAULT_CPA_CALLBACK_MODE = 'step8';
 const DEFAULT_SUB2API_PROXY_NAME = 'shadowrocket';
@@ -1024,6 +1028,14 @@ function normalizeAutoStepDelaySeconds(value) {
   return Math.min(AUTO_STEP_DELAY_MAX_SECONDS, Math.max(AUTO_STEP_DELAY_MIN_SECONDS, Math.floor(numeric)));
 }
 
+function normalizePaypalPhoneInputValue(value) {
+  return String(value || '').trim() || DEFAULT_PAYPAL_PHONE;
+}
+
+function normalizePaypalSmsApiUrlInputValue(value) {
+  return String(value || '').trim() || DEFAULT_PAYPAL_SMS_API_URL;
+}
+
 function normalizeVerificationResendCount(value, fallback) {
   const rawValue = String(value ?? '').trim();
   if (!rawValue) {
@@ -1381,6 +1393,8 @@ function collectSettingsPayload() {
     autoRunDelayEnabled: inputAutoDelayEnabled.checked,
     autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value),
     autoStepDelaySeconds: normalizeAutoStepDelaySeconds(inputAutoStepDelaySeconds.value),
+    paypalPhone: normalizePaypalPhoneInputValue(inputPaypalPhone?.value),
+    paypalSmsApiUrl: normalizePaypalSmsApiUrlInputValue(inputPaypalSmsApiUrl?.value),
     verificationResendCount: normalizeVerificationResendCount(
       inputVerificationResendCount?.value,
       DEFAULT_VERIFICATION_RESEND_COUNT
@@ -1782,6 +1796,12 @@ function applySettingsState(state) {
   inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
   inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state?.autoRunDelayMinutes));
   inputAutoStepDelaySeconds.value = formatAutoStepDelayInputValue(state?.autoStepDelaySeconds);
+  if (inputPaypalPhone) {
+    inputPaypalPhone.value = normalizePaypalPhoneInputValue(state?.paypalPhone);
+  }
+  if (inputPaypalSmsApiUrl) {
+    inputPaypalSmsApiUrl.value = normalizePaypalSmsApiUrlInputValue(state?.paypalSmsApiUrl);
+  }
   if (inputVerificationResendCount) {
     const restoredVerificationResendCount = state?.verificationResendCount !== undefined
       ? state.verificationResendCount
@@ -2459,7 +2479,7 @@ function updateMailProviderUI() {
         : (useLuckmail
           ? '步骤 3 会自动购买 LuckMail 邮箱并用于收码'
         : (useA4sky
-          ? '点击“生成”得到 n{Ymdhis}@a4sky.com;步骤 4/8 会通过下方 IMAP 助手地址直接取码'
+          ? '点击“生成”得到 n{Ymdhis}@edu.a4sky.com;步骤 4 会通过下方 IMAP 助手地址直接取码'
       : (useGeneratedAlias
         ? '步骤 3 会自动生成邮箱,无需手动获取'
         : (useCustomEmail ? '请先填写自定义注册邮箱,成功一轮后会自动清空' : `先自动获取${uiCopy.label},或手动粘贴邮箱后再继续`))));
@@ -2559,10 +2579,6 @@ function updatePanelModeUI() {
   rowSub2ApiGroup.style.display = useSub2Api ? '' : 'none';
   rowSub2ApiDefaultProxy.style.display = useSub2Api ? '' : 'none';
 
-  const step9Btn = document.querySelector('.step-btn[data-step-key="platform-verify"]');
-  if (step9Btn) {
-    step9Btn.textContent = useSub2Api ? 'SUB2API 回调验证' : 'CPA 回调验证';
-  }
 }
 
 // ============================================================
@@ -3622,7 +3638,7 @@ selectMailProvider.addEventListener('change', async () => {
     && isCurrentEmailManagedByLuckmail();
   const leavingA4sky = previousProvider === A4SKY_PROVIDER
     && nextProvider !== A4SKY_PROVIDER
-    && /^n\d{14}@a4sky\.com$/i.test(String(inputEmail.value || latestState?.email || '').trim());
+    && /^n\d{14}@edu\.a4sky\.com$/i.test(String(inputEmail.value || latestState?.email || '').trim());
   const leavingGeneratedAlias = (
     previousProvider !== nextProvider
     || (previousProvider === '2925' && normalizeMail2925Mode(previousMail2925Mode) !== getSelectedMail2925Mode())
@@ -3958,6 +3974,23 @@ inputAutoStepDelaySeconds.addEventListener('blur', () => {
   saveSettings({ silent: true }).catch(() => { });
 });
 
+inputPaypalPhone?.addEventListener('input', () => {
+  markSettingsDirty(true);
+  scheduleSettingsAutoSave();
+});
+inputPaypalPhone?.addEventListener('blur', () => {
+  inputPaypalPhone.value = normalizePaypalPhoneInputValue(inputPaypalPhone.value);
+  saveSettings({ silent: true }).catch(() => { });
+});
+inputPaypalSmsApiUrl?.addEventListener('input', () => {
+  markSettingsDirty(true);
+  scheduleSettingsAutoSave();
+});
+inputPaypalSmsApiUrl?.addEventListener('blur', () => {
+  inputPaypalSmsApiUrl.value = normalizePaypalSmsApiUrlInputValue(inputPaypalSmsApiUrl.value);
+  saveSettings({ silent: true }).catch(() => { });
+});
+
 inputVerificationResendCount?.addEventListener('input', () => {
   markSettingsDirty(true);
   scheduleSettingsAutoSave();
@@ -4174,6 +4207,12 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
       if (message.payload.autoStepDelaySeconds !== undefined) {
         inputAutoStepDelaySeconds.value = formatAutoStepDelayInputValue(message.payload.autoStepDelaySeconds);
       }
+      if (message.payload.paypalPhone !== undefined && inputPaypalPhone) {
+        inputPaypalPhone.value = normalizePaypalPhoneInputValue(message.payload.paypalPhone);
+      }
+      if (message.payload.paypalSmsApiUrl !== undefined && inputPaypalSmsApiUrl) {
+        inputPaypalSmsApiUrl.value = normalizePaypalSmsApiUrlInputValue(message.payload.paypalSmsApiUrl);
+      }
       if (
         (
           message.payload.verificationResendCount !== undefined

+ 4 - 4
tests/activation-utils.test.js

@@ -6,7 +6,7 @@ const {
   isRecoverableStep9AuthFailure,
 } = require('../content/activation-utils.js');
 
-test('getActivationStrategy prefers requestSubmit for submit buttons inside forms', () => {
+test('getActivationStrategy uses native click for submit buttons inside forms', () => {
   assert.deepEqual(
     getActivationStrategy({
       tagName: 'button',
@@ -14,7 +14,7 @@ test('getActivationStrategy prefers requestSubmit for submit buttons inside form
       hasForm: true,
       pathname: '/email-verification',
     }),
-    { method: 'requestSubmit' }
+    { method: 'click' }
   );
 });
 
@@ -38,7 +38,7 @@ test('getActivationStrategy uses native click for non-submit actions', () => {
   );
 });
 
-test('getActivationStrategy only uses requestSubmit on email verification routes', () => {
+test('getActivationStrategy uses native click on email verification routes', () => {
   assert.deepEqual(
     getActivationStrategy({
       tagName: 'button',
@@ -56,7 +56,7 @@ test('getActivationStrategy only uses requestSubmit on email verification routes
       hasForm: true,
       pathname: '/email-verification',
     }),
-    { method: 'requestSubmit' }
+    { method: 'click' }
   );
 });
 

+ 9 - 9
tests/auto-run-step4-restart.test.js

@@ -56,14 +56,16 @@ const bundle = [
   extractFunction('isAddPhoneAuthUrl'),
   extractFunction('isAddPhoneAuthState'),
   extractFunction('getPostStep6AutoRestartDecision'),
+  extractFunction('getNextActiveStep'),
   extractFunction('runAutoSequenceFromStep'),
 ].join('\n');
 
 test('auto-run restarts from step 1 with the same email after step 4 failure', async () => {
   const api = new Function(`
-const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
-const LAST_STEP_ID = 10;
-const FINAL_OAUTH_CHAIN_START_STEP = 7;
+const STEP_IDS = [1, 2, 3, 4, 5, 6, 7, 8, 9];
+const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0 };
+const LAST_STEP_ID = 9;
+const FINAL_OAUTH_CHAIN_START_STEP = null;
 const chrome = {
   tabs: {
     update: async () => {},
@@ -85,10 +87,9 @@ let remainingFailures = 1;
       4: 'pending',
       5: 'pending',
       6: 'pending',
-    7: 'pending',
-    8: 'pending',
-    9: 'pending',
-    10: 'pending',
+      7: 'pending',
+      8: 'pending',
+      9: 'pending',
   },
 };
 const events = {
@@ -159,7 +160,6 @@ async function invalidateDownstreamAfterStepRestart(step, options = {}) {
       7: 'pending',
       8: 'pending',
       9: 'pending',
-      10: 'pending',
     },
   };
 }
@@ -202,7 +202,7 @@ return {
     },
   ]);
   assert.deepStrictEqual(events.emails, ['keep@example.com', 'keep@example.com']);
-  assert.deepStrictEqual(events.steps, [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
+  assert.deepStrictEqual(events.steps, [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
   assert.equal(currentState.email, 'keep@example.com');
   assert.equal(currentState.password, 'Secret123!');
   assert.equal(events.logs.some(({ message }) => /沿用当前邮箱回到步骤 1 重新开始/.test(message)), true);

+ 26 - 34
tests/auto-run-step6-restart.test.js

@@ -57,22 +57,24 @@ const bundle = [
   extractFunction('isAddPhoneAuthUrl'),
   extractFunction('isAddPhoneAuthState'),
   extractFunction('getPostStep6AutoRestartDecision'),
+  extractFunction('getNextActiveStep'),
   extractFunction('runAutoSequenceFromStep'),
 ].join('\n');
 
 function createHarness(options = {}) {
   const {
-    startStep = 7,
-    failureStep = 10,
+    startStep = 6,
+    failureStep = 6,
     failureBudget = 1,
     failureMessage = '认证失败: Request failed with status code 502',
     authState = { state: 'password_page', url: 'https://auth.openai.com/log-in' },
   } = options;
 
   return new Function(`
-const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
-const LAST_STEP_ID = 10;
-const FINAL_OAUTH_CHAIN_START_STEP = 7;
+const STEP_IDS = [1, 2, 3, 4, 5, 6, 7, 8, 9];
+const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0 };
+const LAST_STEP_ID = 9;
+const FINAL_OAUTH_CHAIN_START_STEP = null;
 const LOG_PREFIX = '[test]';
 const chrome = {
   tabs: {
@@ -157,35 +159,25 @@ return {
 `)();
 }
 
-test('auto-run keeps restarting from step 7 after post-login failures without a hard cap', async () => {
+test('auto-run no longer restarts from removed OAuth chain after payment-link failures', async () => {
   const harness = createHarness({
-    failureStep: 10,
-    failureBudget: 6,
+    failureStep: 6,
+    failureBudget: 1,
     failureMessage: '认证失败: Request failed with status code 502',
     authState: { state: 'password_page', url: 'https://auth.openai.com/log-in' },
   });
 
-  const events = await harness.run();
-
-  assert.equal(events.invalidations.length, 6);
-  assert.deepStrictEqual(
-    events.steps,
-    [
-      7, 8, 9, 10,
-      7, 8, 9, 10,
-      7, 8, 9, 10,
-      7, 8, 9, 10,
-      7, 8, 9, 10,
-      7, 8, 9, 10,
-      7, 8, 9, 10,
-    ]
-  );
-  assert.ok(events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
+  const result = await harness.runAndCaptureError();
+
+  assert.ok(result?.error);
+  assert.equal(result.events.invalidations.length, 0);
+  assert.deepStrictEqual(result.events.steps, [6]);
+  assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
 });
 
-test('auto-run stops restarting once add-phone is detected', async () => {
+test('auto-run does not revive removed step 7 when add-phone is detected after step 6', async () => {
   const harness = createHarness({
-    failureStep: 7,
+    failureStep: 6,
     failureBudget: 1,
     failureMessage: '当前页面已进入手机号页面。URL: https://auth.openai.com/add-phone',
     authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' },
@@ -195,13 +187,13 @@ test('auto-run stops restarting once add-phone is detected', async () => {
 
   assert.ok(result?.error);
   assert.equal(result.events.invalidations.length, 0);
-  assert.deepStrictEqual(result.events.steps, [7]);
-  assert.ok(result.events.logs.some(({ message }) => /进入 add-phone/.test(message)));
+  assert.deepStrictEqual(result.events.steps, [6]);
+  assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
 });
 
-test('auto-run stops restarting on generic phone-page failure messages even without add-phone url', async () => {
+test('auto-run does not restart removed OAuth chain on generic phone-page failures', async () => {
   const harness = createHarness({
-    failureStep: 9,
+    failureStep: 6,
     failureBudget: 1,
     failureMessage: '步骤 8:当前认证页进入手机号页面,当前流程无法继续自动授权。',
     authState: { state: 'password_page', url: 'https://auth.openai.com/log-in' },
@@ -211,13 +203,13 @@ test('auto-run stops restarting on generic phone-page failure messages even with
 
   assert.ok(result?.error);
   assert.equal(result.events.invalidations.length, 0);
-  assert.deepStrictEqual(result.events.steps, [7, 8, 9]);
+  assert.deepStrictEqual(result.events.steps, [6]);
   assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
 });
 
-test('auto-run stop errors after step 7 are rethrown immediately instead of restarting', async () => {
+test('auto-run stop errors after removed OAuth chain are rethrown immediately', async () => {
   const harness = createHarness({
-    failureStep: 9,
+    failureStep: 6,
     failureBudget: 1,
     failureMessage: '流程已被用户停止。',
     authState: { state: 'password_page', url: 'https://auth.openai.com/log-in' },
@@ -227,6 +219,6 @@ test('auto-run stop errors after step 7 are rethrown immediately instead of rest
 
   assert.equal(result?.error?.message, '流程已被用户停止。');
   assert.equal(result.events.invalidations.length, 0);
-  assert.deepStrictEqual(result.events.steps, [7, 8, 9]);
+  assert.deepStrictEqual(result.events.steps, [6]);
   assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
 });

+ 129 - 0
tests/background-cpa-api.test.js

@@ -0,0 +1,129 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+function loadCpaApi(fetchImpl) {
+  const source = fs.readFileSync('background/cpa-api.js', 'utf8');
+  const scope = {};
+  new Function('self', 'fetch', `${source}; return self.MultiPageBackgroundCpaApi;`)(scope, fetchImpl);
+  return scope.MultiPageBackgroundCpaApi;
+}
+
+function base64UrlJson(value) {
+  return Buffer.from(JSON.stringify(value), 'utf8')
+    .toString('base64')
+    .replace(/\+/g, '-')
+    .replace(/\//g, '_')
+    .replace(/=+$/g, '');
+}
+
+function jwt(payload) {
+  return `${base64UrlJson({ alg: 'RS256', typ: 'JWT' })}.${base64UrlJson(payload)}.signature`;
+}
+
+test('buildCpaSessionAuthJson converts ChatGPT session to codex auth JSON', () => {
+  const api = loadCpaApi(async () => {
+    throw new Error('fetch should not be called');
+  }).createCpaApi();
+  const accessToken = jwt({
+    exp: 1893456000,
+    'https://api.openai.com/auth': {
+      chatgpt_account_id: 'acct_123',
+      chatgpt_user_id: 'user_123',
+      chatgpt_plan_type: 'plus',
+    },
+    'https://api.openai.com/profile': {
+      email: 'profile@example.com',
+    },
+  });
+
+  const result = api.buildCpaSessionAuthJson({
+    accessToken,
+    session: {
+      user: { email: 'session@example.com' },
+      refresh_token: 'refresh-token',
+      session_token: 'session-token',
+    },
+  }, {
+    now: new Date('2026-05-21T00:00:00.000Z'),
+  });
+
+  assert.equal(result.fileName, 'codex-session@example.com-plus.json');
+  assert.equal(result.email, 'session@example.com');
+  assert.equal(result.accountId, 'acct_123');
+  assert.equal(result.hasRefreshToken, true);
+  assert.deepEqual(result.authJson, {
+    type: 'codex',
+    account_id: 'acct_123',
+    chatgpt_account_id: 'acct_123',
+    email: 'session@example.com',
+    name: 'session@example.com',
+    plan_type: 'plus',
+    chatgpt_plan_type: 'plus',
+    id_token: result.authJson.id_token,
+    id_token_synthetic: true,
+    access_token: accessToken,
+    refresh_token: 'refresh-token',
+    session_token: 'session-token',
+    last_refresh: '2026-05-21T00:00:00.000Z',
+    expired: '2030-01-01T00:00:00.000Z',
+  });
+  assert.match(result.authJson.id_token, /\.synthetic$/);
+});
+
+test('importCurrentChatGptSession posts auth JSON to CPA management endpoint', async () => {
+  const calls = [];
+  const fetchImpl = async (url, options = {}) => {
+    calls.push({ url, options });
+    return {
+      ok: true,
+      status: 200,
+      json: async () => ({ ok: true }),
+    };
+  };
+  const logs = [];
+  const api = loadCpaApi(fetchImpl).createCpaApi({
+    addLog: async (message, level) => logs.push({ message, level }),
+    fetchImpl,
+  });
+  const accessToken = jwt({
+    exp: 1893456000,
+    'https://api.openai.com/auth': {
+      chatgpt_account_id: 'acct_abc',
+      chatgpt_plan_type: 'plus',
+    },
+    'https://api.openai.com/profile': {
+      email: 'sync@example.com',
+    },
+  });
+
+  const result = await api.importCurrentChatGptSession({
+    vpsUrl: 'http://127.0.0.1:8317/management.html#/oauth',
+    vpsPassword: 'management-secret',
+    session: {
+      accessToken,
+      user: { email: 'sync@example.com' },
+    },
+  }, {
+    logLabel: '步骤 10',
+    now: new Date('2026-05-21T00:00:00.000Z'),
+  });
+
+  assert.equal(result.cpaImportedFileName, 'codex-sync@example.com-plus.json');
+  assert.equal(result.cpaImportedEmail, 'sync@example.com');
+  assert.equal(calls.length, 1);
+  assert.equal(
+    calls[0].url,
+    'http://127.0.0.1:8317/v0/management/auth-files?name=codex-sync%40example.com-plus.json'
+  );
+  assert.equal(calls[0].options.method, 'POST');
+  assert.equal(calls[0].options.headers.Authorization, 'Bearer management-secret');
+  assert.equal(calls[0].options.headers['X-Management-Key'], 'management-secret');
+
+  const body = JSON.parse(calls[0].options.body);
+  assert.equal(body.type, 'codex');
+  assert.equal(body.email, 'sync@example.com');
+  assert.equal(body.access_token, accessToken);
+  assert.equal(body.chatgpt_account_id, 'acct_abc');
+  assert.equal(logs.some((entry) => /CPA 会话导入完成/.test(entry.message) && entry.level === 'ok'), true);
+});

+ 1 - 1
tests/background-generated-email-module.test.js

@@ -34,6 +34,6 @@ test('generated email helper supports a4sky mailbox format', async () => {
 
   const email = await helpers.fetchGeneratedEmail({ mailProvider: 'a4sky' });
 
-  assert.match(email, /^n\d{14}@a4sky\.com$/i);
+  assert.match(email, /^n\d{14}@edu\.a4sky\.com$/i);
   assert.equal(savedEmail, email);
 });

+ 18 - 0
tests/background-message-router-step2-skip.test.js

@@ -195,3 +195,21 @@ test('message router marks step 3 failed when post-submit finalize fails', async
   assert.equal(events.logs.some(({ message }) => /步骤 3 失败:步骤 3 提交后仍停留在密码页。/.test(message)), true);
   assert.deepStrictEqual(response, { ok: true, error: '步骤 3 提交后仍停留在密码页。' });
 });
+
+test('message router suppresses duplicate content ready logs for same source tab', async () => {
+  const { router, events } = createRouter();
+
+  await router.handleMessage({
+    type: 'CONTENT_SCRIPT_READY',
+    source: 'checkout-stripe',
+  }, { tab: { id: 662543926 } });
+  await router.handleMessage({
+    type: 'CONTENT_SCRIPT_READY',
+    source: 'checkout-stripe',
+  }, { tab: { id: 662543926 } });
+
+  assert.deepStrictEqual(
+    events.logs.filter((entry) => /内容脚本已就绪/.test(entry.message)).map((entry) => entry.message),
+    ['内容脚本已就绪:(标签页 662543926)']
+  );
+});

+ 2 - 0
tests/background-step-modules.test.js

@@ -16,6 +16,8 @@ test('background imports step 1~10 modules', () => {
     'background/steps/fetch-login-code.js',
     'background/steps/confirm-oauth.js',
     'background/steps/platform-verify.js',
+    'background/cpa-api.js',
+    'background/steps/sync-cpa-session.js',
   ].forEach((path) => {
     assert.match(source, new RegExp(path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
   });

+ 119 - 0
tests/background-step10-cpa-sync.test.js

@@ -0,0 +1,119 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+function loadModules(fetchImpl) {
+  const cpaSource = fs.readFileSync('background/cpa-api.js', 'utf8');
+  const stepSource = fs.readFileSync('background/steps/sync-cpa-session.js', 'utf8');
+  const scope = {};
+  new Function('self', 'fetch', `${cpaSource}\n${stepSource}; return self;`)(scope, fetchImpl);
+  return scope;
+}
+
+function base64UrlJson(value) {
+  return Buffer.from(JSON.stringify(value), 'utf8')
+    .toString('base64')
+    .replace(/\+/g, '-')
+    .replace(/\//g, '_')
+    .replace(/=+$/g, '');
+}
+
+function jwt(payload) {
+  return `${base64UrlJson({ alg: 'RS256', typ: 'JWT' })}.${base64UrlJson(payload)}.signature`;
+}
+
+test('step 10 reads ChatGPT session and imports CPA auth JSON', async () => {
+  const accessToken = jwt({
+    exp: 1893456000,
+    'https://api.openai.com/auth': {
+      chatgpt_account_id: 'acct_step10',
+      chatgpt_plan_type: 'plus',
+    },
+    'https://api.openai.com/profile': {
+      email: 'step10@example.com',
+    },
+  });
+  const fetchCalls = [];
+  const fetchImpl = async (url, options = {}) => {
+    fetchCalls.push({ url, options });
+    if (url === 'https://chatgpt.com/api/auth/session') {
+      return {
+        ok: true,
+        status: 200,
+        text: async () => JSON.stringify({
+          accessToken,
+          user: { email: 'step10@example.com' },
+        }),
+      };
+    }
+    if (url === 'https://cpa.example.com/v0/management/auth-files?name=codex-step10%40example.com-plus.json') {
+      return {
+        ok: true,
+        status: 200,
+        json: async () => ({ ok: true }),
+      };
+    }
+    throw new Error(`unexpected fetch: ${url}`);
+  };
+
+  const scope = loadModules(fetchImpl);
+  const events = {
+    completed: null,
+    logs: [],
+  };
+  const executor = scope.MultiPageBackgroundCpaSessionSync.createCpaSessionSyncExecutor({
+    addLog: async (message, level = 'info') => events.logs.push({ message, level }),
+    completeStepFromBackground: async (step, payload) => {
+      events.completed = { step, payload };
+    },
+    createCpaApi: scope.MultiPageBackgroundCpaApi.createCpaApi,
+    fetchImpl,
+    getPanelMode: (state) => state.panelMode || 'cpa',
+  });
+
+  await executor.executeStep10({
+    panelMode: 'cpa',
+    vpsUrl: 'https://cpa.example.com/management.html#/oauth',
+    vpsPassword: 'secret',
+  });
+
+  assert.equal(events.completed.step, 10);
+  assert.deepEqual(events.completed.payload, {
+    verifiedStatus: 'CPA 会话导入完成:step10@example.com',
+    cpaImportedFileName: 'codex-step10@example.com-plus.json',
+    cpaImportedEmail: 'step10@example.com',
+  });
+  assert.equal(fetchCalls.length, 2);
+  assert.equal(fetchCalls[0].url, 'https://chatgpt.com/api/auth/session');
+  assert.equal(fetchCalls[1].options.headers.Authorization, 'Bearer secret');
+  assert.equal(JSON.parse(fetchCalls[1].options.body).access_token, accessToken);
+});
+
+test('step 10 skips CPA sync in sub2api mode', async () => {
+  const scope = loadModules(async () => {
+    throw new Error('fetch should not be called when sub2api is selected');
+  });
+  const events = {
+    completed: null,
+    logs: [],
+  };
+  const executor = scope.MultiPageBackgroundCpaSessionSync.createCpaSessionSyncExecutor({
+    addLog: async (message, level = 'info') => events.logs.push({ message, level }),
+    completeStepFromBackground: async (step, payload) => {
+      events.completed = { step, payload };
+    },
+    createCpaApi: scope.MultiPageBackgroundCpaApi.createCpaApi,
+    getPanelMode: () => 'sub2api',
+  });
+
+  await executor.executeStep10({ panelMode: 'sub2api' });
+
+  assert.deepEqual(events.completed, {
+    step: 10,
+    payload: {
+      cpaSyncSkipped: true,
+      cpaSyncSkipReason: 'sub2api-mode',
+    },
+  });
+  assert.equal(events.logs.some((entry) => /跳过 CPA session 同步/.test(entry.message)), true);
+});

+ 105 - 0
tests/background-step11-payurl.test.js

@@ -0,0 +1,105 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+function loadStep11(fetchImpl) {
+  const source = fs.readFileSync('background/steps/get-plus-link.js', 'utf8');
+  const scope = {};
+  new Function('self', 'fetch', `${source}; return self.MultiPageBackgroundStep11;`)(scope, fetchImpl);
+  return scope.MultiPageBackgroundStep11;
+}
+
+test('step 6 fetches ChatGPT session token and retries payurl checkout', async () => {
+  const fetchCalls = [];
+  const eventsOrder = [];
+  const fetchImpl = async (url, options = {}) => {
+    eventsOrder.push(`fetch:${url}`);
+    fetchCalls.push({ url, options });
+    if (url === 'https://chatgpt.com/api/auth/session') {
+      return {
+        ok: true,
+        status: 200,
+        text: async () => JSON.stringify({ accessToken: 'session-token' }),
+      };
+    }
+    if (url === 'https://payurl.ark2.cn/api/checkout' && fetchCalls.filter((call) => call.url === url).length === 1) {
+      return {
+        ok: false,
+        status: 502,
+        text: async () => JSON.stringify({ error: 'temporary' }),
+      };
+    }
+    if (url === 'https://payurl.ark2.cn/api/checkout') {
+      return {
+        ok: true,
+        status: 200,
+        text: async () => JSON.stringify({
+          checkout_session_id: 'cs_test_123',
+          url: 'https://pay.openai.com/c/pay/cs_test_123#token',
+          chatgpt_checkout_url: 'https://chatgpt.com/checkout/openai_llc/cs_test_123',
+          openai_payurl: 'https://pay.openai.com/c/pay/cs_test_123#token',
+        }),
+      };
+    }
+    throw new Error(`unexpected fetch: ${url}`);
+  };
+
+  const api = loadStep11(fetchImpl);
+  const events = {
+    logs: [],
+    completed: null,
+    navigatedUrl: '',
+  };
+
+  const executor = api.createStep11Executor({
+    addLog: async (message, level = 'info') => events.logs.push({ message, level }),
+    chrome: {
+      scripting: {
+        executeScript: async () => {
+          throw new Error('page fallback should not be used when background session fetch succeeds');
+        },
+      },
+    },
+    completeStepFromBackground: async (step, payload) => {
+      events.completed = { step, payload };
+    },
+    getTabId: async () => 123,
+    reuseOrCreateTab: async (source, url) => {
+      events.navigatedUrl = url;
+      assert.equal(source, 'signup-page');
+    },
+    waitForTabStableComplete: async (tabId, options) => {
+      eventsOrder.push('wait-stable');
+      assert.equal(tabId, 123);
+      assert.equal(options.stableMs, 1500);
+      return { id: tabId, status: 'complete', url: 'https://chatgpt.com/' };
+    },
+  });
+
+  await executor.executeStep11();
+
+  const payurlCalls = fetchCalls.filter((call) => call.url === 'https://payurl.ark2.cn/api/checkout');
+  assert.equal(payurlCalls.length, 2);
+  assert.deepEqual(eventsOrder.slice(0, 2), [
+    'wait-stable',
+    'fetch:https://chatgpt.com/api/auth/session',
+  ]);
+  assert.equal(events.navigatedUrl, 'https://pay.openai.com/c/pay/cs_test_123#token');
+  assert.deepEqual(events.completed, {
+    step: 6,
+    payload: {
+      checkoutUrl: 'https://pay.openai.com/c/pay/cs_test_123#token',
+      checkoutSessionId: 'cs_test_123',
+      chatgptCheckoutUrl: 'https://chatgpt.com/checkout/openai_llc/cs_test_123',
+      openaiPayUrl: 'https://pay.openai.com/c/pay/cs_test_123#token',
+    },
+  });
+
+  const payload = JSON.parse(payurlCalls[1].options.body);
+  assert.equal(payload.token, 'session-token');
+  assert.equal(payload.plan, 'plus');
+  assert.equal(payload.checkout_ui_mode, 'hosted');
+  assert.equal(payload.promo_code, 'STRIPEATLASGPT4BIZ050126');
+  assert.equal(payload.workspace_name, 'linux-do');
+  assert.equal(payload.seat_quantity, 2);
+});

+ 1 - 1
tests/background-step4-a4sky-no-prefetch.test.js

@@ -38,7 +38,7 @@ test('step 4 does not request a fresh code first for A4Sky mailbox', async () =>
   });
 
   await executor.executeStep4({
-    email: 'n20260419100609@a4sky.com',
+    email: 'n20260419100609@edu.a4sky.com',
     password: 'Secret123!',
   });
 

+ 61 - 0
tests/background-step9-paypal-sms.test.js

@@ -0,0 +1,61 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+function loadStep14() {
+  const source = fs.readFileSync('background/steps/fill-paypal-payment.js', 'utf8');
+  const scope = {};
+  new Function('self', `${source}; return self.MultiPageBackgroundStep14;`)(scope);
+  return scope.MultiPageBackgroundStep14;
+}
+
+test('step 9 passes configured PayPal phone and sms api to content script', async () => {
+  const api = loadStep14();
+  let sentMessage = null;
+  const executor = api.createStep14Executor({
+    addLog: async () => {},
+    fetchCheckoutAddress: async () => ({
+      street: '1 Main St',
+      city: 'New York',
+      state: 'New York',
+      zip: '10001',
+    }),
+    generateRandomEmail: () => 'checkout@example.com',
+    sendToContentScriptResilient: async (_source, message) => {
+      sentMessage = message;
+    },
+  });
+
+  await executor.executeStep14({
+    paypalPhone: '+15822201173',
+    paypalSmsApiUrl: 'http://a.62-us.com/api/get_sms?key=test',
+  });
+
+  assert.equal(sentMessage.step, 9);
+  assert.equal(sentMessage.payload.phone, '5822201173');
+  assert.equal(sentMessage.payload.paypalPhone, '+15822201173');
+  assert.equal(sentMessage.payload.paypalSmsApiUrl, 'http://a.62-us.com/api/get_sms?key=test');
+
+  const card = sentMessage.payload.card;
+  assert.equal(card.Credit_Card_Type, 'Visa');
+  assert.match(card.Credit_Card_Number, /^4\d{15}$/);
+  assert.equal(isLuhnValid(card.Credit_Card_Number), true);
+  assert.match(card.Expires, /^(0[1-9]|1[0-2])\/\d{2}$/);
+  assert.match(card.CVV2, /^\d{3}$/);
+});
+
+function isLuhnValid(value) {
+  let sum = 0;
+  let shouldDouble = false;
+  for (let index = String(value).length - 1; index >= 0; index -= 1) {
+    let digit = Number(value[index]);
+    if (!Number.isInteger(digit)) return false;
+    if (shouldDouble) {
+      digit *= 2;
+      if (digit > 9) digit -= 9;
+    }
+    sum += digit;
+    shouldDouble = !shouldDouble;
+  }
+  return sum % 10 === 0;
+}

+ 56 - 0
tests/checkout-api-utils.test.js

@@ -0,0 +1,56 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+function loadCheckoutApiUtils() {
+  const source = fs.readFileSync('background/checkout-api-utils.js', 'utf8');
+  const scope = {};
+  new Function('self', `${source}; return self.MultiPageCheckoutApiUtils;`)(scope);
+  return scope.MultiPageCheckoutApiUtils;
+}
+
+test('extractPaypalSmsCode reads code from json or plain sms text', () => {
+  const api = loadCheckoutApiUtils();
+
+  assert.equal(
+    api.extractPaypalSmsCode(JSON.stringify({ msg: 'PayPal code: 482913. Do not share.' })),
+    '482913'
+  );
+  assert.equal(
+    api.extractPaypalSmsCode('暂无短信'),
+    ''
+  );
+  assert.equal(
+    api.extractPaypalSmsCode('旧码 111111 新码 222222', { excludeCodes: ['111111'] }),
+    '222222'
+  );
+  assert.equal(
+    api.extractPaypalSmsCode("yes|PayPal: 804999 is your security code. Don't share it.|(PayPal)|到期时间:2026-06-29 00:00:00"),
+    '804999'
+  );
+});
+
+test('fetchPaypalSmsCode polls until the sms code appears', async () => {
+  const api = loadCheckoutApiUtils();
+  const calls = [];
+
+  const result = await api.fetchPaypalSmsCode({
+    smsApiUrl: 'http://a.62-us.com/api/get_sms?key=test',
+    maxAttempts: 2,
+    intervalMs: 1,
+  }, {
+    fetchImpl: async (url) => {
+      calls.push(url);
+      return {
+        ok: true,
+        status: 200,
+        text: async () => calls.length === 1 ? 'not ready' : '{"data":"PayPal verification code 654321"}',
+      };
+    },
+    sleep: async () => {},
+  });
+
+  assert.equal(result.code, '654321');
+  assert.equal(result.attempt, 2);
+  assert.equal(calls.length, 2);
+});

+ 10 - 6
tests/step-definitions-module.test.js

@@ -10,11 +10,15 @@ test('step definitions module exposes ordered shared step metadata', () => {
   const steps = api.getSteps();
 
   assert.equal(Array.isArray(steps), true);
-  assert.equal(steps.length >= 10, true);
+  assert.equal(steps.length, 10);
   assert.deepStrictEqual(
     steps.map((step) => step.order),
     steps.map((step) => step.order).slice().sort((left, right) => left - right)
   );
+  assert.deepStrictEqual(
+    steps.map((step) => step.id),
+    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+  );
   assert.deepStrictEqual(
     steps.map((step) => step.key),
     [
@@ -23,11 +27,11 @@ test('step definitions module exposes ordered shared step metadata', () => {
       'fill-password',
       'fetch-signup-code',
       'fill-profile',
-      'clear-login-cookies',
-      'oauth-login',
-      'fetch-login-code',
-      'confirm-oauth',
-      'platform-verify',
+      'get-plus-link',
+      'fill-stripe-checkout',
+      'fill-paypal-login',
+      'fill-paypal-payment',
+      'sync-cpa-session',
     ]
   );
 });