Kaynağa Gözat

feat: Enhance Step 9 diagnostics and error handling

- Refactor getStatusBadgeEntries to use createStep9Entry for better encapsulation.
- Introduce error visual signals detection in createStep9Entry.
- Add functions to identify OAuth callback timeout failures and step 9 failure texts.
- Update buildStep9StatusDiagnostics to include error visual summaries and handle conflicts between success and failure states.
- Implement new tests for step 9 diagnostics, ensuring correct behavior with success badges and error banners.
- Remove outdated tests related to step 5 onboarding and redirect race conditions.
- Add new tests for auto-run behavior in step 6 restart scenarios.
QLHazyCoder 4 ay önce
ebeveyn
işleme
c745405f90

+ 3 - 3
README.md

@@ -415,7 +415,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
 2. 根据 `Mail` 选择邮箱来源
 3. 如果 `Mail = Hotmail`,会从账号池自动分配一个可用账号
 4. 如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取邮箱(Duck / Cloudflare / iCloud 等)
-5. Step 2 点击注册、填写邮箱并继续到密码页
+5. Step 2 点击注册、填写邮箱,并按真实落地页进入密码页或直接进入邮箱验证码页
 6. 如果自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue`
 7. 继续执行 Step 3 ~ Step 9
 
@@ -447,7 +447,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
 - 自动点击进入注册流程
 - 自动填写邮箱
 - 点击 `继续`
-- 等待跳转到 `https://auth.openai.com/create-account/password`
+- 等待真实落地页;进入 `https://auth.openai.com/create-account/password` 时继续 Step 3,进入 `https://auth.openai.com/email-verification` 时自动跳过 Step 3 直接进入 Step 4
 
 ### Step 3: Fill Password
 
@@ -485,7 +485,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
 - 页面要求 `age`
 
 如果页面是生日模式,会填写年月日;如果页面上存在 `input[name='age']`,则直接填写年龄。
-点击 `完成帐户创建` 后,Step 5 会立刻记为完成,不再等待页面跳转结果;只有在提交瞬间发生页面跳转、且后台还没收到完成信号时,才会兜底检查是否已跳到 ChatGPT
+点击 `完成帐户创建` 后,Step 5 会立刻记为完成,不再等待页面跳转结果;自动运行在进入 Step 6 前只会等待当前页面加载完成,不再接管 ChatGPT 跳转或 onboarding 跳过逻辑
 
 ### Step 6: Login via OAuth
 

+ 100 - 107
background.js

@@ -3403,6 +3403,10 @@ async function waitForTabUrlMatch(tabId, matcher, options = {}) {
   return tabRuntime.waitForTabUrlMatch(tabId, matcher, options);
 }
 
+async function waitForTabComplete(tabId, options = {}) {
+  return tabRuntime.waitForTabComplete(tabId, options);
+}
+
 async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
   return tabRuntime.ensureContentScriptReadyOnTab(source, tabId, options);
 }
