Эх сурвалжийг харах

Refactor signup process and enhance OAuth handling

- Updated signup-page.js to support additional message types for signup entry and password page readiness.
- Introduced new helper functions for managing signup entry states and filling email/password.
- Enhanced error handling and logging for better debugging during the signup process.
- Modified sub2api-panel.js and vps-panel.js to handle REQUEST_OAUTH_URL messages and improved logging.
- Updated sidepanel.html to reflect changes in button labels for clarity in user actions.
QLHazyCoder 4 сар өмнө
parent
commit
6b723c5ef0

+ 23 - 25
README.md

@@ -395,9 +395,9 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
 
 侧边栏共有 9 个步骤按钮,可逐步执行:
 
-1. `Get OAuth Link`
-2. `Open Signup`
-3. `Fill Email / Password`
+1. `Open ChatGPT`
+2. `Signup + Email`
+3. `Fill Password`
 4. `Get Signup Code`
 5. `Fill Name / Birthday`
 6. `Login via OAuth`
@@ -411,11 +411,11 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
 
 当前 Auto 逻辑是:
 
-1. Step 1 获取 CPA OAuth 链接
-2. Step 2 打开 OpenAI 注册页
-3. 根据 `Mail` 选择邮箱来源
-4. 如果 `Mail = Hotmail`,会从账号池自动分配一个可用账号
-5. 如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取邮箱(Duck 或 Cloudflare)
+1. Step 1 打开 `https://chatgpt.com/`
+2. 根据 `Mail` 选择邮箱来源
+3. 如果 `Mail = Hotmail`,会从账号池自动分配一个可用账号
+4. 如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取邮箱(Duck / Cloudflare / iCloud 等)
+5. Step 2 点击注册、填写邮箱并继续到密码页
 6. 如果自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue`
 7. 继续执行 Step 3 ~ Step 9
 
@@ -430,32 +430,30 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
 
 ## 详细步骤说明
 
-### Step 1: Get OAuth Link
+### Step 1: Open ChatGPT
 
-通过 `content/vps-panel.js`:
+通过动态注入的 `content/signup-page.js`:
 
-- 打开 CPA OAuth 面板
-- 等待 `Codex OAuth` 卡片出现
-- 点击“登录”
-- 读取页面里的授权链接
+- 打开 `https://chatgpt.com/`
+- 确认官网首页或注册入口弹窗已经可操作
 
-结果会保存到侧边栏的 `OAuth` 字段
+这一步不再获取 `OAuth` 链接;`OAuth` 链接会在 Step 6 内部按需刷新。
 
-### Step 2: Open Signup
+### Step 2: Signup + Email
 
 通过 `content/signup-page.js`:
 
-- 打开授权链接
-- 查找 `Sign up / Register / 创建账户` 按钮
+- 在官网首页查找 `免费注册 / Sign up / Register / 创建账户`
 - 自动点击进入注册流程
+- 自动填写邮箱
+- 点击 `继续`
+- 等待跳转到 `https://auth.openai.com/create-account/password`
 
-### Step 3: Fill Email / Password
+### Step 3: Fill Password
 
-- 如果侧边栏邮箱为空,会先按当前“邮箱生成”配置自动获取邮箱;失败时再提示手动粘贴
-- 自动填写邮箱
-- 如页面先要求邮箱,再进入密码页,会自动切页继续填写
+- 使用第 2 步已经确定好的邮箱
 - 使用自定义密码或自动生成密码
-- 提交注册表单
+- 在密码页填写密码并提交注册表单
 
 实际使用的密码会写入会话状态,并同步到侧边栏显示。
 
@@ -625,8 +623,8 @@ background.js              后台主控,编排 1~9 步、Tab 复用、状态
 manifest.json              扩展清单
 data/names.js              随机姓名、生日数据
 content/utils.js           通用工具:等待元素、点击、日志、停止控制
-content/vps-panel.js       CPA 面板步骤:Step 1 / Step 9
-content/signup-page.js     OpenAI 注册/登录页步骤:Step 2 / 3 / 5 / 6 / 8
+content/vps-panel.js       CPA 面板步骤:内部 OAuth 刷新 / Step 9
+content/signup-page.js     ChatGPT 官网 + OpenAI 注册/登录页步骤:Step 1 / 2 / 3 / 5 / 6 / 8
 hotmail-utils.js           Hotmail 收信相关通用辅助
 content/duck-mail.js       Duck 邮箱自动获取
 content/qq-mail.js         QQ 邮箱验证码轮询

+ 230 - 64
background.js

@@ -3160,6 +3160,13 @@ function isSignupPageHost(hostname = '') {
   return ['auth0.openai.com', 'auth.openai.com', 'accounts.openai.com'].includes(hostname);
 }
 
