From c745405f90e921b72278b7619dea296b7c9e2d33 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Fri, 17 Apr 2026 04:33:14 +0800 Subject: [PATCH] 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. --- README.md | 6 +- background.js | 207 +++++++-------- background/logging-status.js | 2 +- background/steps/fill-profile.js | 65 +---- background/tab-runtime.js | 24 ++ content/activation-utils.js | 10 +- content/signup-page.js | 151 ----------- content/vps-panel.js | 245 +++++++++++++++--- tests/activation-utils.test.js | 10 + tests/auto-run-step6-restart.test.js | 197 ++++++++++++++ ...kground-step5-submit-short-circuit.test.js | 51 ++-- tests/background-tab-runtime-module.test.js | 42 +++ tests/step5-chatgpt-onboarding.test.js | 172 ------------ tests/step5-chatgpt-redirect-race.test.js | 123 --------- tests/step9-status-diagnostics.test.js | 126 +++++++++ 项目完整链路说明.md | 17 +- 项目文件结构说明.md | 15 +- 17 files changed, 772 insertions(+), 691 deletions(-) create mode 100644 tests/auto-run-step6-restart.test.js delete mode 100644 tests/step5-chatgpt-onboarding.test.js delete mode 100644 tests/step5-chatgpt-redirect-race.test.js create mode 100644 tests/step9-status-diagnostics.test.js diff --git a/README.md b/README.md index 96b4f0b..b8fad02 100644 --- a/README.md +++ b/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 diff --git a/background.js b/background.js index eea794d..0bdf7b0 100644 --- a/background.js +++ b/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, } ); diff --git a/background/logging-status.js b/background/logging-status.js index e97a743..51e98d8 100644 --- a/background/logging-status.js +++ b/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) { diff --git a/background/steps/fill-profile.js b/background/steps/fill-profile.js index c1557f0..b45977b 100644 --- a/background/steps/fill-profile.js +++ b/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 }; diff --git a/background/tab-runtime.js b/background/tab-runtime.js index 27bf04c..4153cc6 100644 --- a/background/tab-runtime.js +++ b/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, }; diff --git a/content/activation-utils.js b/content/activation-utils.js index d0796f6..fbda8d9 100644 --- a/content/activation-utils.js +++ b/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 { diff --git a/content/signup-page.js b/content/signup-page.js index f267c48..bf82642 100644 --- a/content/signup-page.js +++ b/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 }; -} diff --git a/content/vps-panel.js b/content/vps-panel.js index a6b3143..688dde8 100644 --- a/content/vps-panel.js +++ b/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 diff --git a/tests/activation-utils.test.js b/tests/activation-utils.test.js index 37632a0..181c597 100644 --- a/tests/activation-utils.test.js +++ b/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 diff --git a/tests/auto-run-step6-restart.test.js b/tests/auto-run-step6-restart.test.js new file mode 100644 index 0000000..b189897 --- /dev/null +++ b/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))); +}); diff --git a/tests/background-step5-submit-short-circuit.test.js b/tests/background-step5-submit-short-circuit.test.js index 1e83856..f150162 100644 --- a/tests/background-step5-submit-short-circuit.test.js +++ b/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))); }); diff --git a/tests/background-tab-runtime-module.test.js b/tests/background-tab-runtime-module.test.js index a583a34..e57a555 100644 --- a/tests/background-tab-runtime-module.test.js +++ b/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); +}); diff --git a/tests/step5-chatgpt-onboarding.test.js b/tests/step5-chatgpt-onboarding.test.js deleted file mode 100644 index 5bc8d57..0000000 --- a/tests/step5-chatgpt-onboarding.test.js +++ /dev/null @@ -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); -}); diff --git a/tests/step5-chatgpt-redirect-race.test.js b/tests/step5-chatgpt-redirect-race.test.js deleted file mode 100644 index 797b39c..0000000 --- a/tests/step5-chatgpt-redirect-race.test.js +++ /dev/null @@ -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); -}); diff --git a/tests/step9-status-diagnostics.test.js b/tests/step9-status-diagnostics.test.js new file mode 100644 index 0000000..17512c5 --- /dev/null +++ b/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'); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 30ae708..3dd3846 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.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. 如果失败,根据设置决定: - 立即停止 - 当前轮重试 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 19d13bc..c1d639e 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.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` 钩子执行顺序。