@@ -3631,7 +3635,7 @@ function isStep9RecoverableAuthError(error) {
 
 function isLegacyStep9RecoverableAuthError(error) {
   const message = String(typeof error === 'string' ? error : error?.message || '');
-  return /STEP9_OAUTH_TIMEOUT::|认证失败:\s*Timeout waiting for OAuth callback/i.test(message);
+  return /STEP9_OAUTH_TIMEOUT::|认证失败:\s*(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded)/i.test(message);
 }
 
 function isStepDoneStatus(status) {
@@ -4708,6 +4712,17 @@ async function executeStepAndWait(step, delayAfter = 2000) {
     await executeStep(step);
   }
 
+  if (step === 5) {
+    const signupTabId = await getTabId('signup-page');
+    if (signupTabId) {
+      await addLog('自动运行:步骤 5 已收到完成信号,正在等待当前页面完成加载...', 'info');
+      await waitForTabComplete(signupTabId, {
+        timeoutMs: 15000,
+        retryDelayMs: 300,
+      });
+    }
+  }
+
   // Extra delay for page transitions / DOM updates
   if (delayAfter > 0) {
     await sleepWithStop(delayAfter + Math.floor(Math.random() * 1200));
@@ -4791,7 +4806,7 @@ const AUTO_STEP_DELAYS = {
   2: 2000,
   3: 3000,
   4: 2000,
-  5: 3000,
+  5: 0,
   6: 3000,
   7: 2000,
   8: 2000,
@@ -4996,8 +5011,7 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
 
 async function runAutoSequenceFromStep(startStep, context = {}) {
   const { targetRun, totalRuns, attemptRuns, continued = false } = context;
-  const maxStep9RestartAttempts = 5;
-  let step9RestartAttempts = 0;
+  let postStep6RestartCount = 0;
 
   if (continued) {
     await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续当前进度,从步骤 ${startStep} 开始(第 ${attemptRuns} 次尝试)===`, 'info');
@@ -5048,27 +5062,31 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
       }
       step += 1;
     } catch (err) {
-      const latestState = await getState();
-      const currentMail = getMailConfig(latestState);
-      const shouldRetryStep9 = step === 9
-        && (
-          isLegacyStep9RecoverableAuthError(err)
-          || (currentMail.provider === HOTMAIL_PROVIDER && isStep9RecoverableAuthError(err))
-        )
-        && step9RestartAttempts < maxStep9RestartAttempts;
-
-      if (shouldRetryStep9) {
-        step9RestartAttempts += 1;
+      const restartDecision = await getPostStep6AutoRestartDecision(step, err);
+      if (restartDecision.shouldRestart) {
+        postStep6RestartCount += 1;
+        const authState = restartDecision.authState;
+        const authStateLabel = authState?.state ? getLoginAuthStateLabel(authState.state) : '未知页面';
+        const authStateSuffix = authState?.url
+          ? `当前认证页:${authStateLabel}(${authState.url})`
+          : authState?.state
+            ? `当前认证页:${authStateLabel}`
+            : '未获取到认证页状态';
         await addLog(
-          `步骤 9:检测到 CPA 认证失败,正在回到步骤 6 重新开始授权流程(${step9RestartAttempts}/${maxStep9RestartAttempts})...`,
+          `步骤 ${step}:检测到报错且当前未进入 add-phone,正在回到步骤 6 重新开始授权流程(第 ${postStep6RestartCount} 次重开)。${authStateSuffix};原因:${restartDecision.errorMessage || '未知错误'}`,
           'warn'
         );
-        await invalidateDownstreamAfterStepRestart(6, {
-          logLabel: `步骤 9 认证失败后准备回到步骤 6 重试(${step9RestartAttempts}/${maxStep9RestartAttempts})`,
+        await invalidateDownstreamAfterStepRestart(5, {
+          logLabel: `步骤 ${step} 报错后准备回到步骤 6 重试(第 ${postStep6RestartCount} 次重开)`,
         });
         step = 6;
         continue;
       }
+
+      if (restartDecision.blockedByAddPhone) {
+        const addPhoneUrl = restartDecision.authState?.url || 'https://auth.openai.com/add-phone';
+        await addLog(`步骤 ${step}:检测到认证流程进入 add-phone(${addPhoneUrl}),停止自动回到步骤 6 重开。`, 'warn');
+      }
       throw err;
     }
   }
@@ -5297,13 +5315,7 @@ const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({
   addLog,
   generateRandomBirthday,
   generateRandomName,
-  getState,
-  getTabId,
-  handleChatgptOnboardingSkip,
-  isRetryableContentScriptTransportError,
-  LOG_PREFIX,
   sendToContentScript,
-  waitForStep5ChatgptRedirect,
 });
 const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({
   addLog,
@@ -5651,91 +5663,10 @@ async function executeStep4(state) {
 // Step 5: Fill Name & Birthday (via signup-page.js)
 // ============================================================
 
-async function waitForStep5ChatgptRedirect(tabId, timeoutMs = 15000) {
-  if (!Number.isInteger(tabId)) {
-    return null;
-  }
-
-  const matchedTab = await waitForTabUrlMatch(tabId, (url) => /chatgpt\.com/i.test(url || ''), {
-    timeoutMs,
-    retryDelayMs: 300,
-  });
-  if (matchedTab) {
-    return matchedTab;
-  }
-
-  const currentTab = await chrome.tabs.get(tabId).catch(() => null);
-  if (currentTab && /chatgpt\.com/i.test(currentTab.url || '')) {
-    return currentTab;
-  }
-
-  return null;
-}
-
 async function executeStep5(state) {
   return step5Executor.executeStep5(state);
 }
 
-async function handleChatgptOnboardingSkip(knownTabId) {
-  const signupTabId = knownTabId || await getTabId('signup-page');
-  if (!signupTabId) {
-    throw new Error('步骤 5:无法找到注册页标签页,无法处理 ChatGPT 引导页。');
-  }
-
-  // Wait for the tab to navigate to chatgpt.com (may already be there)
-  const start = Date.now();
-  const timeout = 15000;
-  let tabUrl = '';
-  while (Date.now() - start < timeout) {
-    throwIfStopped();
-    const tab = await chrome.tabs.get(signupTabId).catch(() => null);
-    if (tab && /chatgpt\.com/i.test(tab.url || '')) {
-      tabUrl = tab.url;
-      break;
-    }
-    await sleepWithStop(500);
-  }
-
-  if (!tabUrl) {
-    throw new Error('步骤 5:等待页面跳转到 ChatGPT 引导页超时。');
-  }
-
-  // Wait for page to finish loading
-  await sleepWithStop(2000);
-
-  // Inject content script on chatgpt.com
-  await ensureContentScriptReadyOnTab('signup-page', signupTabId, {
-    inject: SIGNUP_PAGE_INJECT_FILES,
-    injectSource: 'signup-page',
-    timeoutMs: 15000,
-    logMessage: '步骤 5:正在等待 ChatGPT 引导页内容脚本就绪...',
-  });
-
-  // Send CHATGPT_SKIP_ONBOARDING message
-  const result = await sendToContentScriptResilient('signup-page', {
-    type: 'CHATGPT_SKIP_ONBOARDING',
-    source: 'background',
-    payload: {},
-  }, {
-    timeoutMs: 60000,
-    retryDelayMs: 1000,
-    logMessage: '步骤 5:ChatGPT 引导页正在处理,等待跳过完成...',
-  });
-
-  if (result?.error) {
-    throw new Error(result.error);
-  }
-
-  if (result?.alreadyCompleted) {
-    await addLog('步骤 5:已进入 ChatGPT 页面,无需继续跳过引导,注册成功。', 'ok');
-    await completeStepFromBackground(5, { chatgptHome: true });
-    return;
-  }
-
-  await addLog('步骤 5:ChatGPT 引导页跳过完成,注册成功。', 'ok');
-  await completeStepFromBackground(5, { chatgptOnboarding: true });
-}
-
 // ============================================================
 // Step 6 Cookie Cleanup
 // ============================================================
@@ -5880,7 +5811,69 @@ function isStep6RecoverableResult(result) {
   return result?.step6Outcome === 'recoverable';
 }
 
-async function getLoginAuthStateFromContent() {
+function isAddPhoneAuthUrl(url) {
+  return /https:\/\/auth\.openai\.com\/add-phone(?:[/?#]|$)/i.test(String(url || '').trim());
+}
+
+function isAddPhoneAuthState(authState = {}) {
+  return authState?.state === 'add_phone_page'
+    || Boolean(authState?.addPhonePage)
+    || isAddPhoneAuthUrl(authState?.url);
+}
+
+async function getPostStep6AutoRestartDecision(step, error) {
+  const normalizedStep = Number(step);
+  const errorMessage = getErrorMessage(error);
+  if (!Number.isFinite(normalizedStep) || normalizedStep < 6 || normalizedStep > 9) {
+    return {
+      shouldRestart: false,
+      blockedByAddPhone: false,
+      errorMessage,
+      authState: null,
+    };
+  }
+
+  if (isAddPhoneAuthUrl(errorMessage)) {
+    return {
+      shouldRestart: false,
+      blockedByAddPhone: true,
+      errorMessage,
+      authState: null,
+    };
+  }
+
+  let authState = null;
+  try {
+    authState = await getLoginAuthStateFromContent({
+      logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 6 重开...`,
+    });
+  } catch (inspectError) {
+    console.warn(LOG_PREFIX, '[AutoRun] failed to inspect login auth state after post-step6 error', {
+      step: normalizedStep,
+      sourceError: errorMessage,
+      inspectError: inspectError?.message || inspectError,
+    });
+  }
+
+  if (isAddPhoneAuthState(authState)) {
+    return {
+      shouldRestart: false,
+      blockedByAddPhone: true,
+      errorMessage,
+      authState,
+    };
+  }
+
+  return {
+    shouldRestart: true,
+    blockedByAddPhone: false,
+    errorMessage,
+    authState,
+  };
+}
+
+async function getLoginAuthStateFromContent(options = {}) {
+  const { logMessage = '步骤 7:认证页正在切换,等待页面重新就绪后继续确认验证码页状态...' } = options;
   const result = await sendToContentScriptResilient(
     'signup-page',
     {
@@ -5891,7 +5884,7 @@ async function getLoginAuthStateFromContent() {
     {
       timeoutMs: 15000,
       retryDelayMs: 600,
-      logMessage: '步骤 7:认证页正在切换,等待页面重新就绪后继续确认验证码页状态...',
+      logMessage,
     }
   );
 

+ 1 - 1
background/logging-status.js

@@ -94,7 +94,7 @@
 
     function isLegacyStep9RecoverableAuthError(error) {
       const message = String(typeof error === 'string' ? error : error?.message || '');
-      return /STEP9_OAUTH_TIMEOUT::|认证失败:\s*Timeout waiting for OAuth callback/i.test(message);
+      return /STEP9_OAUTH_TIMEOUT::|认证失败:\s*(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded)/i.test(message);
     }
 
     function isStepDoneStatus(status) {

+ 6 - 59
background/steps/fill-profile.js

@@ -6,74 +6,21 @@
       addLog,
       generateRandomBirthday,
       generateRandomName,
-      getState,
-      getTabId,
-      handleChatgptOnboardingSkip,
-      isRetryableContentScriptTransportError,
-      LOG_PREFIX,
       sendToContentScript,
-      waitForStep5ChatgptRedirect,
     } = deps;
 
-    async function isStepAlreadyCompleted(step) {
-      if (typeof getState !== 'function') {
-        return false;
-      }
-      const latestState = await getState().catch(() => null);
-      return latestState?.stepStatuses?.[step] === 'completed';
-    }
-
     async function executeStep5() {
       const { firstName, lastName } = generateRandomName();
       const { year, month, day } = generateRandomBirthday();
 
       await addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`);
 
-      let step5Result = null;
-      let step5TransportError = null;
-
-      try {
-        step5Result = await sendToContentScript('signup-page', {
-          type: 'EXECUTE_STEP',
-          step: 5,
-          source: 'background',
-          payload: { firstName, lastName, year, month, day },
-        });
-      } catch (err) {
-        if (isRetryableContentScriptTransportError(err)) {
-          step5TransportError = err;
-          console.log(LOG_PREFIX, '步骤 5:内容脚本通信中断,正在检查是否跳转到 ChatGPT 引导页...', err?.message);
-        } else {
-          throw err;
-        }
-      }
-
-      if (step5TransportError && await isStepAlreadyCompleted(5)) {
-        await addLog('步骤 5:提交后的页面跳转打断了响应,但已收到完成信号,直接结束当前步骤。', 'info');
-        return;
-      }
-
-      if (step5Result?.chatgptOnboarding) {
-        await addLog('步骤 5:检测到 ChatGPT 引导页跳转,正在处理引导页跳过...');
-        await handleChatgptOnboardingSkip();
-        return;
-      }
-
-      if (step5Result?.chatgptHome) {
-        await addLog('步骤 5:检测到已进入 ChatGPT 页面,注册成功。', 'ok');
-        return;
-      }
-
-      if (step5TransportError) {
-        const signupTabId = await getTabId('signup-page');
-        const redirectedTab = await waitForStep5ChatgptRedirect(signupTabId);
-        if (redirectedTab) {
-          await addLog('步骤 5:内容脚本因页面跳转到 ChatGPT 而断开,正在处理引导页跳过...');
-          await handleChatgptOnboardingSkip(redirectedTab.id);
-          return;
-        }
-        throw step5TransportError;
-      }
+      await sendToContentScript('signup-page', {
+        type: 'EXECUTE_STEP',
+        step: 5,
+        source: 'background',
+        payload: { firstName, lastName, year, month, day },
+      });
     }
 
     return { executeStep5 };

+ 24 - 0
background/tab-runtime.js

@@ -202,6 +202,29 @@
       return null;
     }
 
+    async function waitForTabComplete(tabId, options = {}) {
+      const { timeoutMs = 15000, retryDelayMs = 300 } = options;
+      const start = Date.now();
+
+      while (Date.now() - start < timeoutMs) {
+        try {
+          const tab = await chrome.tabs.get(tabId);
+          if (tab?.status === 'complete') {
+            return tab;
+          }
+        } catch {
+          return null;
+        }
+        await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
+      }
+
+      try {
+        return await chrome.tabs.get(tabId);
+      } catch {
+        return null;
+      }
+    }
+
     async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
       const {
         inject = null,
@@ -619,6 +642,7 @@
       sendToContentScriptResilient,
       sendToMailContentScriptResilient,
       summarizeMessageResultForDebug,
+      waitForTabComplete,
       waitForTabUrlFamily,
       waitForTabUrlMatch,
     };

+ 7 - 3
content/activation-utils.js

@@ -38,12 +38,16 @@
   }
 
   function isRecoverableStep9AuthFailure(statusText) {
-    const text = String(statusText || '').trim();
-    if (!/认证失败:\s*/i.test(text)) {
+    const text = String(statusText || '').replace(/\s+/g, ' ').trim();
+    if (!text) {
       return false;
     }
 
-    return /timeout waiting for oauth callback|status code 5\d{2}|bad gateway|gateway timeout|temporarily unavailable/i.test(text);
+    if (/oauth flow is not pending/i.test(text)) {
+      return true;
+    }
+
+    return /(?:认证失败|回调 URL 提交失败):\s*/i.test(text);
   }
 
   return {

+ 0 - 151
content/signup-page.js

@@ -22,7 +22,6 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1'
       || message.type === 'RESEND_VERIFICATION_CODE'
       || message.type === 'ENSURE_SIGNUP_ENTRY_READY'
       || message.type === 'ENSURE_SIGNUP_PASSWORD_PAGE_READY'
-      || message.type === 'CHATGPT_SKIP_ONBOARDING'
     ) {
       resetStopState();
       handleCommand(message).then((result) => {
@@ -84,8 +83,6 @@ async function handleCommand(message) {
       return getStep8State();
     case 'STEP8_TRIGGER_CONTINUE':
       return await step8_triggerContinue(message.payload);
-    case 'CHATGPT_SKIP_ONBOARDING':
-      return await skipChatgptOnboarding();
   }
 }
 
@@ -103,8 +100,6 @@ const VERIFICATION_CODE_INPUT_SELECTOR = [
 const ONE_TIME_CODE_LOGIN_PATTERN = /使用一次性验证码登录|改用(?:一次性)?验证码(?:登录)?|使用验证码登录|一次性验证码|验证码登录|one[-\s]*time\s*(?:passcode|password|code)|use\s+(?:a\s+)?one[-\s]*time\s*(?:passcode|password|code)(?:\s+instead)?|use\s+(?:a\s+)?code(?:\s+instead)?|sign\s+in\s+with\s+(?:email|code)|email\s+(?:me\s+)?(?:a\s+)?code/i;
 
 const RESEND_VERIFICATION_CODE_PATTERN = /重新发送(?:验证码)?|再次发送(?:验证码)?|重发(?:验证码)?|未收到(?:验证码|邮件)|resend(?:\s+code)?|send\s+(?:a\s+)?new\s+code|send\s+(?:it\s+)?again|request\s+(?:a\s+)?new\s+code|didn'?t\s+receive/i;
-const CHATGPT_ONBOARDING_TEXT_PATTERN = /welcome\s+to\s+chatgpt|what\s+should\s+chatgpt\s+call\s+you|how\s+do\s+you\s+want\s+chatgpt\s+to\s+respond|tell\s+chatgpt\s+what\s+traits|personal(?:ize|ise)\s+your\s+experience|介绍一下你自己|ChatGPT 应该如何称呼你|你希望 ChatGPT 如何回应/i;
-const CHATGPT_HOME_TEXT_PATTERN = /new\s+chat|temporary\s+chat|message\s+chatgpt|send\s+a\s+message|chatgpt\s+can\s+make\s+mistakes|新建聊天|临时聊天|给\s*ChatGPT\s*发消息|ChatGPT\s*可能会犯错/i;
 
 function isVisibleElement(el) {
   if (!el) return false;
@@ -780,69 +775,6 @@ function getStep5ErrorText() {
   return messages.find((text) => STEP5_SUBMIT_ERROR_PATTERN.test(text)) || '';
 }
 
-function isChatgptOnboardingPage() {
-  if (!isChatgptUrl()) {
-    return false;
-  }
-
-  if (findChatgptSkipButton()) {
-    return true;
-  }
-
-  return CHATGPT_ONBOARDING_TEXT_PATTERN.test(getPageTextSnapshot());
-}
-
-function isChatgptUrl(rawUrl = location.href) {
-  return /(^https?:\/\/)?([^.]+\.)?chatgpt\.com(?::\d+)?/i.test(rawUrl || '');
-}
-
-function hasVisibleElementMatchingSelector(selector) {
-  return Array.from(document.querySelectorAll(selector)).some(isVisibleElement);
-}
-
-function isChatgptAuthenticatedHomePage() {
-  if (!isChatgptUrl()) {
-    return false;
-  }
-
-  if (isChatgptOnboardingPage()) {
-    return false;
-  }
-
-  const appShellSelectors = [
-    'textarea[placeholder*="Message" i]',
-    'textarea[aria-label*="Message" i]',
-    '[data-testid="composer-root"]',
-    '[data-testid="history-and-skills"]',
-    'button[aria-label*="New chat" i]',
-    'a[aria-label*="New chat" i]',
-    'a[href^="/c/"]',
-  ];
-  if (appShellSelectors.some((selector) => hasVisibleElementMatchingSelector(selector))) {
-    return true;
-  }
-
-  return CHATGPT_HOME_TEXT_PATTERN.test(getPageTextSnapshot());
-}
-
-async function waitForChatgptPostSignupState(timeout = 15000) {
-  const start = Date.now();
-  while (Date.now() - start < timeout) {
-    throwIfStopped();
-
-    if (isChatgptOnboardingPage()) {
-      return 'onboarding';
-    }
-
-    if (isChatgptAuthenticatedHomePage()) {
-      return 'home';
-    }
-
-    await sleep(300);
-  }
-
-  return null;
-}
 
 function isSignupPasswordPage() {
   return /\/create-account\/password(?:[/?#]|$)/i.test(location.pathname || '');
@@ -2208,86 +2140,3 @@ async function step5_fillNameBirthday(payload) {
   return completionPayload;
 }
 
-// ============================================================
-// ChatGPT Onboarding: Skip buttons after signup redirect
-// ============================================================
-
-function findChatgptSkipButton() {
-  // Look for buttons containing "Skip" or "跳过" text with btn-ghost class
-  const buttons = document.querySelectorAll('button');
-  for (const btn of buttons) {
-    if (!isVisibleElement(btn)) continue;
-    const text = (btn.textContent || '').trim();
-    if (/^(skip|跳过)$/i.test(text) && /btn-ghost/i.test(btn.className)) {
-      return btn;
-    }
-  }
-  // Fallback: look for any button matching skip text without class constraint
-  for (const btn of buttons) {
-    if (!isVisibleElement(btn)) continue;
-    const text = (btn.textContent || '').trim();
-    if (/^(skip|跳过)$/i.test(text)) {
-      return btn;
-    }
-  }
-  return null;
-}
-
-async function waitForChatgptSkipButton(timeout = 15000) {
-  const start = Date.now();
-  while (Date.now() - start < timeout) {
-    throwIfStopped();
-    const btn = findChatgptSkipButton();
-    if (btn && isVisibleElement(btn)) {
-      return btn;
-    }
-    await sleep(300);
-  }
-  return null;
-}
-
-async function skipChatgptOnboarding() {
-  log('ChatGPT 页面:正在确认是引导页还是已登录主页...');
-
-  const initialState = await waitForChatgptPostSignupState(15000);
-  if (initialState === 'home') {
-    log('ChatGPT 页面:未出现可跳过引导,已进入登录后的 ChatGPT 页面,按注册成功处理。', 'ok');
-    return { success: true, alreadyCompleted: true };
-  }
-
-  if (initialState !== 'onboarding') {
-    throw new Error('ChatGPT 页面:未检测到可跳过引导,也未确认进入登录后的主页。URL: ' + location.href);
-  }
-
-  log('ChatGPT 引导页:正在查找第一个"Skip/跳过"按钮...');
-
-  // Find first Skip button: sibling of "Next" button, with btn-ghost class
-  const firstSkipBtn = await waitForChatgptSkipButton(15000);
-  if (!firstSkipBtn) {
-    throw new Error('ChatGPT 引导页:未找到第一个"Skip/跳过"按钮。URL: ' + location.href);
-  }
-
-  await humanPause(500, 1200);
-  simulateClick(firstSkipBtn);
-  log('ChatGPT 引导页:已点击第一个"Skip/跳过"按钮,等待第二个按钮出现...');
-
-  // Wait 3 seconds for the second skip button to appear
-  await sleep(3000);
-
-  // Find second Skip button: sibling of "Continue" button, with btn-ghost class
-  const secondSkipBtn = await waitForChatgptSkipButton(5000);
-  if (!secondSkipBtn) {
-    if (isChatgptAuthenticatedHomePage()) {
-      log('ChatGPT 引导页:未找到第二个"Skip/跳过"按钮,但页面已进入登录后的 ChatGPT 页面。', 'ok');
-    } else {
-      log('ChatGPT 引导页:5秒内未找到第二个"Skip/跳过"按钮,判定注册已完成。', 'ok');
-    }
-    return { success: true };
-  }
-
-  await humanPause(500, 1200);
-  simulateClick(secondSkipBtn);
-  log('ChatGPT 引导页:已点击第二个"Skip/跳过"按钮,注册流程完成。', 'ok');
-
-  return { success: true };
-}

+ 212 - 33
content/vps-panel.js

@@ -208,13 +208,7 @@ function getStatusBadgeEntries() {
     for (const candidate of candidates) {
       if (seen.has(candidate)) continue;
       seen.add(candidate);
-      entries.push({
-        element: candidate,
-        selector,
-        visible: isVisibleElement(candidate),
-        text: (candidate.textContent || '').replace(/\s+/g, ' ').trim(),
-        className: String(candidate.className || '').replace(/\s+/g, ' ').trim(),
-      });
+      entries.push(createStep9Entry(candidate, selector));
     }
   }
 
@@ -227,7 +221,8 @@ function summarizeStatusBadgeEntries(entries) {
     .map((entry, index) => {
       const text = entry.text || '(空文本)';
       const className = entry.className ? ` class=${getInlineTextSnippet(entry.className, 80)}` : '';
-      return `#${index + 1}="${getInlineTextSnippet(text, 80)}"${className}`;
+      const errorVisual = entry.errorVisualSummary ? ` error=${getInlineTextSnippet(entry.errorVisualSummary, 80)}` : '';
+      return `#${index + 1}="${getInlineTextSnippet(text, 80)}"${className}${errorVisual}`;
     })
     .join(' | ');
 }
@@ -242,6 +237,20 @@ function normalizeStep9StatusText(statusText) {
   return String(statusText || '').replace(/\s+/g, ' ').trim();
 }
 
+function isOAuthCallbackTimeoutFailure(statusText) {
+  return /认证失败:\s*(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded)/i.test(statusText || '');
+}
+
+function isStep9FailureText(statusText) {
+  const text = normalizeStep9StatusText(statusText);
+  if (!text) return false;
+  if (isOAuthCallbackTimeoutFailure(text)) return true;
+  if (typeof isRecoverableStep9AuthFailure === 'function' && isRecoverableStep9AuthFailure(text)) {
+    return true;
+  }
+  return /回调\s*url\s*提交失败|callback\s*url\s*submit\s*failed|oauth flow is not pending/i.test(text);
+}
+
 function isStep9SuccessStatus(statusText) {
   return STEP9_SUCCESS_STATUSES.has(normalizeStep9StatusText(statusText));
 }
@@ -251,37 +260,188 @@ function isStep9SuccessLikeStatus(statusText) {
   return /authentication successful|аутентификац.*успеш|认证成功/i.test(text);
 }
 
-function getStatusBadgeDiagnostics() {
-  const entries = getStatusBadgeEntries();
+function parseCssColorChannels(colorText) {
+  const text = String(colorText || '').trim().toLowerCase();
+  if (!text || text === 'transparent' || text === 'inherit' || text === 'initial' || text === 'unset') {
+    return null;
+  }
+
+  if (text.startsWith('#')) {
+    const hex = text.slice(1);
+    if (hex.length === 3 || hex.length === 4) {
+      const expanded = hex.split('').map((part) => part + part);
+      const [r, g, b, a = 'ff'] = expanded;
+      return {
+        r: Number.parseInt(r, 16),
+        g: Number.parseInt(g, 16),
+        b: Number.parseInt(b, 16),
+        a: Number.parseInt(a, 16) / 255,
+      };
+    }
+    if (hex.length === 6 || hex.length === 8) {
+      const parts = hex.match(/.{1,2}/g) || [];
+      const [r, g, b, a = 'ff'] = parts;
+      return {
+        r: Number.parseInt(r, 16),
+        g: Number.parseInt(g, 16),
+        b: Number.parseInt(b, 16),
+        a: Number.parseInt(a, 16) / 255,
+      };
+    }
+  }
+
+  if (text.startsWith('rgb')) {
+    const numericParts = text.match(/[\d.]+/g) || [];
+    if (numericParts.length >= 3) {
+      const [r, g, b, a = '1'] = numericParts.map(Number);
+      return { r, g, b, a };
+    }
+  }
+
+  return null;
+}
+
+function isReddishColor(colorText) {
+  const channels = parseCssColorChannels(colorText);
+  if (!channels) return false;
+  const { r, g, b, a = 1 } = channels;
+  if (a <= 0.05) return false;
+  return r >= 120 && r >= g + 35 && r >= b + 35;
+}
+
+function getStep9ErrorVisualSignals(element, className = '') {
+  const signals = [];
+  const normalizedClassName = String(className || '').replace(/\s+/g, ' ').trim();
+  if (/(?:error|danger|fail|destructive|text-red|text-danger|alert)/i.test(normalizedClassName)) {
+    signals.push(`class=${getInlineTextSnippet(normalizedClassName, 80)}`);
+  }
+
+  if (!element) {
+    return signals;
+  }
+
+  const style = window.getComputedStyle(element);
+  if (isReddishColor(style.color)) {
+    signals.push(`color=${style.color}`);
+  }
+  if (isReddishColor(style.borderColor)) {
+    signals.push(`border=${style.borderColor}`);
+  }
+  if (isReddishColor(style.backgroundColor)) {
+    signals.push(`background=${style.backgroundColor}`);
+  }
+
+  return signals;
+}
+
+function createStep9Entry(candidate, selector) {
+  const className = String(candidate?.className || '').replace(/\s+/g, ' ').trim();
+  const errorVisualSignals = getStep9ErrorVisualSignals(candidate, className);
+  return {
+    element: candidate,
+    selector,
+    visible: isVisibleElement(candidate),
+    text: normalizeStep9StatusText(candidate?.textContent || ''),
+    className,
+    errorVisualSignals,
+    errorVisualSummary: errorVisualSignals.join(', '),
+    hasErrorVisualSignal: errorVisualSignals.length > 0,
+  };
+}
+
+function getStep9PageErrorSelectors() {
+  return [
+    '[role="alert"]',
+    '[aria-live="assertive"]',
+    '[aria-live="polite"]',
+    '.alert',
+    '[class*="alert"]',
+    '[class*="error"]',
+    '[class*="danger"]',
+    '.text-danger',
+    '.text-red',
+  ];
+}
+
+function getStep9PageErrorEntries() {
+  const seen = new Set();
+  const entries = [];
+
+  for (const selector of getStep9PageErrorSelectors()) {
+    const candidates = document.querySelectorAll(selector);
+    for (const candidate of candidates) {
+      if (seen.has(candidate)) continue;
+      seen.add(candidate);
+      if (!isVisibleElement(candidate)) continue;
+
+      const entry = createStep9Entry(candidate, selector);
+      if (!isStep9FailureText(entry.text)) continue;
+      entries.push(entry);
+    }
+  }
+
+  return entries;
+}
+
+function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSnippet = '') {
   const visibleEntries = entries.filter((entry) => entry.visible);
-  const selectedEntry = visibleEntries[0] || null;
-  const selectedText = selectedEntry?.text || '';
   const successLikeEntries = visibleEntries.filter((entry) => isStep9SuccessLikeStatus(entry.text));
-  const exactSuccessEntries = visibleEntries.filter((entry) => isStep9SuccessStatus(entry.text));
+  const exactSuccessEntries = visibleEntries.filter((entry) => isStep9SuccessStatus(entry.text) && !entry.hasErrorVisualSignal);
+  const failureEntries = visibleEntries.filter((entry) => isStep9FailureText(entry.text));
+  const errorStyledEntries = visibleEntries.filter((entry) => entry.hasErrorVisualSignal);
+  const allFailureEntries = [...failureEntries, ...pageErrorEntries];
+  const decisiveFailureEntry = allFailureEntries[0] || null;
+  const selectedEntry = decisiveFailureEntry || exactSuccessEntries[0] || visibleEntries[0] || null;
+  const selectedText = selectedEntry?.text || '';
   const visibleSummary = summarizeStatusBadgeEntries(visibleEntries);
-  const pageSnippet = getPageTextSnippet();
+  const successLikeSummary = summarizeStatusBadgeEntries(successLikeEntries);
+  const exactSuccessSummary = summarizeStatusBadgeEntries(exactSuccessEntries);
+  const failureSummary = summarizeStatusBadgeEntries(failureEntries);
+  const pageErrorSummary = summarizeStatusBadgeEntries(pageErrorEntries);
+  const errorStyledSummary = summarizeStatusBadgeEntries(errorStyledEntries);
+  const extraFailureSuffix = pageErrorEntries.length ? `;额外错误提示:${pageErrorSummary}` : '';
+  const errorStyledSuffix = errorStyledEntries.length ? `;红色/错误样式徽标:${errorStyledSummary}` : '';
 
   return {
     selectedText,
+    exactSuccessText: exactSuccessEntries[0]?.text || '',
+    failureText: decisiveFailureEntry?.text || '',
     visibleCount: visibleEntries.length,
     visibleSummary,
     hasSuccessLikeVisibleBadge: successLikeEntries.length > 0,
     hasExactSuccessVisibleBadge: exactSuccessEntries.length > 0,
-    successLikeSummary: summarizeStatusBadgeEntries(successLikeEntries),
-    exactSuccessSummary: summarizeStatusBadgeEntries(exactSuccessEntries),
+    hasFailureVisibleBadge: allFailureEntries.length > 0,
+    hasErrorStyledVisibleBadge: errorStyledEntries.length > 0,
+    successLikeSummary,
+    exactSuccessSummary,
+    failureSummary,
+    pageErrorSummary,
+    errorStyledSummary,
     pageSnippet,
     signature: JSON.stringify({
       selectedText,
       visibleCount: visibleEntries.length,
       visibleSummary,
-      successLikeSummary: summarizeStatusBadgeEntries(successLikeEntries),
+      successLikeSummary,
+      exactSuccessSummary,
+      failureSummary,
+      pageErrorSummary,
+      errorStyledSummary,
     }),
     summary: selectedText
-      ? `当前选中徽标="${getInlineTextSnippet(selectedText, 80)}";可见徽标 ${visibleEntries.length} 个:${visibleSummary}`
-      : `当前未选中任何可见状态徽标;可见徽标 ${visibleEntries.length} 个:${visibleSummary};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`,
+      ? `当前聚焦状态="${getInlineTextSnippet(selectedText, 80)}";可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix}`
+      : `当前未选中任何可见状态徽标;可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`,
   };
 }
 
+function getStatusBadgeDiagnostics() {
+  return buildStep9StatusDiagnostics(
+    getStatusBadgeEntries(),
+    getStep9PageErrorEntries(),
+    getPageTextSnippet()
+  );
+}
+
 function getStatusBadgeElement() {
   const visibleEntry = getStatusBadgeEntries().find((entry) => entry.visible);
   return visibleEntry ? visibleEntry.element : null;
@@ -292,20 +452,16 @@ function getStatusBadgeText() {
   return diagnostics.selectedText;
 }
 
-function isOAuthCallbackTimeoutFailure(statusText) {
-  return /认证失败:\s*Timeout waiting for OAuth callback/i.test(statusText || '');
-}
-
 async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS) {
   const start = Date.now();
   let lastDiagnosticsSignature = '';
   let lastHeartbeatLoggedAt = 0;
   let lastSuccessLikeMismatchSignature = '';
+  let lastSuccessFailureConflictSignature = '';
 
   while (Date.now() - start < timeout) {
     throwIfStopped();
     const diagnostics = getStatusBadgeDiagnostics();
-    const statusText = diagnostics.selectedText;
     const elapsed = Date.now() - start;
 
     if (diagnostics.signature !== lastDiagnosticsSignature) {
@@ -324,36 +480,59 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
         selectedText: diagnostics.selectedText,
         successLikeSummary: diagnostics.successLikeSummary,
         visibleSummary: diagnostics.visibleSummary,
+        errorStyledSummary: diagnostics.errorStyledSummary,
       });
       if (mismatchSignature !== lastSuccessLikeMismatchSignature) {
         lastSuccessLikeMismatchSignature = mismatchSignature;
+        const errorStyledSuffix = diagnostics.hasErrorStyledVisibleBadge
+          ? `;错误样式徽标:${diagnostics.errorStyledSummary}`
+          : '';
         log(
-          `步骤 9:检测到“认证成功”相关徽标,但未命中精确条件。当前选中="${getInlineTextSnippet(diagnostics.selectedText || '(空)', 80)}";成功相关徽标:${diagnostics.successLikeSummary}`,
+          `步骤 9:检测到“认证成功”相关徽标,但未命中精确成功条件。当前聚焦="${getInlineTextSnippet(diagnostics.selectedText || '(空)', 80)}";成功相关徽标:${diagnostics.successLikeSummary}${errorStyledSuffix}`,
           'warn'
         );
         console.warn(LOG_PREFIX, '[Step 9] success-like badge detected without exact match', diagnostics);
       }
     }
 
-    if (isOAuthCallbackTimeoutFailure(statusText)) {
-      throw new Error(`STEP9_OAUTH_TIMEOUT::${statusText}`);
+    if (diagnostics.hasExactSuccessVisibleBadge && diagnostics.hasFailureVisibleBadge) {
+      const conflictSignature = JSON.stringify({
+        exactSuccessSummary: diagnostics.exactSuccessSummary,
+        failureSummary: diagnostics.failureSummary,
+        pageErrorSummary: diagnostics.pageErrorSummary,
+      });
+      if (conflictSignature !== lastSuccessFailureConflictSignature) {
+        lastSuccessFailureConflictSignature = conflictSignature;
+        const failureSummary = diagnostics.pageErrorSummary !== '无可见状态徽标'
+          ? diagnostics.pageErrorSummary
+          : diagnostics.failureSummary;
+        log(
+          `步骤 9:同时检测到成功徽标和失败提示,本轮不判定成功。成功徽标:${diagnostics.exactSuccessSummary};失败提示:${failureSummary}`,
+          'warn'
+        );
+        console.warn(LOG_PREFIX, '[Step 9] success badge is blocked by visible failure', diagnostics);
+      }
     }
-    if (typeof isRecoverableStep9AuthFailure === 'function' && isRecoverableStep9AuthFailure(statusText)) {
-      throw new Error(`STEP9_OAUTH_RETRY::${statusText}`);
+
+    if (diagnostics.failureText) {
+      if (isOAuthCallbackTimeoutFailure(diagnostics.failureText)) {
+        throw new Error(`STEP9_OAUTH_TIMEOUT::${diagnostics.failureText}`);
+      }
+      throw new Error(`STEP9_OAUTH_RETRY::${diagnostics.failureText}`);
     }
-    if (isStep9SuccessStatus(statusText)) {
-      return statusText;
+    if (diagnostics.exactSuccessText) {
+      return diagnostics.exactSuccessText;
     }
     await sleep(200);
   }
 
   const finalDiagnostics = getStatusBadgeDiagnostics();
-  const finalText = finalDiagnostics.selectedText;
+  const finalText = finalDiagnostics.failureText || finalDiagnostics.selectedText;
   const diagnosticsSuffix = ` 当前诊断:${finalDiagnostics.summary}`;
   if (isOAuthCallbackTimeoutFailure(finalText)) {
     throw new Error(`STEP9_OAUTH_TIMEOUT::${finalText}${diagnosticsSuffix}`);
   }
-  if (typeof isRecoverableStep9AuthFailure === 'function' && isRecoverableStep9AuthFailure(finalText)) {
+  if (isStep9FailureText(finalText)) {
     throw new Error(`STEP9_OAUTH_RETRY::${finalText}${diagnosticsSuffix}`);
   }
   throw new Error(finalText

+ 10 - 0
tests/activation-utils.test.js

@@ -71,6 +71,16 @@ test('isRecoverableStep9AuthFailure matches timeout and CPA auth failure statuse
     true
   );
 
+  assert.equal(
+    isRecoverableStep9AuthFailure('认证失败: timeout of 30000ms exceeded'),
+    true
+  );
+
+  assert.equal(
+    isRecoverableStep9AuthFailure('回调 URL 提交失败: oauth flow is not pending'),
+    true
+  );
+
   assert.equal(
     isRecoverableStep9AuthFailure('认证成功!'),
     false

+ 197 - 0
tests/auto-run-step6-restart.test.js

@@ -0,0 +1,197 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+const source = fs.readFileSync('background.js', 'utf8');
+
+function extractFunction(name) {
+  const markers = [`async function ${name}(`, `function ${name}(`];
+  const start = markers
+    .map((marker) => source.indexOf(marker))
+    .find((index) => index >= 0);
+  if (start < 0) {
+    throw new Error(`missing function ${name}`);
+  }
+
+  let parenDepth = 0;
+  let signatureEnded = false;
+  let braceStart = -1;
+  for (let i = start; i < source.length; i += 1) {
+    const ch = source[i];
+    if (ch === '(') {
+      parenDepth += 1;
+    } else if (ch === ')') {
+      parenDepth -= 1;
+      if (parenDepth === 0) {
+        signatureEnded = true;
+      }
+    } else if (ch === '{' && signatureEnded) {
+      braceStart = i;
+      break;
+    }
+  }
+
+  if (braceStart < 0) {
+    throw new Error(`missing body for function ${name}`);
+  }
+
+  let depth = 0;
+  let end = braceStart;
+  for (; end < source.length; end += 1) {
+    const ch = source[end];
+    if (ch === '{') depth += 1;
+    if (ch === '}') {
+      depth -= 1;
+      if (depth === 0) {
+        end += 1;
+        break;
+      }
+    }
+  }
+
+  return source.slice(start, end);
+}
+
+const bundle = [
+  extractFunction('isAddPhoneAuthUrl'),
+  extractFunction('isAddPhoneAuthState'),
+  extractFunction('getPostStep6AutoRestartDecision'),
+  extractFunction('runAutoSequenceFromStep'),
+].join('\n');
+
+function createHarness(options = {}) {
+  const {
+    startStep = 6,
+    failureStep = 9,
+    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 };
+const LOG_PREFIX = '[test]';
+const chrome = {
+  tabs: {
+    update: async () => {},
+  },
+};
+
+let remainingFailures = ${JSON.stringify(failureBudget)};
+const events = {
+  steps: [],
+  logs: [],
+  invalidations: [],
+};
+
+async function addLog(message, level = 'info') {
+  events.logs.push({ message, level });
+}
+
+async function ensureAutoEmailReady() {}
+async function broadcastAutoRunStatus() {}
+async function getState() {
+  return {
+    stepStatuses: { 3: 'completed' },
+    mailProvider: '163',
+  };
+}
+function isStepDoneStatus(status) {
+  return status === 'completed' || status === 'manual_completed' || status === 'skipped';
+}
+async function executeStepAndWait(step) {
+  events.steps.push(step);
+  if (step === ${JSON.stringify(failureStep)} && remainingFailures > 0) {
+    remainingFailures -= 1;
+    throw new Error(${JSON.stringify(failureMessage)});
+  }
+}
+async function getTabId() {
+  return 1;
+}
+function shouldSkipLoginVerificationForCpaCallback() {
+  return false;
+}
+async function invalidateDownstreamAfterStepRestart(step, options = {}) {
+  events.invalidations.push({ step, options });
+}
+function getLoginAuthStateLabel(state) {
+  return state || 'unknown';
+}
+function getErrorMessage(error) {
+  return error?.message || String(error || '');
+}
+async function getLoginAuthStateFromContent() {
+  return ${JSON.stringify(authState)};
+}
+
+${bundle}
+
+return {
+  async run() {
+    await runAutoSequenceFromStep(${JSON.stringify(startStep)}, {
+      targetRun: 1,
+      totalRuns: 1,
+      attemptRuns: 1,
+      continued: false,
+    });
+    return events;
+  },
+  async runAndCaptureError() {
+    try {
+      await runAutoSequenceFromStep(${JSON.stringify(startStep)}, {
+        targetRun: 1,
+        totalRuns: 1,
+        attemptRuns: 1,
+        continued: false,
+      });
+      return null;
+    } catch (error) {
+      return { error, events };
+    }
+  },
+};
+`)();
+}
+
+test('auto-run keeps restarting from step 6 after post-login failures without a hard cap', async () => {
+  const harness = createHarness({
+    failureStep: 9,
+    failureBudget: 6,
+    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,
+    [
+      6, 7, 8, 9,
+      6, 7, 8, 9,
+      6, 7, 8, 9,
+      6, 7, 8, 9,
+      6, 7, 8, 9,
+      6, 7, 8, 9,
+      6, 7, 8, 9,
+    ]
+  );
+  assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新开始授权流程/.test(message)));
+});
+
+test('auto-run stops restarting once add-phone is detected', async () => {
+  const harness = createHarness({
+    failureStep: 6,
+    failureBudget: 1,
+    failureMessage: '当前页面已进入手机号页。URL: https://auth.openai.com/add-phone',
+    authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' },
+  });
+
+  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 }) => /进入 add-phone/.test(message)));
+});

+ 23 - 28
tests/background-step5-submit-short-circuit.test.js

@@ -6,47 +6,42 @@ const source = fs.readFileSync('background/steps/fill-profile.js', 'utf8');
 const globalScope = {};
 const api = new Function('self', `${source}; return self.MultiPageBackgroundStep5;`)(globalScope);
 
-test('step 5 transport error returns immediately after completion signal arrives', async () => {
+test('step 5 forwards generated profile data and relies on completion signal flow', async () => {
   const events = {
-    redirectWaitCalls: 0,
-    onboardingCalls: 0,
     logs: [],
+    messages: [],
   };
 
-  const transportError = new Error('The message port closed before a response was received.');
-
   const executor = api.createStep5Executor({
     addLog: async (message, level) => {
       events.logs.push({ message, level: level || 'info' });
     },
     generateRandomBirthday: () => ({ year: 2003, month: 6, day: 19 }),
     generateRandomName: () => ({ firstName: 'Test', lastName: 'User' }),
-    getState: async () => ({
-      stepStatuses: {
-        5: 'completed',
-      },
-    }),
-    getTabId: async () => 123,
-    handleChatgptOnboardingSkip: async () => {
-      events.onboardingCalls += 1;
-    },
-    isRetryableContentScriptTransportError: (error) => error === transportError,
-    LOG_PREFIX: '[test]',
-    sendToContentScript: async () => {
-      throw transportError;
-    },
-    waitForStep5ChatgptRedirect: async () => {
-      events.redirectWaitCalls += 1;
-      return { id: 123, url: 'https://chatgpt.com/' };
+    sendToContentScript: async (source, message) => {
+      events.messages.push({ source, message });
+      return { accepted: true };
     },
   });
 
   await executor.executeStep5();
 
-  assert.equal(events.redirectWaitCalls, 0, '收到完成信号后不应再等待 ChatGPT 跳转');
-  assert.equal(events.onboardingCalls, 0, '收到完成信号后不应再触发 onboarding 跳过');
-  assert.ok(
-    events.logs.some(({ message }) => /已收到完成信号,直接结束当前步骤/.test(message)),
-    '应记录 Step 5 已按完成信号直接结束'
-  );
+  assert.deepStrictEqual(events.messages, [
+    {
+      source: 'signup-page',
+      message: {
+        type: 'EXECUTE_STEP',
+        step: 5,
+        source: 'background',
+        payload: {
+          firstName: 'Test',
+          lastName: 'User',
+          year: 2003,
+          month: 6,
+          day: 19,
+        },
+      },
+    },
+  ]);
+  assert.ok(events.logs.some(({ message }) => /已生成姓名 Test User/.test(message)));
 });

+ 42 - 0
tests/background-tab-runtime-module.test.js

@@ -15,3 +15,45 @@ test('tab runtime module exposes a factory', () => {
 
   assert.equal(typeof api?.createTabRuntime, 'function');
 });
+
+test('tab runtime waitForTabComplete waits until tab status becomes complete', async () => {
+  const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
+  const globalScope = {};
+  const api = new Function('self', `${source}; return self.MultiPageBackgroundTabRuntime;`)(globalScope);
+
+  let getCalls = 0;
+  const runtime = api.createTabRuntime({
+    LOG_PREFIX: '[test]',
+    addLog: async () => {},
+    buildLocalhostCleanupPrefix: () => '',
+    chrome: {
+      tabs: {
+        get: async () => {
+          getCalls += 1;
+          return {
+            id: 9,
+            url: 'https://example.com',
+            status: getCalls >= 3 ? 'complete' : 'loading',
+          };
+        },
+        query: async () => [],
+      },
+    },
+    getSourceLabel: (source) => source || 'unknown',
+    getState: async () => ({ tabRegistry: {}, sourceLastUrls: {} }),
+    matchesSourceUrlFamily: () => false,
+    normalizeLocalCpaStep9Mode: () => 'submit',
+    parseUrlSafely: () => null,
+    registerTab: async () => {},
+    setState: async () => {},
+    shouldBypassStep9ForLocalCpa: () => false,
+  });
+
+  const result = await runtime.waitForTabComplete(9, {
+    timeoutMs: 2000,
+    retryDelayMs: 1,
+  });
+
+  assert.equal(result?.status, 'complete');
+  assert.equal(getCalls, 3);
+});

+ 0 - 172
tests/step5-chatgpt-onboarding.test.js

@@ -1,172 +0,0 @@
-const assert = require('assert');
-const fs = require('fs');
-
-const source = fs.readFileSync('content/signup-page.js', 'utf8');
-
-function extractFunction(name) {
-  const markers = [`async function ${name}(`, `function ${name}(`];
-  const start = markers
-    .map((marker) => source.indexOf(marker))
-    .find((index) => index >= 0);
-  if (start < 0) {
-    throw new Error(`missing function ${name}`);
-  }
-
-  let parenDepth = 0;
-  let signatureEnded = false;
-  let braceStart = -1;
-  for (let i = start; i < source.length; i += 1) {
-    const ch = source[i];
-    if (ch === '(') {
-      parenDepth += 1;
-    } else if (ch === ')') {
-      parenDepth -= 1;
-      if (parenDepth === 0) {
-        signatureEnded = true;
-      }
-    } else if (ch === '{' && signatureEnded) {
-      braceStart = i;
-      break;
-    }
-  }
-  if (braceStart < 0) {
-    throw new Error(`missing body for function ${name}`);
-  }
-
-  let depth = 0;
-  let end = braceStart;
-  for (; end < source.length; end += 1) {
-    const ch = source[end];
-    if (ch === '{') depth += 1;
-    if (ch === '}') {
-      depth -= 1;
-      if (depth === 0) {
-        end += 1;
-        break;
-      }
-    }
-  }
-
-  return source.slice(start, end);
-}
-
-const bundle = [
-  extractFunction('getPageTextSnapshot'),
-  extractFunction('findChatgptSkipButton'),
-  extractFunction('waitForChatgptSkipButton'),
-  extractFunction('isChatgptOnboardingPage'),
-  extractFunction('isChatgptUrl'),
-  extractFunction('hasVisibleElementMatchingSelector'),
-  extractFunction('isChatgptAuthenticatedHomePage'),
-  extractFunction('waitForChatgptPostSignupState'),
-  extractFunction('skipChatgptOnboarding'),
-].join('\n');
-
-const api = new Function(`
-const CHATGPT_ONBOARDING_TEXT_PATTERN = /welcome\\s+to\\s+chatgpt|what\\s+should\\s+chatgpt\\s+call\\s+you|how\\s+do\\s+you\\s+want\\s+chatgpt\\s+to\\s+respond|tell\\s+chatgpt\\s+what\\s+traits|personal(?:ize|ise)\\s+your\\s+experience|介绍一下你自己|ChatGPT 应该如何称呼你|你希望 ChatGPT 如何回应/i;
-const CHATGPT_HOME_TEXT_PATTERN = /new\\s+chat|temporary\\s+chat|message\\s+chatgpt|send\\s+a\\s+message|chatgpt\\s+can\\s+make\\s+mistakes|新建聊天|临时聊天|给\\s*ChatGPT\\s*发消息|ChatGPT\\s*可能会犯错/i;
-
-let buttonList = [];
-let selectorMap = {};
-let pageText = '';
-let clickedButtons = [];
-let logs = [];
-const location = { href: 'https://chatgpt.com/' };
-const document = {
-  body: { innerText: '', textContent: '' },
-  querySelectorAll(selector) {
-    if (selector === 'button') {
-      return buttonList;
-    }
-    return selectorMap[selector] || [];
-  },
-};
-
-function isVisibleElement(el) {
-  return Boolean(el) && !el.hidden;
-}
-
-function throwIfStopped() {}
-async function sleep() {}
-async function humanPause() {}
-function log(message, level = 'info') {
-  logs.push({ message, level });
-}
-function simulateClick(button) {
-  clickedButtons.push(button.textContent || button.id || 'button');
-  button.hidden = true;
-}
-
-${bundle}
-
-return {
-  setPage({ href = 'https://chatgpt.com/', text = '', buttons = [], selectors = {} }) {
-    location.href = href;
-    pageText = text;
-    buttonList = buttons.map((button, index) => ({
-      id: button.id || \`button-\${index}\`,
-      textContent: button.textContent || '',
-      className: button.className || '',
-      hidden: Boolean(button.hidden),
-    }));
-    selectorMap = {};
-    for (const [selector, count] of Object.entries(selectors)) {
-      selectorMap[selector] = Array.from({ length: count }, (_, index) => ({ id: \`\${selector}-\${index}\`, hidden: false }));
-    }
-    document.body.innerText = pageText;
-    document.body.textContent = pageText;
-    clickedButtons = [];
-    logs = [];
-  },
-  isChatgptOnboardingPage() {
-    return isChatgptOnboardingPage();
-  },
-  isChatgptAuthenticatedHomePage() {
-    return isChatgptAuthenticatedHomePage();
-  },
-  async skipChatgptOnboarding() {
-    return skipChatgptOnboarding();
-  },
-  snapshot() {
-    return { clickedButtons, logs };
-  },
-};
-`)();
-
-(async () => {
-  api.setPage({
-    href: 'https://chatgpt.com/',
-    text: 'New chat ChatGPT can make mistakes',
-    selectors: {
-      'textarea[placeholder*="Message" i]': 1,
-    },
-  });
-
-  assert.strictEqual(api.isChatgptOnboardingPage(), false, '已登录主页不应仅因 chatgpt.com URL 被误判为 onboarding');
-  assert.strictEqual(api.isChatgptAuthenticatedHomePage(), true, '主页特征存在时应识别为已登录 ChatGPT 页面');
-
-  let result = await api.skipChatgptOnboarding();
-  let snapshot = api.snapshot();
-  assert.deepStrictEqual(result, { success: true, alreadyCompleted: true }, '无 Skip 按钮但已进入主页时应按成功处理');
-  assert.deepStrictEqual(snapshot.clickedButtons, [], '已登录主页场景不应再误点按钮');
-
-  api.setPage({
-    href: 'https://chatgpt.com/',
-    text: 'Welcome to ChatGPT',
-    buttons: [
-      { textContent: 'Skip', className: 'btn-ghost' },
-      { textContent: 'Skip', className: 'btn-ghost' },
-    ],
-  });
-
-  assert.strictEqual(api.isChatgptOnboardingPage(), true, '存在 Skip 按钮时应识别为 onboarding');
-  result = await api.skipChatgptOnboarding();
-  snapshot = api.snapshot();
-  assert.deepStrictEqual(result, { success: true }, '真实 onboarding 仍应继续执行跳过逻辑');
-  assert.deepStrictEqual(snapshot.clickedButtons, ['Skip', 'Skip'], '双 Skip onboarding 应依次点击两个按钮');
-
-  console.log('step5 chatgpt onboarding tests passed');
-})().catch((error) => {
-  console.error(error);
-  process.exit(1);
-});

+ 0 - 123
tests/step5-chatgpt-redirect-race.test.js

@@ -1,123 +0,0 @@
-const assert = require('assert');
-const fs = require('fs');
-
-const source = fs.readFileSync('background.js', 'utf8');
-
-function extractFunction(name) {
-  const markers = [`async function ${name}(`, `function ${name}(`];
-  const start = markers
-    .map((marker) => source.indexOf(marker))
-    .find((index) => index >= 0);
-  if (start < 0) {
-    throw new Error(`missing function ${name}`);
-  }
-
-  let parenDepth = 0;
-  let signatureEnded = false;
-  let braceStart = -1;
-  for (let i = start; i < source.length; i += 1) {
-    const ch = source[i];
-    if (ch === '(') {
-      parenDepth += 1;
-    } else if (ch === ')') {
-      parenDepth -= 1;
-      if (parenDepth === 0) {
-        signatureEnded = true;
-      }
-    } else if (ch === '{' && signatureEnded) {
-      braceStart = i;
-      break;
-    }
-  }
-  if (braceStart < 0) {
-    throw new Error(`missing body for function ${name}`);
-  }
-
-  let depth = 0;
-  let end = braceStart;
-  for (; end < source.length; end += 1) {
-    const ch = source[end];
-    if (ch === '{') depth += 1;
-    if (ch === '}') {
-      depth -= 1;
-      if (depth === 0) {
-        end += 1;
-        break;
-      }
-    }
-  }
-
-  return source.slice(start, end);
-}
-
-const bundle = [
-  extractFunction('waitForStep5ChatgptRedirect'),
-].join('\n');
-
-const api = new Function(`
-let waitArgs = null;
-let waitResult = null;
-let currentTab = null;
-
-const chrome = {
-  tabs: {
-    async get() {
-      return currentTab;
-    },
-  },
-};
-
-async function waitForTabUrlMatch(tabId, matcher, options = {}) {
-  waitArgs = { tabId, matcher, options };
-  return waitResult;
-}
-
-${bundle}
-
-return {
-  async run(tabId, timeoutMs) {
-    return waitForStep5ChatgptRedirect(tabId, timeoutMs);
-  },
-  setWaitResult(value) {
-    waitResult = value;
-  },
-  setCurrentTab(value) {
-    currentTab = value;
-  },
-  snapshot() {
-    return { waitArgs };
-  },
-};
-`)();
-
-(async () => {
-  const redirected = { id: 86, url: 'https://chatgpt.com/' };
-  api.setWaitResult(redirected);
-  api.setCurrentTab({ id: 86, url: 'https://auth.openai.com/' });
-
-  let result = await api.run(86, 22000);
-  let snapshot = api.snapshot();
-  assert.deepStrictEqual(result, redirected, '等待命中 chatgpt.com 时应直接返回匹配到的标签页');
-  assert.strictEqual(snapshot.waitArgs.tabId, 86, '应使用 signup-page 当前标签页等待跳转');
-  assert.strictEqual(snapshot.waitArgs.options.timeoutMs, 22000, '应透传等待超时时间');
-  assert.strictEqual(snapshot.waitArgs.options.retryDelayMs, 300, '应使用较短轮询间隔覆盖 URL 更新 race');
-  assert.strictEqual(snapshot.waitArgs.matcher('https://chatgpt.com/?model=gpt-5'), true, 'matcher 应接受 chatgpt.com');
-  assert.strictEqual(snapshot.waitArgs.matcher('https://auth.openai.com/u/signup'), false, 'matcher 不应把旧认证页误判为成功');
-
-  api.setWaitResult(null);
-  api.setCurrentTab({ id: 86, url: 'https://chatgpt.com/?temporary-chat=true' });
-  result = await api.run(86, 15000);
-  assert.deepStrictEqual(result, { id: 86, url: 'https://chatgpt.com/?temporary-chat=true' }, '等待超时后仍应回读当前标签页 URL 兜底');
-
-  api.setCurrentTab({ id: 86, url: 'https://auth.openai.com/u/signup' });
-  result = await api.run(86, 15000);
-  assert.strictEqual(result, null, '仍停留旧域名时不应误判为跳转成功');
-
-  result = await api.run(null, 15000);
-  assert.strictEqual(result, null, '无有效标签页时应直接返回 null');
-
-  console.log('step5 chatgpt redirect race tests passed');
-})().catch((error) => {
-  console.error(error);
-  process.exit(1);
-});

+ 126 - 0
tests/step9-status-diagnostics.test.js

@@ -0,0 +1,126 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+const source = fs.readFileSync('content/vps-panel.js', 'utf8');
+
+function extractFunction(name) {
+  const markers = [`async function ${name}(`, `function ${name}(`];
+  const start = markers
+    .map((marker) => source.indexOf(marker))
+    .find((index) => index >= 0);
+  if (start < 0) {
+    throw new Error(`missing function ${name}`);
+  }
+
+  let parenDepth = 0;
+  let signatureEnded = false;
+  let braceStart = -1;
+  for (let i = start; i < source.length; i += 1) {
+    const ch = source[i];
+    if (ch === '(') {
+      parenDepth += 1;
+    } else if (ch === ')') {
+      parenDepth -= 1;
+      if (parenDepth === 0) {
+        signatureEnded = true;
+      }
+    } else if (ch === '{' && signatureEnded) {
+      braceStart = i;
+      break;
+    }
+  }
+
+  if (braceStart < 0) {
+    throw new Error(`missing body for function ${name}`);
+  }
+
+  let depth = 0;
+  let end = braceStart;
+  for (; end < source.length; end += 1) {
+    const ch = source[end];
+    if (ch === '{') depth += 1;
+    if (ch === '}') {
+      depth -= 1;
+      if (depth === 0) {
+        end += 1;
+        break;
+      }
+    }
+  }
+
+  return source.slice(start, end);
+}
+
+const bundle = [
+  "const STEP9_SUCCESS_STATUSES = new Set(['Authentication successful!', 'Аутентификация успешна!', '认证成功!']);",
+  extractFunction('getInlineTextSnippet'),
+  extractFunction('summarizeStatusBadgeEntries'),
+  extractFunction('normalizeStep9StatusText'),
+  extractFunction('isOAuthCallbackTimeoutFailure'),
+  extractFunction('isStep9FailureText'),
+  extractFunction('isStep9SuccessStatus'),
+  extractFunction('isStep9SuccessLikeStatus'),
+  extractFunction('buildStep9StatusDiagnostics'),
+].join('\n');
+
+function createApi() {
+  return new Function(`
+function isRecoverableStep9AuthFailure(text) {
+  return /(?:认证失败|回调 URL 提交失败):\\s*/i.test(String(text || '').trim())
+    || /oauth flow is not pending/i.test(String(text || '').trim());
+}
+
+${bundle}
+
+return {
+  buildStep9StatusDiagnostics,
+};
+`)();
+}
+
+test('step 9 does not treat red success badges as exact success', () => {
+  const api = createApi();
+  const diagnostics = api.buildStep9StatusDiagnostics([
+    {
+      visible: true,
+      text: '认证成功!',
+      className: 'status-badge text-danger',
+      hasErrorVisualSignal: true,
+      errorVisualSummary: 'color=rgb(220, 38, 38)',
+    },
+  ], [], 'page');
+
+  assert.equal(diagnostics.hasSuccessLikeVisibleBadge, true);
+  assert.equal(diagnostics.hasExactSuccessVisibleBadge, false);
+  assert.equal(diagnostics.hasErrorStyledVisibleBadge, true);
+});
+
+test('step 9 keeps failure state dominant when success badge and error banner coexist', () => {
+  const api = createApi();
+  const diagnostics = api.buildStep9StatusDiagnostics(
+    [
+      {
+        visible: true,
+        text: '认证成功!',
+        className: 'status-badge',
+        hasErrorVisualSignal: false,
+        errorVisualSummary: '',
+      },
+    ],
+    [
+      {
+        visible: true,
+        text: '回调 URL 提交失败: oauth flow is not pending',
+        className: 'alert alert-danger',
+        hasErrorVisualSignal: true,
+        errorVisualSummary: 'color=rgb(220, 38, 38)',
+      },
+    ],
+    'page'
+  );
+
+  assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
+  assert.equal(diagnostics.hasFailureVisibleBadge, true);
+  assert.equal(diagnostics.failureText, '回调 URL 提交失败: oauth flow is not pending');
+});

+ 12 - 5
项目完整链路说明.md

@@ -206,7 +206,9 @@
 1. 解析本轮应使用的邮箱
 2. 打开或复用注册页
 3. 点击注册入口并提交邮箱
-4. 等待跳转到密码页
+4. 等待邮箱提交后的真实落地页
+5. 如果进入密码页,则继续执行 Step 3
+6. 如果直接进入邮箱验证码页,则自动跳过 Step 3 并进入 Step 4
 
 ### Step 3
 
@@ -254,7 +256,7 @@
 1. 生成随机姓名和生日
 2. 内容脚本填写资料并点击“完成帐户创建”
 3. 点击后立即上报 Step 5 完成,不再等待页面结果
-4. 如果提交瞬间页面跳转导致响应通道中断,后台仅在未收到完成信号时兜底判断是否跳到 ChatGPT
+4. 自动运行在进入 Step 6 前仅额外等待当前页面加载完成,不再在 Step 5 阶段接管 ChatGPT 跳转或 onboarding 跳过
 
 ### Step 6
 
@@ -270,6 +272,7 @@
 4. 登录
 5. 确保真正进入验证码页
 6. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次
+7. 自动运行一旦进入步骤 6 之后的链路,若后续步骤报错且认证页未进入 `https://auth.openai.com/add-phone`,则统一回到步骤 6 重新开始授权流程
 
 ### Step 8
 
@@ -296,9 +299,11 @@
 2. 判断是 CPA 还是 SUB2API
 3. 打开相应后台
 4. 提交回调地址
-5. 完成平台侧验证
-6. 追加账号运行历史成功记录
-7. 做成功后的清理与标记
+5. 仅当出现精确成功徽标,且该徽标不是红色/错误态、页面上也没有同时可见的失败提示时,才判定成功
+6. 识别 `认证失败:*`、`认证失败: timeout of 30000ms exceeded`、`回调 URL 提交失败: oauth flow is not pending` 等失败提示并立即报错
+7. 完成平台侧验证
+8. 追加账号运行历史成功记录
+9. 做成功后的清理与标记
 
 ## 7. 邮箱与 provider 链路
 
@@ -359,6 +364,8 @@
 2. 计算是否从中断点继续
 3. 每轮执行前重置必要运行态
 4. 执行 `runAutoSequenceFromStep`
+   - 步骤 6 内部仍保留登录态恢复的有限重试
+   - 一旦进入步骤 6~9,遇到报错且认证流程未进入 `add-phone`,则自动回到步骤 6 无限重开,直到成功、手动停止或命中 `add-phone`
 5. 如果失败,根据设置决定:
    - 立即停止
    - 当前轮重试

+ 9 - 6
项目文件结构说明.md

@@ -55,16 +55,16 @@
 - `background/steps/fetch-login-code.js`:步骤 7 实现,负责登录验证码阶段的邮箱轮询与回退控制。
 - `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口。
 - `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交。
-- `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写与 ChatGPT onboarding 跳过流程
+- `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本
 - `background/steps/oauth-login.js`:步骤 6 实现,负责刷新 OAuth 链接、登录和确保进入验证码页。
 - `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。
 - `background/steps/platform-verify.js`:步骤 9 实现,负责 CPA / SUB2API 回调验证。
 - `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。
-- `background/steps/submit-signup-email.js`:步骤 2 实现,负责注册入口点击、邮箱提交与密码页等待
+- `background/steps/submit-signup-email.js`:步骤 2 实现,负责注册入口点击、邮箱提交与提交后落地页分支判断
 
 ## `content/`
 
-- `content/activation-utils.js`:内容脚本通用激活策略工具,负责按钮点击方式判断等轻量辅助逻辑
+- `content/activation-utils.js`:内容脚本通用激活策略工具,负责按钮点击方式判断与 Step 9 可恢复失败文案判断
 - `content/duck-mail.js`:DuckDuckGo Email Protection 页面脚本,负责生成或读取 `@duck.com` 地址。
 - `content/gmail-mail.js`:Gmail 邮箱轮询脚本,负责在 Gmail 页面中匹配验证码邮件。
 - `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。
@@ -75,7 +75,7 @@
 - `content/signup-page.js`:注册/登录/授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行。
 - `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调。
 - `content/utils.js`:内容脚本公共工具层,负责日志、READY/COMPLETE/ERROR 上报、元素等待、输入与点击。
-- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址和提交回调 URL
+- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做 Step 9 判定
 
 ## `data/`
 
@@ -118,6 +118,7 @@
 
 - `tests/activation-utils.test.js`:测试内容脚本激活策略与 Step 9 可恢复错误判断。
 - `tests/auto-run-fresh-attempt-reset.test.js`:测试自动运行在新一轮开始前会重置旧运行时上下文。
+- `tests/auto-run-step6-restart.test.js`:测试自动运行在步骤 6 之后遇错时会回到步骤 6 重开,并在命中 add-phone 时停止重开。
 - `tests/auto-step-random-delay.test.js`:测试自动运行步间延迟与旧配置键兼容解析。
 - `tests/background-auto-run-module.test.js`:测试自动运行控制器模块已接入且导出工厂。
 - `tests/background-account-run-history-module.test.js`:测试账号运行历史模块已接入、导出工厂,并能在不启用本地 helper 时正确持久化记录。
@@ -128,10 +129,12 @@
 - `tests/background-logging-status-module.test.js`:测试日志/状态模块已接入且导出工厂。
 - `tests/background-luckmail.test.js`:测试 LuckMail 相关后台逻辑,如购买、复用、标记已用与重置。
 - `tests/background-message-router-module.test.js`:测试消息路由模块已接入且导出工厂。
+- `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑。
 - `tests/background-navigation-utils-module.test.js`:测试导航工具模块已接入且导出工厂。
 - `tests/background-panel-bridge-module.test.js`:测试面板桥接模块已接入且导出工厂。
 - `tests/background-signup-flow-module.test.js`:测试注册页辅助模块已接入且导出工厂。
 - `tests/background-step-modules.test.js`:测试步骤模块文件都已由后台入口加载。
+- `tests/background-step5-submit-short-circuit.test.js`:测试 Step 5 会把生成好的资料直接转发给内容脚本,并依赖完成信号收尾。
 - `tests/background-step6-retry-limit.test.js`:测试 Step 6 的有限重试上限,以及 Step 7 回放时可跳过登录前 Cookie 清理。
 - `tests/background-step7-recovery.test.js`:测试 Step 7 在 CPA 模式下会先回放 Step 6 再提交登录验证码,并为 2925 关闭重发间隔。
 - `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。
@@ -153,8 +156,7 @@
 - `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。
 - `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。
 - `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。
-- `tests/step5-chatgpt-onboarding.test.js`:测试 Step 5 的 ChatGPT onboarding 判断与跳过逻辑。
-- `tests/step5-chatgpt-redirect-race.test.js`:测试 Step 5 跨域跳转 race 下的 chatgpt.com 兜底判断。
+- `tests/step5-direct-complete.test.js`:测试 Step 5 在资料页点击提交后立即完成当前步骤。
 - `tests/step6-login-state.test.js`:测试 Step 6 登录状态判断逻辑。
 - `tests/step8-callback-handling.test.js`:测试 Step 8 回调地址捕获逻辑。
 - `tests/step8-debugger-stop.test.js`:测试 Step 8 调试器点击在 Stop 场景下的中止行为。
@@ -162,5 +164,6 @@
 - `tests/step8-stop-cleanup.test.js`:测试 Step 8 在 Stop 后能正确清理监听器与挂起状态。
 - `tests/step9-cpa-mode.test.js`:测试本地 CPA Step 9 策略判断。
 - `tests/step9-localhost-cleanup-scope.test.js`:测试 Step 9 仅清理精确命中的 localhost callback 和路径前缀残留页。
+- `tests/step9-status-diagnostics.test.js`:测试 Step 9 精确成功判定、红色错误态过滤,以及成功徽标与失败提示并存时的优先级。
 - `tests/verification-stop-propagation.test.js`:测试验证码流程在 Stop 场景下的错误传播与不中途降级。
 - `tests/verification-flow-polling.test.js`:测试 2925 长轮询参数,以及验证码提交流程中的 `beforeSubmit` 钩子执行顺序。