+function isSignupPasswordPageUrl(rawUrl) {
+  const parsed = parseUrlSafely(rawUrl);
+  if (!parsed) return false;
+  return isSignupPageHost(parsed.hostname)
+    && /\/create-account\/password(?:[/?#]|$)/i.test(parsed.pathname || '');
+}
+
 function is163MailHost(hostname = '') {
   return hostname === 'mail.163.com'
     || hostname.endsWith('.mail.163.com')
@@ -3381,6 +3388,26 @@ async function waitForTabUrlFamily(source, tabId, referenceUrl, options = {}) {
   return null;
 }
 
+async function waitForTabUrlMatch(tabId, matcher, options = {}) {
+  const { timeoutMs = 15000, retryDelayMs = 400 } = options;
+  const start = Date.now();
+
+  while (Date.now() - start < timeoutMs) {
+    try {
+      const tab = await chrome.tabs.get(tabId);
+      if (matcher(tab.url || '', tab)) {
+        return tab;
+      }
+    } catch {
+      return null;
+    }
+
+    await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
+  }
+
+  return null;
+}
+
 async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
   const {
     inject = null,
@@ -5118,8 +5145,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([4, 6, 7, 8]);
-const STEP_COMPLETION_SIGNAL_STEPS = new Set([1, 2, 3, 5, 9]);
+const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 7, 8]);
+const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 9]);
 
 function waitForStepComplete(step, timeoutMs = 120000) {
   return new Promise((resolve, reject) => {
@@ -5755,19 +5782,20 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
   if (continued) {
     await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续当前进度,从步骤 ${startStep} 开始(第 ${attemptRuns} 次尝试)===`, 'info');
   } else {
-    await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,获取 OAuth 链接并打开注册页 ===`, 'info');
+    await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,打开官网并进入密码页 ===`, 'info');
+  }
+
+  if (startStep <= 1) {
+    await executeStepAndWait(1, AUTO_STEP_DELAYS[1]);
   }
 
   if (startStep <= 2) {
-    for (const step of [1, 2]) {
-      if (step < startStep) continue;
-      await executeStepAndWait(step, AUTO_STEP_DELAYS[step]);
-    }
+    await ensureAutoEmailReady(targetRun, totalRuns, attemptRuns);
+    await executeStepAndWait(2, AUTO_STEP_DELAYS[2]);
   }
 
   if (startStep <= 3) {
-    await ensureAutoEmailReady(targetRun, totalRuns, attemptRuns);
-    await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,注册、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info');
+    await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,填写密码、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info');
     await broadcastAutoRunStatus('running', {
       currentRun: targetRun,
       totalRuns,
@@ -6416,63 +6444,71 @@ async function resumeAutoRun() {
 }
 
 // ============================================================
-// Step 1: Get OAuth Link
+// Signup / OAuth Helpers
 // ============================================================
 
-async function executeStep1(state) {
+const SIGNUP_ENTRY_URL = 'https://chatgpt.com/';
+const SIGNUP_PAGE_INJECT_FILES = ['content/utils.js', 'content/signup-page.js'];
+
+async function requestOAuthUrlFromPanel(state, options = {}) {
   if (getPanelMode(state) === 'sub2api') {
-    return executeSub2ApiStep1(state);
+    return requestSub2ApiOAuthUrl(state, options);
   }
-  return executeCpaStep1(state);
+  return requestCpaOAuthUrl(state, options);
 }
 
-async function executeCpaStep1(state) {
+async function requestCpaOAuthUrl(state, options = {}) {
+  const { logLabel = 'OAuth 刷新' } = options;
   if (!state.vpsUrl) {
     throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
   }
-  await addLog('步骤 1:正在打开 CPA 面板...');
 
-  const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
+  await addLog(`${logLabel}:正在打开 CPA 面板...`);
 
+  const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
   await closeConflictingTabsForSource('vps-panel', state.vpsUrl);
 
   const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true });
   const tabId = tab.id;
   await rememberSourceLastUrl('vps-panel', state.vpsUrl);
 
-  await addLog('步骤 1:CPA 面板已打开,正在等待页面进入目标地址...');
+  await addLog(`${logLabel}:CPA 面板已打开,正在等待页面进入目标地址...`);
   const matchedTab = await waitForTabUrlFamily('vps-panel', tabId, state.vpsUrl, {
     timeoutMs: 15000,
     retryDelayMs: 400,
   });
   if (!matchedTab) {
-    await addLog('步骤 1:CPA 页面尚未完全进入目标地址,继续尝试连接内容脚本...', 'warn');
+    await addLog(`${logLabel}:CPA 页面尚未完全进入目标地址,继续尝试连接内容脚本...`, 'warn');
   }
 
   await ensureContentScriptReadyOnTab('vps-panel', tabId, {
     inject: injectFiles,
     timeoutMs: 45000,
     retryDelayMs: 900,
-    logMessage: '步骤 1:CPA 面板仍在加载,正在重试连接内容脚本...',
+    logMessage: `${logLabel}:CPA 面板仍在加载,正在重试连接内容脚本...`,
   });
 
   const result = await sendToContentScriptResilient('vps-panel', {
-    type: 'EXECUTE_STEP',
-    step: 1,
+    type: 'REQUEST_OAUTH_URL',
     source: 'background',
-    payload: { vpsPassword: state.vpsPassword },
+    payload: {
+      vpsPassword: state.vpsPassword,
+      logStep: 6,
+    },
   }, {
     timeoutMs: 30000,
     retryDelayMs: 700,
-    logMessage: '步骤 1:CPA 面板通信未就绪,正在等待页面恢复...',
+    logMessage: `${logLabel}:CPA 面板通信未就绪,正在等待页面恢复...`,
   });
 
   if (result?.error) {
     throw new Error(result.error);
   }
+  return result || {};
 }
 
-async function executeSub2ApiStep1(state) {
+async function requestSub2ApiOAuthUrl(state, options = {}) {
+  const { logLabel = 'OAuth 刷新' } = options;
   const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
   const groupName = (state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME).trim() || DEFAULT_SUB2API_GROUP_NAME;
 
@@ -6483,23 +6519,22 @@ async function executeSub2ApiStep1(state) {
     throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。');
   }
 
-  await addLog('步骤 1:正在打开 SUB2API 后台...');
+  await addLog(`${logLabel}:正在打开 SUB2API 后台...`);
 
   const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
-
   await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl);
 
   const tab = await chrome.tabs.create({ url: sub2apiUrl, active: true });
   const tabId = tab.id;
   await rememberSourceLastUrl('sub2api-panel', sub2apiUrl);
 
-  await addLog('步骤 1:SUB2API 页面已打开,正在等待页面进入目标地址...');
+  await addLog(`${logLabel}:SUB2API 页面已打开,正在等待页面进入目标地址...`);
   const matchedTab = await waitForTabUrlFamily('sub2api-panel', tabId, sub2apiUrl, {
     timeoutMs: 15000,
     retryDelayMs: 400,
   });
   if (!matchedTab) {
-    await addLog('步骤 1:SUB2API 页面尚未稳定,继续尝试连接内容脚本...', 'warn');
+    await addLog(`${logLabel}:SUB2API 页面尚未稳定,继续尝试连接内容脚本...`, 'warn');
   }
 
   await ensureContentScriptReadyOnTab('sub2api-panel', tabId, {
@@ -6507,18 +6542,18 @@ async function executeSub2ApiStep1(state) {
     injectSource: 'sub2api-panel',
     timeoutMs: 45000,
     retryDelayMs: 900,
-    logMessage: '步骤 1:SUB2API 页面仍在加载,正在重试连接内容脚本...',
+    logMessage: `${logLabel}:SUB2API 页面仍在加载,正在重试连接内容脚本...`,
   });
 
   const result = await sendToContentScript('sub2api-panel', {
-    type: 'EXECUTE_STEP',
-    step: 1,
+    type: 'REQUEST_OAUTH_URL',
     source: 'background',
     payload: {
       sub2apiUrl,
       sub2apiEmail: state.sub2apiEmail,
       sub2apiPassword: state.sub2apiPassword,
       sub2apiGroupName: groupName,
+      logStep: 6,
     },
   }, {
     responseTimeoutMs: SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
@@ -6527,32 +6562,86 @@ async function executeSub2ApiStep1(state) {
   if (result?.error) {
     throw new Error(result.error);
   }
+  return result || {};
 }
 
-// ============================================================
-// Step 2: Open Signup Page (Background opens tab, signup-page.js clicks Register)
-// ============================================================
+async function openSignupEntryTab(step = 1) {
+  const tabId = await reuseOrCreateTab('signup-page', SIGNUP_ENTRY_URL, {
+    inject: SIGNUP_PAGE_INJECT_FILES,
+    injectSource: 'signup-page',
+  });
 
-async function executeStep2(state) {
-  if (!state.oauthUrl) {
-    throw new Error('缺少 OAuth 链接,请先完成步骤 1。');
-  }
-  await addLog('步骤 2:正在打开认证链接...');
-  await reuseOrCreateTab('signup-page', state.oauthUrl);
+  await ensureContentScriptReadyOnTab('signup-page', tabId, {
+    inject: SIGNUP_PAGE_INJECT_FILES,
+    injectSource: 'signup-page',
+    timeoutMs: 45000,
+    retryDelayMs: 900,
+    logMessage: `步骤 ${step}:ChatGPT 官网仍在加载,正在重试连接内容脚本...`,
+  });
 
-  await sendToContentScript('signup-page', {
-    type: 'EXECUTE_STEP',
-    step: 2,
+  return tabId;
+}
+
+async function ensureSignupEntryPageReady(step = 1) {
+  const tabId = await openSignupEntryTab(step);
+  const result = await sendToContentScriptResilient('signup-page', {
+    type: 'ENSURE_SIGNUP_ENTRY_READY',
+    step,
     source: 'background',
     payload: {},
+  }, {
+    timeoutMs: 20000,
+    retryDelayMs: 700,
+    logMessage: `步骤 ${step}:官网注册入口正在切换,等待页面恢复...`,
   });
+
+  if (result?.error) {
+    throw new Error(result.error);
+  }
+
+  return { tabId, result: result || {} };
 }
 
-// ============================================================
-// Step 3: Fill Email & Password (via signup-page.js)
-// ============================================================
+async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) {
+  const { skipUrlWait = false } = options;
 
-async function executeStep3(state) {
+  if (!skipUrlWait) {
+    const matchedTab = await waitForTabUrlMatch(tabId, (url) => isSignupPasswordPageUrl(url), {
+      timeoutMs: 45000,
+      retryDelayMs: 300,
+    });
+    if (!matchedTab) {
+      throw new Error('等待进入密码页超时,请检查邮箱提交后页面是否仍停留在官网或邮箱页。');
+    }
+  }
+
+  await ensureContentScriptReadyOnTab('signup-page', tabId, {
+    inject: SIGNUP_PAGE_INJECT_FILES,
+    injectSource: 'signup-page',
+    timeoutMs: 45000,
+    retryDelayMs: 900,
+    logMessage: `步骤 ${step}:密码页仍在加载,正在重试连接内容脚本...`,
+  });
+
+  const result = await sendToContentScriptResilient('signup-page', {
+    type: 'ENSURE_SIGNUP_PASSWORD_PAGE_READY',
+    step,
+    source: 'background',
+    payload: {},
+  }, {
+    timeoutMs: 20000,
+    retryDelayMs: 700,
+    logMessage: `步骤 ${step}:认证页正在切换,等待密码页重新就绪...`,
+  });
+
+  if (result?.error) {
+    throw new Error(result.error);
+  }
+
+  return result || {};
+}
+
+async function resolveSignupEmailForFlow(state) {
   let resolvedEmail = state.email;
   if (isHotmailProvider(state)) {
     const account = await ensureHotmailAccountForFlow({
@@ -6572,19 +6661,102 @@ async function executeStep3(state) {
     throw new Error('缺少邮箱地址,请先在侧边栏粘贴邮箱。');
   }
 
-  const password = state.customPassword || generatePassword();
+  return resolvedEmail;
+}
+
+// ============================================================
+// Step 1: Open ChatGPT homepage
+// ============================================================
+
+async function executeStep1() {
+  await addLog('步骤 1:正在打开 ChatGPT 官网...');
+  await ensureSignupEntryPageReady(1);
+  await completeStepFromBackground(1, {});
+}
+
+// ============================================================
+// Step 2: Click signup, fill email, continue to password page
+// ============================================================
+
+async function executeStep2(state) {
+  const resolvedEmail = await resolveSignupEmailForFlow(state);
   if (resolvedEmail !== state.email) {
     await setEmailState(resolvedEmail);
   }
+
+  let signupTabId = await getTabId('signup-page');
+  if (!signupTabId || !(await isTabAlive('signup-page'))) {
+    await addLog('步骤 2:未发现可用的注册页标签,正在重新打开 ChatGPT 官网...', 'warn');
+    signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
+  } else {
+    await chrome.tabs.update(signupTabId, { active: true });
+    await ensureContentScriptReadyOnTab('signup-page', signupTabId, {
+      inject: SIGNUP_PAGE_INJECT_FILES,
+      injectSource: 'signup-page',
+      timeoutMs: 45000,
+      retryDelayMs: 900,
+      logMessage: '步骤 2:注册入口页内容脚本未就绪,正在等待页面恢复...',
+    });
+  }
+
+  const step2Result = await sendToContentScriptResilient('signup-page', {
+    type: 'EXECUTE_STEP',
+    step: 2,
+    source: 'background',
+    payload: { email: resolvedEmail },
+  }, {
+    timeoutMs: 20000,
+    retryDelayMs: 700,
+    logMessage: '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...',
+  });
+
+  if (step2Result?.error) {
+    throw new Error(step2Result.error);
+  }
+
+  if (!step2Result?.alreadyOnPasswordPage) {
+    await addLog(`步骤 2:邮箱 ${resolvedEmail} 已提交,正在等待进入密码页...`);
+  }
+
+  await ensureSignupPasswordPageReadyInTab(signupTabId, 2, {
+    skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage),
+  });
+  await completeStepFromBackground(2, {});
+}
+
+// ============================================================
+// Step 3: Fill Password (via signup-page.js)
+// ============================================================
+
+async function executeStep3(state) {
+  const resolvedEmail = state.email;
+  if (!resolvedEmail) {
+    throw new Error('缺少邮箱地址,请先完成步骤 2。');
+  }
+
+  const signupTabId = await getTabId('signup-page');
+  if (!signupTabId || !(await isTabAlive('signup-page'))) {
+    throw new Error('认证页面标签页已关闭,请先重新完成步骤 2。');
+  }
+
+  const password = state.customPassword || generatePassword();
   await setPasswordState(password);
 
-  // Save account record
   const accounts = state.accounts || [];
   accounts.push({ email: resolvedEmail, password, createdAt: new Date().toISOString() });
   await setState({ accounts });
 
+  await chrome.tabs.update(signupTabId, { active: true });
+  await ensureContentScriptReadyOnTab('signup-page', signupTabId, {
+    inject: SIGNUP_PAGE_INJECT_FILES,
+    injectSource: 'signup-page',
+    timeoutMs: 45000,
+    retryDelayMs: 900,
+    logMessage: '步骤 3:密码页内容脚本未就绪,正在等待页面恢复...',
+  });
+
   await addLog(
-    `步骤 3:正在填写邮箱 ${resolvedEmail},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)`
+    `步骤 3:正在填写密码,邮箱 ${resolvedEmail},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)`
   );
   await sendToContentScript('signup-page', {
     type: 'EXECUTE_STEP',
@@ -7218,20 +7390,15 @@ async function executeStep5(state) {
 
 async function refreshOAuthUrlBeforeStep6(state) {
   await addLog(`步骤 6:正在刷新登录用的 ${getPanelModeLabel(state)} OAuth 链接...`);
-  console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] preparing fresh OAuth via step 1');
-  const waitForFreshOAuth = waitForStepComplete(1, 120000);
-  console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] executing step 1 for fresh OAuth');
-  await executeStep1(state);
-  console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] step 1 execute returned, waiting for completion signal');
-  await waitForFreshOAuth;
-  console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] step 1 completion signal received');
+  console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel');
+  const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: '步骤 6' });
+  await handleStepData(1, refreshResult);
 
-  const latestState = await getState();
-  if (!latestState.oauthUrl) {
+  if (!refreshResult?.oauthUrl) {
     throw new Error('刷新 OAuth 链接后仍未拿到可用链接。');
   }
 
-  return latestState.oauthUrl;
+  return refreshResult.oauthUrl;
 }
 
 function isStep6SuccessResult(result) {
@@ -7368,7 +7535,7 @@ async function runStep7Attempt(state) {
     await chrome.tabs.update(authTabId, { active: true });
   } else {
     if (!state.oauthUrl) {
-      throw new Error('缺少 OAuth 链接,请先完成步骤 1。');
+      throw new Error('缺少登录用 OAuth 链接,请先完成步骤 6。');
     }
     await reuseOrCreateTab('signup-page', state.oauthUrl);
   }
@@ -7481,7 +7648,6 @@ const STEP8_CLICK_EFFECT_TIMEOUT_MS = 15000;
 const STEP8_CLICK_RETRY_DELAY_MS = 500;
 const STEP8_READY_WAIT_TIMEOUT_MS = 30000;
 const STEP8_MAX_ROUNDS = 5;
-const STEP8_SIGNUP_PAGE_INJECT_FILES = ['content/utils.js', 'content/signup-page.js'];
 const STEP8_STRATEGIES = [
   { mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
   { mode: 'debugger', label: 'debugger click' },
@@ -7520,7 +7686,7 @@ function throwIfStep8SettledOrStopped(isSettled = false) {
 
 async function ensureStep8SignupPageReady(tabId, options = {}) {
   await ensureContentScriptReadyOnTab('signup-page', tabId, {
-    inject: STEP8_SIGNUP_PAGE_INJECT_FILES,
+    inject: SIGNUP_PAGE_INJECT_FILES,
     injectSource: 'signup-page',
     timeoutMs: options.timeoutMs ?? 15000,
     retryDelayMs: options.retryDelayMs ?? 600,
@@ -7720,7 +7886,7 @@ function getStep8EffectLabel(effect) {
 
 async function executeStep8(state) {
   if (!state.oauthUrl) {
-    throw new Error('缺少 OAuth 链接,请先完成步骤 1。');
+    throw new Error('缺少登录用 OAuth 链接,请先完成步骤 6。');
   }
 
   await addLog('步骤 8:正在监听 localhost 回调地址...');

+ 276 - 96
content/signup-page.js

@@ -1,48 +1,63 @@
-// content/signup-page.js — Content script for OpenAI auth pages (steps 2, 3, 4-receive, 5)
+// content/signup-page.js — Content script for ChatGPT signup entry + OpenAI auth pages
 // Injected on: auth0.openai.com, auth.openai.com, accounts.openai.com
+// Dynamically injected on: chatgpt.com
 
 console.log('[MultiPage:signup-page] Content script loaded on', location.href);
 
-// Listen for commands from Background
-chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
-  if (
-    message.type === 'EXECUTE_STEP'
-    || message.type === 'FILL_CODE'
-    || message.type === 'STEP8_FIND_AND_CLICK'
-    || message.type === 'STEP8_GET_STATE'
-    || message.type === 'STEP8_TRIGGER_CONTINUE'
-    || message.type === 'GET_LOGIN_AUTH_STATE'
-    || message.type === 'PREPARE_SIGNUP_VERIFICATION'
-    || message.type === 'RESEND_VERIFICATION_CODE'
-  ) {
-    resetStopState();
-    handleCommand(message).then((result) => {
-      sendResponse({ ok: true, ...(result || {}) });
-    }).catch(err => {
-      if (isStopError(err)) {
-        log(`步骤 ${message.step || 8}:已被用户停止。`, 'warn');
-        sendResponse({ stopped: true, error: err.message });
-        return;
-      }
+const SIGNUP_PAGE_LISTENER_SENTINEL = 'data-multipage-signup-page-listener';
+
+if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1') {
+  document.documentElement.setAttribute(SIGNUP_PAGE_LISTENER_SENTINEL, '1');
+
+  // Listen for commands from Background
+  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+    if (
+      message.type === 'EXECUTE_STEP'
+      || message.type === 'FILL_CODE'
+      || message.type === 'STEP8_FIND_AND_CLICK'
+      || message.type === 'STEP8_GET_STATE'
+      || message.type === 'STEP8_TRIGGER_CONTINUE'
+      || message.type === 'GET_LOGIN_AUTH_STATE'
+      || message.type === 'PREPARE_SIGNUP_VERIFICATION'
+      || message.type === 'RESEND_VERIFICATION_CODE'
+      || message.type === 'ENSURE_SIGNUP_ENTRY_READY'
+      || message.type === 'ENSURE_SIGNUP_PASSWORD_PAGE_READY'
+    ) {
+      resetStopState();
+      handleCommand(message).then((result) => {
+        sendResponse({ ok: true, ...(result || {}) });
+      }).catch(err => {
+        if (isStopError(err)) {
+          if (message.step) {
+            log(`步骤 ${message.step || 8}:已被用户停止。`, 'warn');
+          }
+          sendResponse({ stopped: true, error: err.message });
+          return;
+        }
 
-      if (message.type === 'STEP8_FIND_AND_CLICK') {
-        log(`步骤 8:${err.message}`, 'error');
-        sendResponse({ error: err.message });
-        return;
-      }
+        if (message.type === 'STEP8_FIND_AND_CLICK') {
+          log(`步骤 8:${err.message}`, 'error');
+          sendResponse({ error: err.message });
+          return;
+        }
 
-      reportError(message.step, err.message);
-      sendResponse({ error: err.message });
-    });
-    return true;
-  }
-});
+        if (message.step) {
+          reportError(message.step, err.message);
+        }
+        sendResponse({ error: err.message });
+      });
+      return true;
+    }
+  });
+} else {
+  console.log('[MultiPage:signup-page] 消息监听已存在,跳过重复注册');
+}
 
 async function handleCommand(message) {
   switch (message.type) {
     case 'EXECUTE_STEP':
       switch (message.step) {
-        case 2: return await step2_clickRegister();
+        case 2: return await step2_clickRegister(message.payload);
         case 3: return await step3_fillEmailPassword(message.payload);
         case 5: return await step5_fillNameBirthday(message.payload);
         case 6: return await step6_login(message.payload);
@@ -58,6 +73,10 @@ async function handleCommand(message) {
       return await prepareSignupVerificationFlow(message.payload);
     case 'RESEND_VERIFICATION_CODE':
       return await resendVerificationCode(message.step);
+    case 'ENSURE_SIGNUP_ENTRY_READY':
+      return await ensureSignupEntryReady();
+    case 'ENSURE_SIGNUP_PASSWORD_PAGE_READY':
+      return await ensureSignupPasswordPageReady();
     case 'STEP8_FIND_AND_CLICK':
       return await step8_findAndClick();
     case 'STEP8_GET_STATE':
@@ -212,91 +231,252 @@ async function resendVerificationCode(step, timeout = 45000) {
 }
 
 // ============================================================
-// Step 2: Click Register
+// Signup Entry Helpers
 // ============================================================
 
-async function step2_clickRegister() {
-  log('步骤 2:正在查找注册按钮...');
+const SIGNUP_ENTRY_TRIGGER_PATTERN = /免费注册|立即注册|注册|sign\s*up|register|create\s*account|create\s+account/i;
+const SIGNUP_EMAIL_INPUT_SELECTOR = 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i]';
 
-  let registerBtn = null;
-  try {
-    registerBtn = await waitForElementByText(
-      'a, button, [role="button"], [role="link"]',
-      /sign\s*up|register|create\s*account|注册/i,
-      10000
-    );
-  } catch {
-    // Some pages may have a direct link
-    try {
-      registerBtn = await waitForElement('a[href*="signup"], a[href*="register"]', 5000);
-    } catch {
-      throw new Error(
-        '未找到注册按钮。' +
-        '请在 DevTools 中检查认证页面 DOM。URL: ' + location.href
-      );
+function getSignupEmailInput() {
+  const input = document.querySelector(SIGNUP_EMAIL_INPUT_SELECTOR);
+  return input && isVisibleElement(input) ? input : null;
+}
+
+function getSignupEmailContinueButton({ allowDisabled = false } = {}) {
+  const direct = document.querySelector('button[type="submit"], input[type="submit"]');
+  if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) {
+    return direct;
+  }
+
+  const candidates = document.querySelectorAll(
+    'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
+  );
+  return Array.from(candidates).find((el) => {
+    if (!isVisibleElement(el) || (!allowDisabled && !isActionEnabled(el))) return false;
+    return /continue|next|submit|继续|下一步/i.test(getActionText(el));
+  }) || null;
+}
+
+function findSignupEntryTrigger() {
+  const candidates = document.querySelectorAll('a, button, [role="button"], [role="link"]');
+  return Array.from(candidates).find((el) => {
+    if (!isVisibleElement(el) || !isActionEnabled(el)) return false;
+    return SIGNUP_ENTRY_TRIGGER_PATTERN.test(getActionText(el));
+  }) || null;
+}
+
+function getSignupPasswordDisplayedEmail() {
+  const text = (document.body?.innerText || document.body?.textContent || '')
+    .replace(/\s+/g, ' ')
+    .trim();
+  const matches = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig);
+  return matches?.[0] ? String(matches[0]).trim().toLowerCase() : '';
+}
+
+function inspectSignupEntryState() {
+  const passwordInput = getSignupPasswordInput();
+  if (isSignupPasswordPage() && passwordInput) {
+    return {
+      state: 'password_page',
+      passwordInput,
+      submitButton: getSignupPasswordSubmitButton({ allowDisabled: true }),
+      displayedEmail: getSignupPasswordDisplayedEmail(),
+      url: location.href,
+    };
+  }
+
+  const emailInput = getSignupEmailInput();
+  if (emailInput) {
+    return {
+      state: 'email_entry',
+      emailInput,
+      continueButton: getSignupEmailContinueButton({ allowDisabled: true }),
+      url: location.href,
+    };
+  }
+
+  const signupTrigger = findSignupEntryTrigger();
+  if (signupTrigger) {
+    return {
+      state: 'entry_home',
+      signupTrigger,
+      url: location.href,
+    };
+  }
+
+  return {
+    state: 'unknown',
+    url: location.href,
+  };
+}
+
+async function waitForSignupEntryState(options = {}) {
+  const {
+    timeout = 15000,
+    autoOpenEntry = false,
+  } = options;
+  const start = Date.now();
+  let lastTriggerClickAt = 0;
+
+  while (Date.now() - start < timeout) {
+    throwIfStopped();
+    const snapshot = inspectSignupEntryState();
+
+    if (snapshot.state === 'password_page' || snapshot.state === 'email_entry') {
+      return snapshot;
+    }
+
+    if (snapshot.state === 'entry_home') {
+      if (!autoOpenEntry) {
+        return snapshot;
+      }
+
+      if (Date.now() - lastTriggerClickAt >= 1500) {
+        lastTriggerClickAt = Date.now();
+        log('步骤 2:正在点击官网注册入口...');
+        await humanPause(350, 900);
+        simulateClick(snapshot.signupTrigger);
+      }
     }
+
+    await sleep(250);
   }
 
-  await humanPause(450, 1200);
-  reportComplete(2);
-  simulateClick(registerBtn);
-  log('步骤 2:已点击注册按钮');
+  return inspectSignupEntryState();
+}
+
+async function ensureSignupEntryReady(timeout = 15000) {
+  const snapshot = await waitForSignupEntryState({ timeout, autoOpenEntry: false });
+  if (snapshot.state === 'entry_home' || snapshot.state === 'email_entry' || snapshot.state === 'password_page') {
+    return {
+      ready: true,
+      state: snapshot.state,
+      url: snapshot.url || location.href,
+    };
+  }
+
+  throw new Error('当前页面没有可用的注册入口,也不在邮箱/密码页。URL: ' + location.href);
+}
+
+async function ensureSignupPasswordPageReady(timeout = 20000) {
+  const start = Date.now();
+
+  while (Date.now() - start < timeout) {
+    throwIfStopped();
+    const passwordInput = getSignupPasswordInput();
+    if (isSignupPasswordPage() && passwordInput) {
+      return {
+        ready: true,
+        state: 'password_page',
+        url: location.href,
+      };
+    }
+    await sleep(200);
+  }
+
+  throw new Error('等待进入密码页超时。URL: ' + location.href);
+}
+
+async function fillSignupEmailAndContinue(email, step) {
+  if (!email) throw new Error(`未提供邮箱地址,步骤 ${step} 无法继续。`);
+  const normalizedEmail = String(email || '').trim().toLowerCase();
+
+  const snapshot = await waitForSignupEntryState({
+    timeout: 20000,
+    autoOpenEntry: true,
+  });
+
+  if (snapshot.state === 'password_page') {
+    if (snapshot.displayedEmail && snapshot.displayedEmail !== normalizedEmail) {
+      throw new Error(`步骤 ${step}:当前密码页邮箱为 ${snapshot.displayedEmail},与目标邮箱 ${email} 不一致,请先回到步骤 1 重新开始。`);
+    }
+    log(`步骤 ${step}:当前已在密码页,无需重复提交邮箱。`);
+    return {
+      alreadyOnPasswordPage: true,
+      url: snapshot.url || location.href,
+    };
+  }
+
+  if (snapshot.state !== 'email_entry' || !snapshot.emailInput) {
+    throw new Error(`步骤 ${step}:未找到可用的邮箱输入入口。URL: ${location.href}`);
+  }
+
+  log(`步骤 ${step}:正在填写邮箱:${email}`);
+  await humanPause(500, 1400);
+  fillInput(snapshot.emailInput, email);
+  log(`步骤 ${step}:邮箱已填写`);
+
+  const continueButton = snapshot.continueButton || getSignupEmailContinueButton({ allowDisabled: true });
+  if (!continueButton || !isActionEnabled(continueButton)) {
+    throw new Error(`步骤 ${step}:未找到可点击的“继续”按钮。URL: ${location.href}`);
+  }
+
+  log(`步骤 ${step}:邮箱已准备提交,正在前往密码页...`);
+  window.setTimeout(() => {
+    try {
+      simulateClick(continueButton);
+    } catch (error) {
+      console.error('[MultiPage:signup-page] deferred signup email submit failed:', error?.message || error);
+    }
+  }, 120);
+
+  return {
+    submitted: true,
+    email,
+    url: location.href,
+  };
 }
 
 // ============================================================
-// Step 3: Fill Email & Password
+// Step 2: Click Register, fill email, then continue to password page
 // ============================================================
 
-async function step3_fillEmailPassword(payload) {
+async function step2_clickRegister(payload = {}) {
   const { email } = payload;
-  if (!email) throw new Error('未提供邮箱地址,请先在侧边栏粘贴邮箱。');
+  return fillSignupEmailAndContinue(email, 2);
+}
 
-  log(`步骤 3:正在填写邮箱:${email}`);
+// ============================================================
+// Step 3: Fill Password
+// ============================================================
 
-  // Find email input
-  let emailInput = null;
-  try {
-    emailInput = await waitForElement(
-      'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email"], input[placeholder*="Email"]',
-      10000
-    );
-  } catch {
-    throw new Error('在注册页未找到邮箱输入框。URL: ' + location.href);
+async function step3_fillEmailPassword(payload) {
+  const { email, password } = payload;
+  if (!password) throw new Error('未提供密码,步骤 3 需要可用密码。');
+  const normalizedEmail = String(email || '').trim().toLowerCase();
+
+  let snapshot = inspectSignupEntryState();
+  if (snapshot.state === 'entry_home') {
+    throw new Error('当前仍停留在 ChatGPT 官网首页,请先完成步骤 2。');
   }
 
-  await humanPause(500, 1400);
-  fillInput(emailInput, email);
-  log('步骤 3:邮箱已填写');
-
-  // Check if password field is on the same page
-  let passwordInput = document.querySelector('input[type="password"]');
-
-  if (!passwordInput) {
-    // Need to submit email first to get to password page
-    log('步骤 3:暂未发现密码输入框,先提交邮箱...');
-    const submitBtn = document.querySelector('button[type="submit"]')
-      || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
-
-    if (submitBtn) {
-      await humanPause(400, 1100);
-      simulateClick(submitBtn);
-      log('步骤 3:邮箱已提交,正在等待密码输入框...');
-      await sleep(2000);
+  if (snapshot.state === 'email_entry') {
+    const transition = await fillSignupEmailAndContinue(email, 3);
+    if (!transition.alreadyOnPasswordPage) {
+      await sleep(1200);
+      await ensureSignupPasswordPageReady();
     }
+    snapshot = inspectSignupEntryState();
+  }
 
-    try {
-      passwordInput = await waitForElement('input[type="password"]', 10000);
-    } catch {
-      throw new Error('提交邮箱后仍未找到密码输入框。URL: ' + location.href);
-    }
+  if (snapshot.state !== 'password_page' || !snapshot.passwordInput) {
+    await ensureSignupPasswordPageReady();
+    snapshot = inspectSignupEntryState();
+  }
+
+  if (snapshot.state !== 'password_page' || !snapshot.passwordInput) {
+    throw new Error('在密码页未找到密码输入框。URL: ' + location.href);
+  }
+  if (normalizedEmail && snapshot.displayedEmail && snapshot.displayedEmail !== normalizedEmail) {
+    throw new Error(`当前密码页邮箱为 ${snapshot.displayedEmail},与目标邮箱 ${email} 不一致,请先回到步骤 1 重新开始。`);
   }
 
-  if (!payload.password) throw new Error('未提供密码,步骤 3 需要可用密码。');
   await humanPause(600, 1500);
-  fillInput(passwordInput, payload.password);
+  fillInput(snapshot.passwordInput, password);
   log('步骤 3:密码已填写');
 
-  const submitBtn = document.querySelector('button[type="submit"]')
+  const submitBtn = snapshot.submitButton
+    || getSignupPasswordSubmitButton({ allowDisabled: true })
     || await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
 
   // Report complete BEFORE submit, because submit causes page navigation

+ 28 - 11
content/sub2api-panel.js

@@ -13,17 +13,24 @@ if (document.documentElement.getAttribute(SUB2API_PANEL_LISTENER_SENTINEL) !== '
   document.documentElement.setAttribute(SUB2API_PANEL_LISTENER_SENTINEL, '1');
 
   chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
-    if (message.type === 'EXECUTE_STEP') {
+    if (message.type === 'EXECUTE_STEP' || message.type === 'REQUEST_OAUTH_URL') {
       resetStopState();
-      handleStep(message.step, message.payload).then(() => {
-        sendResponse({ ok: true });
+      const handler = message.type === 'REQUEST_OAUTH_URL'
+        ? requestOAuthUrl(message.payload)
+        : handleStep(message.step, message.payload);
+      handler.then((result) => {
+        sendResponse({ ok: true, ...(result || {}) });
       }).catch((err) => {
         if (isStopError(err)) {
-          log(`步骤 ${message.step}:已被用户停止。`, 'warn');
+          if (message.step) {
+            log(`步骤 ${message.step}:已被用户停止。`, 'warn');
+          }
           sendResponse({ stopped: true, error: err.message });
           return;
         }
-        reportError(message.step, err.message);
+        if (message.step) {
+          reportError(message.step, err.message);
+        }
         sendResponse({ error: err.message });
       });
       return true;
@@ -66,6 +73,10 @@ async function handleStep(step, payload = {}) {
   }
 }
 
+async function requestOAuthUrl(payload = {}) {
+  return step1_generateOpenAiAuthUrl(payload, { report: false });
+}
+
 async function requestJson(origin, path, options = {}) {
   throwIfStopped();
   const {
@@ -287,7 +298,9 @@ function openAccountsPageSoon(origin) {
   }, 500);
 }
 
-async function step1_generateOpenAiAuthUrl(payload = {}) {
+async function step1_generateOpenAiAuthUrl(payload = {}, options = {}) {
+  const { report = true } = options;
+  const logStep = Number.isInteger(payload?.logStep) ? payload.logStep : 1;
   const redirectUri = normalizeRedirectUri();
   const groupName = (payload.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME).trim() || SUB2API_DEFAULT_GROUP_NAME;
 
@@ -295,8 +308,8 @@ async function step1_generateOpenAiAuthUrl(payload = {}) {
   const group = await getGroupByName(origin, token, groupName);
   const draftName = buildDraftAccountName(group.name || groupName);
 
-  log(`步骤 1:已登录 SUB2API,使用分组 ${group.name}(#${group.id})。`);
-  log(`步骤 1:正在向 SUB2API 生成 OpenAI Auth 链接,回调地址为 ${redirectUri}。`);
+  log(`步骤 ${logStep}:已登录 SUB2API,使用分组 ${group.name}(#${group.id})。`);
+  log(`步骤 ${logStep}:正在向 SUB2API 生成 OpenAI Auth 链接,回调地址为 ${redirectUri}。`);
 
   const authData = await requestJson(origin, '/api/v1/admin/openai/generate-auth-url', {
     method: 'POST',
@@ -314,15 +327,19 @@ async function step1_generateOpenAiAuthUrl(payload = {}) {
     throw new Error('SUB2API 未返回完整的 auth_url / session_id。');
   }
 
-  log(`步骤 1:已获取 SUB2API OAuth 链接:${oauthUrl.slice(0, 96)}...`, 'ok');
-  reportComplete(1, {
+  log(`步骤 ${logStep}:已获取 SUB2API OAuth 链接:${oauthUrl.slice(0, 96)}...`, 'ok');
+  const result = {
     oauthUrl,
     sub2apiSessionId: sessionId,
     sub2apiOAuthState: oauthState,
     sub2apiGroupId: group.id,
     sub2apiDraftName: draftName,
-  });
+  };
+  if (report) {
+    reportComplete(1, result);
+  }
   openAccountsPageSoon(origin);
+  return result;
 }
 
 async function step9_submitOpenAiCallback(payload = {}) {

+ 35 - 16
content/vps-panel.js

@@ -36,31 +36,41 @@ if (document.documentElement.getAttribute(VPS_PANEL_LISTENER_SENTINEL) !== '1')
 
   // Listen for commands from Background
   chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
-    if (message.type === 'EXECUTE_STEP') {
+    if (message.type === 'EXECUTE_STEP' || message.type === 'REQUEST_OAUTH_URL') {
       resetStopState();
       const startedAt = Date.now();
-      console.log(LOG_PREFIX, `EXECUTE_STEP received for step ${message.step}`, {
+      const actionLabel = message.type === 'REQUEST_OAUTH_URL'
+        ? 'REQUEST_OAUTH_URL'
+        : `EXECUTE_STEP received for step ${message.step}`;
+      console.log(LOG_PREFIX, actionLabel, {
         url: location.href,
         payloadKeys: Object.keys(message.payload || {}),
         snapshot: getVpsPanelSnapshot(),
       });
-      handleStep(message.step, message.payload).then(() => {
-        console.log(LOG_PREFIX, `EXECUTE_STEP resolved for step ${message.step} after ${Date.now() - startedAt}ms`, {
+      const handler = message.type === 'REQUEST_OAUTH_URL'
+        ? requestOAuthUrl(message.payload)
+        : handleStep(message.step, message.payload);
+      handler.then((result) => {
+        console.log(LOG_PREFIX, `${actionLabel} resolved after ${Date.now() - startedAt}ms`, {
           url: location.href,
           snapshot: getVpsPanelSnapshot(),
         });
-        sendResponse({ ok: true });
+        sendResponse({ ok: true, ...(result || {}) });
       }).catch(err => {
-        console.error(LOG_PREFIX, `EXECUTE_STEP rejected for step ${message.step} after ${Date.now() - startedAt}ms: ${err?.message || err}`, {
+        console.error(LOG_PREFIX, `${actionLabel} rejected after ${Date.now() - startedAt}ms: ${err?.message || err}`, {
           url: location.href,
           snapshot: getVpsPanelSnapshot(),
         });
         if (isStopError(err)) {
-          log(`步骤 ${message.step}:已被用户停止。`, 'warn');
+          if (message.step) {
+            log(`步骤 ${message.step}:已被用户停止。`, 'warn');
+          }
           sendResponse({ stopped: true, error: err.message });
           return;
         }
-        reportError(message.step, err.message);
+        if (message.step) {
+          reportError(message.step, err.message);
+        }
         sendResponse({ error: err.message });
       });
       return true;
@@ -489,21 +499,27 @@ async function ensureOAuthManagementPage(vpsPassword, step = 1, timeout = 45000)
   throw new Error('无法进入 CPA 的 OAuth 管理页面,请检查面板是否正常加载。URL: ' + location.href);
 }
 
+async function requestOAuthUrl(payload = {}) {
+  return step1_getOAuthLink(payload, { report: false });
+}
+
 // ============================================================
 // Step 1: Get OAuth Link
 // ============================================================
 
-async function step1_getOAuthLink(payload) {
+async function step1_getOAuthLink(payload, options = {}) {
+  const { report = true } = options;
   const { vpsPassword } = payload || {};
+  const logStep = Number.isInteger(payload?.logStep) ? payload.logStep : 1;
   console.log(LOG_PREFIX, '[Step 1] step1_getOAuthLink start', {
     url: location.href,
     hasVpsPassword: Boolean(vpsPassword),
     snapshot: getVpsPanelSnapshot(),
   });
 
-  log('步骤 1:正在等待 CPA 面板加载并进入 OAuth 页面...');
+  log(`步骤 ${logStep}:正在等待 CPA 面板加载并进入 OAuth 页面...`);
 
-  const { header, authUrlEl: existingAuthUrlEl } = await ensureOAuthManagementPage(vpsPassword, 1);
+  const { header, authUrlEl: existingAuthUrlEl } = await ensureOAuthManagementPage(vpsPassword, logStep);
   let authUrlEl = existingAuthUrlEl;
   console.log(LOG_PREFIX, '[Step 1] ensureOAuthManagementPage resolved', {
     url: location.href,
@@ -523,7 +539,7 @@ async function step1_getOAuthLink(payload) {
         url: location.href,
         buttonText: getInlineTextSnippet(getActionText(loginBtn), 80),
       });
-      log('步骤 1:OAuth 登录按钮当前不可用,正在等待授权链接出现...');
+      log(`步骤 ${logStep}:OAuth 登录按钮当前不可用,正在等待授权链接出现...`);
     } else {
       await humanPause(500, 1400);
       simulateClick(loginBtn);
@@ -531,7 +547,7 @@ async function step1_getOAuthLink(payload) {
         url: location.href,
         buttonText: getInlineTextSnippet(getActionText(loginBtn), 80),
       });
-      log('步骤 1:已点击 OAuth 登录按钮,正在等待授权链接...');
+      log(`步骤 ${logStep}:已点击 OAuth 登录按钮,正在等待授权链接...`);
     }
 
     try {
@@ -543,7 +559,7 @@ async function step1_getOAuthLink(payload) {
       );
     }
   } else {
-    log('步骤 1:CPA 面板上已显示授权链接。');
+    log(`步骤 ${logStep}:CPA 面板上已显示授权链接。`);
   }
 
   const oauthUrl = (authUrlEl.textContent || '').trim();
@@ -551,12 +567,15 @@ async function step1_getOAuthLink(payload) {
     throw new Error(`拿到的 OAuth 链接无效:\"${oauthUrl.slice(0, 50)}\"。应为 http 开头的 URL。`);
   }
 
-  log(`步骤 1:已获取 OAuth 链接:${oauthUrl.slice(0, 80)}...`, 'ok');
+  log(`步骤 ${logStep}:已获取 OAuth 链接:${oauthUrl.slice(0, 80)}...`, 'ok');
   console.log(LOG_PREFIX, '[Step 1] reporting completion with oauthUrl', {
     url: location.href,
     oauthUrlPreview: oauthUrl.slice(0, 120),
   });
-  reportComplete(1, { oauthUrl });
+  if (report) {
+    reportComplete(1, { oauthUrl });
+  }
+  return { oauthUrl };
 }
 
 // ============================================================

+ 3 - 3
sidepanel/sidepanel.html

@@ -524,17 +524,17 @@
     <div class="steps-list">
       <div class="step-row" data-step="1">
         <div class="step-indicator" data-step="1"><span class="step-num">1</span></div>
-        <button class="step-btn" data-step="1">获取 OAuth 链接</button>
+        <button class="step-btn" data-step="1">打开 ChatGPT 官网</button>
         <span class="step-status" data-step="1"></span>
       </div>
       <div class="step-row" data-step="2">
         <div class="step-indicator" data-step="2"><span class="step-num">2</span></div>
-        <button class="step-btn" data-step="2">打开注册页</button>
+        <button class="step-btn" data-step="2">注册并输入邮箱</button>
         <span class="step-status" data-step="2"></span>
       </div>
       <div class="step-row" data-step="3">
         <div class="step-indicator" data-step="3"><span class="step-num">3</span></div>
-        <button class="step-btn" data-step="3">填写邮箱和密码</button>
+        <button class="step-btn" data-step="3">填写密码并继续</button>
         <span class="step-status" data-step="3"></span>
       </div>
       <div class="step-row" data-step="4">