diff --git a/README.md b/README.md index b6aca3f..7ff8602 100644 --- a/README.md +++ b/README.md @@ -395,9 +395,9 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 侧边栏共有 9 个步骤按钮,可逐步执行: -1. `Get OAuth Link` -2. `Open Signup` -3. `Fill Email / Password` +1. `Open ChatGPT` +2. `Signup + Email` +3. `Fill Password` 4. `Get Signup Code` 5. `Fill Name / Birthday` 6. `Login via OAuth` @@ -411,11 +411,11 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 当前 Auto 逻辑是: -1. Step 1 获取 CPA OAuth 链接 -2. Step 2 打开 OpenAI 注册页 -3. 根据 `Mail` 选择邮箱来源 -4. 如果 `Mail = Hotmail`,会从账号池自动分配一个可用账号 -5. 如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取邮箱(Duck 或 Cloudflare) +1. Step 1 打开 `https://chatgpt.com/` +2. 根据 `Mail` 选择邮箱来源 +3. 如果 `Mail = Hotmail`,会从账号池自动分配一个可用账号 +4. 如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取邮箱(Duck / Cloudflare / iCloud 等) +5. Step 2 点击注册、填写邮箱并继续到密码页 6. 如果自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue` 7. 继续执行 Step 3 ~ Step 9 @@ -430,32 +430,30 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 ## 详细步骤说明 -### Step 1: Get OAuth Link +### Step 1: Open ChatGPT -通过 `content/vps-panel.js`: +通过动态注入的 `content/signup-page.js`: -- 打开 CPA OAuth 面板 -- 等待 `Codex OAuth` 卡片出现 -- 点击“登录” -- 读取页面里的授权链接 +- 打开 `https://chatgpt.com/` +- 确认官网首页或注册入口弹窗已经可操作 -结果会保存到侧边栏的 `OAuth` 字段。 +这一步不再获取 `OAuth` 链接;`OAuth` 链接会在 Step 6 内部按需刷新。 -### Step 2: Open Signup +### Step 2: Signup + Email 通过 `content/signup-page.js`: -- 打开授权链接 -- 查找 `Sign up / Register / 创建账户` 按钮 +- 在官网首页查找 `免费注册 / Sign up / Register / 创建账户` - 自动点击进入注册流程 - -### Step 3: Fill Email / Password - -- 如果侧边栏邮箱为空,会先按当前“邮箱生成”配置自动获取邮箱;失败时再提示手动粘贴 - 自动填写邮箱 -- 如页面先要求邮箱,再进入密码页,会自动切页继续填写 +- 点击 `继续` +- 等待跳转到 `https://auth.openai.com/create-account/password` + +### Step 3: Fill Password + +- 使用第 2 步已经确定好的邮箱 - 使用自定义密码或自动生成密码 -- 提交注册表单 +- 在密码页填写密码并提交注册表单 实际使用的密码会写入会话状态,并同步到侧边栏显示。 @@ -625,8 +623,8 @@ background.js 后台主控,编排 1~9 步、Tab 复用、状态 manifest.json 扩展清单 data/names.js 随机姓名、生日数据 content/utils.js 通用工具:等待元素、点击、日志、停止控制 -content/vps-panel.js CPA 面板步骤:Step 1 / Step 9 -content/signup-page.js OpenAI 注册/登录页步骤:Step 2 / 3 / 5 / 6 / 8 +content/vps-panel.js CPA 面板步骤:内部 OAuth 刷新 / Step 9 +content/signup-page.js ChatGPT 官网 + OpenAI 注册/登录页步骤:Step 1 / 2 / 3 / 5 / 6 / 8 hotmail-utils.js Hotmail 收信相关通用辅助 content/duck-mail.js Duck 邮箱自动获取 content/qq-mail.js QQ 邮箱验证码轮询 diff --git a/background.js b/background.js index 8a37557..8f90f64 100644 --- a/background.js +++ b/background.js @@ -3160,6 +3160,13 @@ function isSignupPageHost(hostname = '') { return ['auth0.openai.com', 'auth.openai.com', 'accounts.openai.com'].includes(hostname); } +function isSignupPasswordPageUrl(rawUrl) { + const parsed = parseUrlSafely(rawUrl); + if (!parsed) return false; + return isSignupPageHost(parsed.hostname) + && /\/create-account\/password(?:[/?#]|$)/i.test(parsed.pathname || ''); +} + function is163MailHost(hostname = '') { return hostname === 'mail.163.com' || hostname.endsWith('.mail.163.com') @@ -3381,6 +3388,26 @@ async function waitForTabUrlFamily(source, tabId, referenceUrl, options = {}) { return null; } +async function waitForTabUrlMatch(tabId, matcher, options = {}) { + const { timeoutMs = 15000, retryDelayMs = 400 } = options; + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + try { + const tab = await chrome.tabs.get(tabId); + if (matcher(tab.url || '', tab)) { + return tab; + } + } catch { + return null; + } + + await new Promise((resolve) => setTimeout(resolve, retryDelayMs)); + } + + return null; +} + async function ensureContentScriptReadyOnTab(source, tabId, options = {}) { const { inject = null, @@ -5118,8 +5145,8 @@ async function handleStepData(step, payload) { const stepWaiters = new Map(); let resumeWaiter = null; const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000; -const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([4, 6, 7, 8]); -const STEP_COMPLETION_SIGNAL_STEPS = new Set([1, 2, 3, 5, 9]); +const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 7, 8]); +const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 9]); function waitForStepComplete(step, timeoutMs = 120000) { return new Promise((resolve, reject) => { @@ -5755,19 +5782,20 @@ async function runAutoSequenceFromStep(startStep, context = {}) { if (continued) { await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续当前进度,从步骤 ${startStep} 开始(第 ${attemptRuns} 次尝试)===`, 'info'); } else { - await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,获取 OAuth 链接并打开注册页 ===`, 'info'); + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,打开官网并进入密码页 ===`, 'info'); + } + + if (startStep <= 1) { + await executeStepAndWait(1, AUTO_STEP_DELAYS[1]); } if (startStep <= 2) { - for (const step of [1, 2]) { - if (step < startStep) continue; - await executeStepAndWait(step, AUTO_STEP_DELAYS[step]); - } + await ensureAutoEmailReady(targetRun, totalRuns, attemptRuns); + await executeStepAndWait(2, AUTO_STEP_DELAYS[2]); } if (startStep <= 3) { - await ensureAutoEmailReady(targetRun, totalRuns, attemptRuns); - await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,注册、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info'); + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,填写密码、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info'); await broadcastAutoRunStatus('running', { currentRun: targetRun, totalRuns, @@ -6416,63 +6444,71 @@ async function resumeAutoRun() { } // ============================================================ -// Step 1: Get OAuth Link +// Signup / OAuth Helpers // ============================================================ -async function executeStep1(state) { +const SIGNUP_ENTRY_URL = 'https://chatgpt.com/'; +const SIGNUP_PAGE_INJECT_FILES = ['content/utils.js', 'content/signup-page.js']; + +async function requestOAuthUrlFromPanel(state, options = {}) { if (getPanelMode(state) === 'sub2api') { - return executeSub2ApiStep1(state); + return requestSub2ApiOAuthUrl(state, options); } - return executeCpaStep1(state); + return requestCpaOAuthUrl(state, options); } -async function executeCpaStep1(state) { +async function requestCpaOAuthUrl(state, options = {}) { + const { logLabel = 'OAuth 刷新' } = options; if (!state.vpsUrl) { throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。'); } - await addLog('步骤 1:正在打开 CPA 面板...'); + + await addLog(`${logLabel}:正在打开 CPA 面板...`); const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js']; - await closeConflictingTabsForSource('vps-panel', state.vpsUrl); const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true }); const tabId = tab.id; await rememberSourceLastUrl('vps-panel', state.vpsUrl); - await addLog('步骤 1:CPA 面板已打开,正在等待页面进入目标地址...'); + await addLog(`${logLabel}:CPA 面板已打开,正在等待页面进入目标地址...`); const matchedTab = await waitForTabUrlFamily('vps-panel', tabId, state.vpsUrl, { timeoutMs: 15000, retryDelayMs: 400, }); if (!matchedTab) { - await addLog('步骤 1:CPA 页面尚未完全进入目标地址,继续尝试连接内容脚本...', 'warn'); + await addLog(`${logLabel}:CPA 页面尚未完全进入目标地址,继续尝试连接内容脚本...`, 'warn'); } await ensureContentScriptReadyOnTab('vps-panel', tabId, { inject: injectFiles, timeoutMs: 45000, retryDelayMs: 900, - logMessage: '步骤 1:CPA 面板仍在加载,正在重试连接内容脚本...', + logMessage: `${logLabel}:CPA 面板仍在加载,正在重试连接内容脚本...`, }); const result = await sendToContentScriptResilient('vps-panel', { - type: 'EXECUTE_STEP', - step: 1, + type: 'REQUEST_OAUTH_URL', source: 'background', - payload: { vpsPassword: state.vpsPassword }, + payload: { + vpsPassword: state.vpsPassword, + logStep: 6, + }, }, { timeoutMs: 30000, retryDelayMs: 700, - logMessage: '步骤 1:CPA 面板通信未就绪,正在等待页面恢复...', + logMessage: `${logLabel}:CPA 面板通信未就绪,正在等待页面恢复...`, }); if (result?.error) { throw new Error(result.error); } + return result || {}; } -async function executeSub2ApiStep1(state) { +async function requestSub2ApiOAuthUrl(state, options = {}) { + const { logLabel = 'OAuth 刷新' } = options; const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl); const groupName = (state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME).trim() || DEFAULT_SUB2API_GROUP_NAME; @@ -6483,23 +6519,22 @@ async function executeSub2ApiStep1(state) { throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。'); } - await addLog('步骤 1:正在打开 SUB2API 后台...'); + await addLog(`${logLabel}:正在打开 SUB2API 后台...`); const injectFiles = ['content/utils.js', 'content/sub2api-panel.js']; - await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl); const tab = await chrome.tabs.create({ url: sub2apiUrl, active: true }); const tabId = tab.id; await rememberSourceLastUrl('sub2api-panel', sub2apiUrl); - await addLog('步骤 1:SUB2API 页面已打开,正在等待页面进入目标地址...'); + await addLog(`${logLabel}:SUB2API 页面已打开,正在等待页面进入目标地址...`); const matchedTab = await waitForTabUrlFamily('sub2api-panel', tabId, sub2apiUrl, { timeoutMs: 15000, retryDelayMs: 400, }); if (!matchedTab) { - await addLog('步骤 1:SUB2API 页面尚未稳定,继续尝试连接内容脚本...', 'warn'); + await addLog(`${logLabel}:SUB2API 页面尚未稳定,继续尝试连接内容脚本...`, 'warn'); } await ensureContentScriptReadyOnTab('sub2api-panel', tabId, { @@ -6507,18 +6542,18 @@ async function executeSub2ApiStep1(state) { injectSource: 'sub2api-panel', timeoutMs: 45000, retryDelayMs: 900, - logMessage: '步骤 1:SUB2API 页面仍在加载,正在重试连接内容脚本...', + logMessage: `${logLabel}:SUB2API 页面仍在加载,正在重试连接内容脚本...`, }); const result = await sendToContentScript('sub2api-panel', { - type: 'EXECUTE_STEP', - step: 1, + type: 'REQUEST_OAUTH_URL', source: 'background', payload: { sub2apiUrl, sub2apiEmail: state.sub2apiEmail, sub2apiPassword: state.sub2apiPassword, sub2apiGroupName: groupName, + logStep: 6, }, }, { responseTimeoutMs: SUB2API_STEP1_RESPONSE_TIMEOUT_MS, @@ -6527,32 +6562,86 @@ async function executeSub2ApiStep1(state) { if (result?.error) { throw new Error(result.error); } + return result || {}; } -// ============================================================ -// Step 2: Open Signup Page (Background opens tab, signup-page.js clicks Register) -// ============================================================ +async function openSignupEntryTab(step = 1) { + const tabId = await reuseOrCreateTab('signup-page', SIGNUP_ENTRY_URL, { + inject: SIGNUP_PAGE_INJECT_FILES, + injectSource: 'signup-page', + }); -async function executeStep2(state) { - if (!state.oauthUrl) { - throw new Error('缺少 OAuth 链接,请先完成步骤 1。'); - } - await addLog('步骤 2:正在打开认证链接...'); - await reuseOrCreateTab('signup-page', state.oauthUrl); + await ensureContentScriptReadyOnTab('signup-page', tabId, { + inject: SIGNUP_PAGE_INJECT_FILES, + injectSource: 'signup-page', + timeoutMs: 45000, + retryDelayMs: 900, + logMessage: `步骤 ${step}:ChatGPT 官网仍在加载,正在重试连接内容脚本...`, + }); - await sendToContentScript('signup-page', { - type: 'EXECUTE_STEP', - step: 2, + return tabId; +} + +async function ensureSignupEntryPageReady(step = 1) { + const tabId = await openSignupEntryTab(step); + const result = await sendToContentScriptResilient('signup-page', { + type: 'ENSURE_SIGNUP_ENTRY_READY', + step, source: 'background', payload: {}, + }, { + timeoutMs: 20000, + retryDelayMs: 700, + logMessage: `步骤 ${step}:官网注册入口正在切换,等待页面恢复...`, }); + + if (result?.error) { + throw new Error(result.error); + } + + return { tabId, result: result || {} }; } -// ============================================================ -// Step 3: Fill Email & Password (via signup-page.js) -// ============================================================ +async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) { + const { skipUrlWait = false } = options; -async function executeStep3(state) { + if (!skipUrlWait) { + const matchedTab = await waitForTabUrlMatch(tabId, (url) => isSignupPasswordPageUrl(url), { + timeoutMs: 45000, + retryDelayMs: 300, + }); + if (!matchedTab) { + throw new Error('等待进入密码页超时,请检查邮箱提交后页面是否仍停留在官网或邮箱页。'); + } + } + + await ensureContentScriptReadyOnTab('signup-page', tabId, { + inject: SIGNUP_PAGE_INJECT_FILES, + injectSource: 'signup-page', + timeoutMs: 45000, + retryDelayMs: 900, + logMessage: `步骤 ${step}:密码页仍在加载,正在重试连接内容脚本...`, + }); + + const result = await sendToContentScriptResilient('signup-page', { + type: 'ENSURE_SIGNUP_PASSWORD_PAGE_READY', + step, + source: 'background', + payload: {}, + }, { + timeoutMs: 20000, + retryDelayMs: 700, + logMessage: `步骤 ${step}:认证页正在切换,等待密码页重新就绪...`, + }); + + if (result?.error) { + throw new Error(result.error); + } + + return result || {}; +} + +async function resolveSignupEmailForFlow(state) { let resolvedEmail = state.email; if (isHotmailProvider(state)) { const account = await ensureHotmailAccountForFlow({ @@ -6572,19 +6661,102 @@ async function executeStep3(state) { throw new Error('缺少邮箱地址,请先在侧边栏粘贴邮箱。'); } - const password = state.customPassword || generatePassword(); + return resolvedEmail; +} + +// ============================================================ +// Step 1: Open ChatGPT homepage +// ============================================================ + +async function executeStep1() { + await addLog('步骤 1:正在打开 ChatGPT 官网...'); + await ensureSignupEntryPageReady(1); + await completeStepFromBackground(1, {}); +} + +// ============================================================ +// Step 2: Click signup, fill email, continue to password page +// ============================================================ + +async function executeStep2(state) { + const resolvedEmail = await resolveSignupEmailForFlow(state); if (resolvedEmail !== state.email) { await setEmailState(resolvedEmail); } + + let signupTabId = await getTabId('signup-page'); + if (!signupTabId || !(await isTabAlive('signup-page'))) { + await addLog('步骤 2:未发现可用的注册页标签,正在重新打开 ChatGPT 官网...', 'warn'); + signupTabId = (await ensureSignupEntryPageReady(2)).tabId; + } else { + await chrome.tabs.update(signupTabId, { active: true }); + await ensureContentScriptReadyOnTab('signup-page', signupTabId, { + inject: SIGNUP_PAGE_INJECT_FILES, + injectSource: 'signup-page', + timeoutMs: 45000, + retryDelayMs: 900, + logMessage: '步骤 2:注册入口页内容脚本未就绪,正在等待页面恢复...', + }); + } + + const step2Result = await sendToContentScriptResilient('signup-page', { + type: 'EXECUTE_STEP', + step: 2, + source: 'background', + payload: { email: resolvedEmail }, + }, { + timeoutMs: 20000, + retryDelayMs: 700, + logMessage: '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...', + }); + + if (step2Result?.error) { + throw new Error(step2Result.error); + } + + if (!step2Result?.alreadyOnPasswordPage) { + await addLog(`步骤 2:邮箱 ${resolvedEmail} 已提交,正在等待进入密码页...`); + } + + await ensureSignupPasswordPageReadyInTab(signupTabId, 2, { + skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage), + }); + await completeStepFromBackground(2, {}); +} + +// ============================================================ +// Step 3: Fill Password (via signup-page.js) +// ============================================================ + +async function executeStep3(state) { + const resolvedEmail = state.email; + if (!resolvedEmail) { + throw new Error('缺少邮箱地址,请先完成步骤 2。'); + } + + const signupTabId = await getTabId('signup-page'); + if (!signupTabId || !(await isTabAlive('signup-page'))) { + throw new Error('认证页面标签页已关闭,请先重新完成步骤 2。'); + } + + const password = state.customPassword || generatePassword(); await setPasswordState(password); - // Save account record const accounts = state.accounts || []; accounts.push({ email: resolvedEmail, password, createdAt: new Date().toISOString() }); await setState({ accounts }); + await chrome.tabs.update(signupTabId, { active: true }); + await ensureContentScriptReadyOnTab('signup-page', signupTabId, { + inject: SIGNUP_PAGE_INJECT_FILES, + injectSource: 'signup-page', + timeoutMs: 45000, + retryDelayMs: 900, + logMessage: '步骤 3:密码页内容脚本未就绪,正在等待页面恢复...', + }); + await addLog( - `步骤 3:正在填写邮箱 ${resolvedEmail},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)` + `步骤 3:正在填写密码,邮箱为 ${resolvedEmail},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)` ); await sendToContentScript('signup-page', { type: 'EXECUTE_STEP', @@ -7218,20 +7390,15 @@ async function executeStep5(state) { async function refreshOAuthUrlBeforeStep6(state) { await addLog(`步骤 6:正在刷新登录用的 ${getPanelModeLabel(state)} OAuth 链接...`); - console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] preparing fresh OAuth via step 1'); - const waitForFreshOAuth = waitForStepComplete(1, 120000); - console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] executing step 1 for fresh OAuth'); - await executeStep1(state); - console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] step 1 execute returned, waiting for completion signal'); - await waitForFreshOAuth; - console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] step 1 completion signal received'); + console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel'); + const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: '步骤 6' }); + await handleStepData(1, refreshResult); - const latestState = await getState(); - if (!latestState.oauthUrl) { + if (!refreshResult?.oauthUrl) { throw new Error('刷新 OAuth 链接后仍未拿到可用链接。'); } - return latestState.oauthUrl; + return refreshResult.oauthUrl; } function isStep6SuccessResult(result) { @@ -7368,7 +7535,7 @@ async function runStep7Attempt(state) { await chrome.tabs.update(authTabId, { active: true }); } else { if (!state.oauthUrl) { - throw new Error('缺少 OAuth 链接,请先完成步骤 1。'); + throw new Error('缺少登录用 OAuth 链接,请先完成步骤 6。'); } await reuseOrCreateTab('signup-page', state.oauthUrl); } @@ -7481,7 +7648,6 @@ const STEP8_CLICK_EFFECT_TIMEOUT_MS = 15000; const STEP8_CLICK_RETRY_DELAY_MS = 500; const STEP8_READY_WAIT_TIMEOUT_MS = 30000; const STEP8_MAX_ROUNDS = 5; -const STEP8_SIGNUP_PAGE_INJECT_FILES = ['content/utils.js', 'content/signup-page.js']; const STEP8_STRATEGIES = [ { mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' }, { mode: 'debugger', label: 'debugger click' }, @@ -7520,7 +7686,7 @@ function throwIfStep8SettledOrStopped(isSettled = false) { async function ensureStep8SignupPageReady(tabId, options = {}) { await ensureContentScriptReadyOnTab('signup-page', tabId, { - inject: STEP8_SIGNUP_PAGE_INJECT_FILES, + inject: SIGNUP_PAGE_INJECT_FILES, injectSource: 'signup-page', timeoutMs: options.timeoutMs ?? 15000, retryDelayMs: options.retryDelayMs ?? 600, @@ -7720,7 +7886,7 @@ function getStep8EffectLabel(effect) { async function executeStep8(state) { if (!state.oauthUrl) { - throw new Error('缺少 OAuth 链接,请先完成步骤 1。'); + throw new Error('缺少登录用 OAuth 链接,请先完成步骤 6。'); } await addLog('步骤 8:正在监听 localhost 回调地址...'); diff --git a/content/signup-page.js b/content/signup-page.js index 2da5088..943689b 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -1,48 +1,63 @@ -// content/signup-page.js — Content script for OpenAI auth pages (steps 2, 3, 4-receive, 5) +// content/signup-page.js — Content script for ChatGPT signup entry + OpenAI auth pages // Injected on: auth0.openai.com, auth.openai.com, accounts.openai.com +// Dynamically injected on: chatgpt.com console.log('[MultiPage:signup-page] Content script loaded on', location.href); -// Listen for commands from Background -chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if ( - message.type === 'EXECUTE_STEP' - || message.type === 'FILL_CODE' - || message.type === 'STEP8_FIND_AND_CLICK' - || message.type === 'STEP8_GET_STATE' - || message.type === 'STEP8_TRIGGER_CONTINUE' - || message.type === 'GET_LOGIN_AUTH_STATE' - || message.type === 'PREPARE_SIGNUP_VERIFICATION' - || message.type === 'RESEND_VERIFICATION_CODE' - ) { - resetStopState(); - handleCommand(message).then((result) => { - sendResponse({ ok: true, ...(result || {}) }); - }).catch(err => { - if (isStopError(err)) { - log(`步骤 ${message.step || 8}:已被用户停止。`, 'warn'); - sendResponse({ stopped: true, error: err.message }); - return; - } +const SIGNUP_PAGE_LISTENER_SENTINEL = 'data-multipage-signup-page-listener'; - if (message.type === 'STEP8_FIND_AND_CLICK') { - log(`步骤 8:${err.message}`, 'error'); +if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1') { + document.documentElement.setAttribute(SIGNUP_PAGE_LISTENER_SENTINEL, '1'); + + // Listen for commands from Background + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if ( + message.type === 'EXECUTE_STEP' + || message.type === 'FILL_CODE' + || message.type === 'STEP8_FIND_AND_CLICK' + || message.type === 'STEP8_GET_STATE' + || message.type === 'STEP8_TRIGGER_CONTINUE' + || message.type === 'GET_LOGIN_AUTH_STATE' + || message.type === 'PREPARE_SIGNUP_VERIFICATION' + || message.type === 'RESEND_VERIFICATION_CODE' + || message.type === 'ENSURE_SIGNUP_ENTRY_READY' + || message.type === 'ENSURE_SIGNUP_PASSWORD_PAGE_READY' + ) { + resetStopState(); + handleCommand(message).then((result) => { + sendResponse({ ok: true, ...(result || {}) }); + }).catch(err => { + if (isStopError(err)) { + if (message.step) { + log(`步骤 ${message.step || 8}:已被用户停止。`, 'warn'); + } + sendResponse({ stopped: true, error: err.message }); + return; + } + + if (message.type === 'STEP8_FIND_AND_CLICK') { + log(`步骤 8:${err.message}`, 'error'); + sendResponse({ error: err.message }); + return; + } + + if (message.step) { + reportError(message.step, err.message); + } sendResponse({ error: err.message }); - return; - } - - reportError(message.step, err.message); - sendResponse({ error: err.message }); - }); - return true; - } -}); + }); + return true; + } + }); +} else { + console.log('[MultiPage:signup-page] 消息监听已存在,跳过重复注册'); +} async function handleCommand(message) { switch (message.type) { case 'EXECUTE_STEP': switch (message.step) { - case 2: return await step2_clickRegister(); + case 2: return await step2_clickRegister(message.payload); case 3: return await step3_fillEmailPassword(message.payload); case 5: return await step5_fillNameBirthday(message.payload); case 6: return await step6_login(message.payload); @@ -58,6 +73,10 @@ async function handleCommand(message) { return await prepareSignupVerificationFlow(message.payload); case 'RESEND_VERIFICATION_CODE': return await resendVerificationCode(message.step); + case 'ENSURE_SIGNUP_ENTRY_READY': + return await ensureSignupEntryReady(); + case 'ENSURE_SIGNUP_PASSWORD_PAGE_READY': + return await ensureSignupPasswordPageReady(); case 'STEP8_FIND_AND_CLICK': return await step8_findAndClick(); case 'STEP8_GET_STATE': @@ -212,91 +231,252 @@ async function resendVerificationCode(step, timeout = 45000) { } // ============================================================ -// Step 2: Click Register +// Signup Entry Helpers // ============================================================ -async function step2_clickRegister() { - log('步骤 2:正在查找注册按钮...'); +const SIGNUP_ENTRY_TRIGGER_PATTERN = /免费注册|立即注册|注册|sign\s*up|register|create\s*account|create\s+account/i; +const SIGNUP_EMAIL_INPUT_SELECTOR = 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i]'; - let registerBtn = null; - try { - registerBtn = await waitForElementByText( - 'a, button, [role="button"], [role="link"]', - /sign\s*up|register|create\s*account|注册/i, - 10000 - ); - } catch { - // Some pages may have a direct link - try { - registerBtn = await waitForElement('a[href*="signup"], a[href*="register"]', 5000); - } catch { - throw new Error( - '未找到注册按钮。' + - '请在 DevTools 中检查认证页面 DOM。URL: ' + location.href - ); - } +function getSignupEmailInput() { + const input = document.querySelector(SIGNUP_EMAIL_INPUT_SELECTOR); + return input && isVisibleElement(input) ? input : null; +} + +function getSignupEmailContinueButton({ allowDisabled = false } = {}) { + const direct = document.querySelector('button[type="submit"], input[type="submit"]'); + if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) { + return direct; } - await humanPause(450, 1200); - reportComplete(2); - simulateClick(registerBtn); - log('步骤 2:已点击注册按钮'); + const candidates = document.querySelectorAll( + 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]' + ); + return Array.from(candidates).find((el) => { + if (!isVisibleElement(el) || (!allowDisabled && !isActionEnabled(el))) return false; + return /continue|next|submit|继续|下一步/i.test(getActionText(el)); + }) || null; +} + +function findSignupEntryTrigger() { + const candidates = document.querySelectorAll('a, button, [role="button"], [role="link"]'); + return Array.from(candidates).find((el) => { + if (!isVisibleElement(el) || !isActionEnabled(el)) return false; + return SIGNUP_ENTRY_TRIGGER_PATTERN.test(getActionText(el)); + }) || null; +} + +function getSignupPasswordDisplayedEmail() { + const text = (document.body?.innerText || document.body?.textContent || '') + .replace(/\s+/g, ' ') + .trim(); + const matches = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig); + return matches?.[0] ? String(matches[0]).trim().toLowerCase() : ''; +} + +function inspectSignupEntryState() { + const passwordInput = getSignupPasswordInput(); + if (isSignupPasswordPage() && passwordInput) { + return { + state: 'password_page', + passwordInput, + submitButton: getSignupPasswordSubmitButton({ allowDisabled: true }), + displayedEmail: getSignupPasswordDisplayedEmail(), + url: location.href, + }; + } + + const emailInput = getSignupEmailInput(); + if (emailInput) { + return { + state: 'email_entry', + emailInput, + continueButton: getSignupEmailContinueButton({ allowDisabled: true }), + url: location.href, + }; + } + + const signupTrigger = findSignupEntryTrigger(); + if (signupTrigger) { + return { + state: 'entry_home', + signupTrigger, + url: location.href, + }; + } + + return { + state: 'unknown', + url: location.href, + }; +} + +async function waitForSignupEntryState(options = {}) { + const { + timeout = 15000, + autoOpenEntry = false, + } = options; + const start = Date.now(); + let lastTriggerClickAt = 0; + + while (Date.now() - start < timeout) { + throwIfStopped(); + const snapshot = inspectSignupEntryState(); + + if (snapshot.state === 'password_page' || snapshot.state === 'email_entry') { + return snapshot; + } + + if (snapshot.state === 'entry_home') { + if (!autoOpenEntry) { + return snapshot; + } + + if (Date.now() - lastTriggerClickAt >= 1500) { + lastTriggerClickAt = Date.now(); + log('步骤 2:正在点击官网注册入口...'); + await humanPause(350, 900); + simulateClick(snapshot.signupTrigger); + } + } + + await sleep(250); + } + + return inspectSignupEntryState(); +} + +async function ensureSignupEntryReady(timeout = 15000) { + const snapshot = await waitForSignupEntryState({ timeout, autoOpenEntry: false }); + if (snapshot.state === 'entry_home' || snapshot.state === 'email_entry' || snapshot.state === 'password_page') { + return { + ready: true, + state: snapshot.state, + url: snapshot.url || location.href, + }; + } + + throw new Error('当前页面没有可用的注册入口,也不在邮箱/密码页。URL: ' + location.href); +} + +async function ensureSignupPasswordPageReady(timeout = 20000) { + const start = Date.now(); + + while (Date.now() - start < timeout) { + throwIfStopped(); + const passwordInput = getSignupPasswordInput(); + if (isSignupPasswordPage() && passwordInput) { + return { + ready: true, + state: 'password_page', + url: location.href, + }; + } + await sleep(200); + } + + throw new Error('等待进入密码页超时。URL: ' + location.href); +} + +async function fillSignupEmailAndContinue(email, step) { + if (!email) throw new Error(`未提供邮箱地址,步骤 ${step} 无法继续。`); + const normalizedEmail = String(email || '').trim().toLowerCase(); + + const snapshot = await waitForSignupEntryState({ + timeout: 20000, + autoOpenEntry: true, + }); + + if (snapshot.state === 'password_page') { + if (snapshot.displayedEmail && snapshot.displayedEmail !== normalizedEmail) { + throw new Error(`步骤 ${step}:当前密码页邮箱为 ${snapshot.displayedEmail},与目标邮箱 ${email} 不一致,请先回到步骤 1 重新开始。`); + } + log(`步骤 ${step}:当前已在密码页,无需重复提交邮箱。`); + return { + alreadyOnPasswordPage: true, + url: snapshot.url || location.href, + }; + } + + if (snapshot.state !== 'email_entry' || !snapshot.emailInput) { + throw new Error(`步骤 ${step}:未找到可用的邮箱输入入口。URL: ${location.href}`); + } + + log(`步骤 ${step}:正在填写邮箱:${email}`); + await humanPause(500, 1400); + fillInput(snapshot.emailInput, email); + log(`步骤 ${step}:邮箱已填写`); + + const continueButton = snapshot.continueButton || getSignupEmailContinueButton({ allowDisabled: true }); + if (!continueButton || !isActionEnabled(continueButton)) { + throw new Error(`步骤 ${step}:未找到可点击的“继续”按钮。URL: ${location.href}`); + } + + log(`步骤 ${step}:邮箱已准备提交,正在前往密码页...`); + window.setTimeout(() => { + try { + simulateClick(continueButton); + } catch (error) { + console.error('[MultiPage:signup-page] deferred signup email submit failed:', error?.message || error); + } + }, 120); + + return { + submitted: true, + email, + url: location.href, + }; } // ============================================================ -// Step 3: Fill Email & Password +// Step 2: Click Register, fill email, then continue to password page +// ============================================================ + +async function step2_clickRegister(payload = {}) { + const { email } = payload; + return fillSignupEmailAndContinue(email, 2); +} + +// ============================================================ +// Step 3: Fill Password // ============================================================ async function step3_fillEmailPassword(payload) { - const { email } = payload; - if (!email) throw new Error('未提供邮箱地址,请先在侧边栏粘贴邮箱。'); + const { email, password } = payload; + if (!password) throw new Error('未提供密码,步骤 3 需要可用密码。'); + const normalizedEmail = String(email || '').trim().toLowerCase(); - log(`步骤 3:正在填写邮箱:${email}`); - - // Find email input - let emailInput = null; - try { - emailInput = await waitForElement( - 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email"], input[placeholder*="Email"]', - 10000 - ); - } catch { - throw new Error('在注册页未找到邮箱输入框。URL: ' + location.href); + let snapshot = inspectSignupEntryState(); + if (snapshot.state === 'entry_home') { + throw new Error('当前仍停留在 ChatGPT 官网首页,请先完成步骤 2。'); } - await humanPause(500, 1400); - fillInput(emailInput, email); - log('步骤 3:邮箱已填写'); - - // Check if password field is on the same page - let passwordInput = document.querySelector('input[type="password"]'); - - if (!passwordInput) { - // Need to submit email first to get to password page - log('步骤 3:暂未发现密码输入框,先提交邮箱...'); - const submitBtn = document.querySelector('button[type="submit"]') - || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null); - - if (submitBtn) { - await humanPause(400, 1100); - simulateClick(submitBtn); - log('步骤 3:邮箱已提交,正在等待密码输入框...'); - await sleep(2000); - } - - try { - passwordInput = await waitForElement('input[type="password"]', 10000); - } catch { - throw new Error('提交邮箱后仍未找到密码输入框。URL: ' + location.href); + if (snapshot.state === 'email_entry') { + const transition = await fillSignupEmailAndContinue(email, 3); + if (!transition.alreadyOnPasswordPage) { + await sleep(1200); + await ensureSignupPasswordPageReady(); } + snapshot = inspectSignupEntryState(); + } + + if (snapshot.state !== 'password_page' || !snapshot.passwordInput) { + await ensureSignupPasswordPageReady(); + snapshot = inspectSignupEntryState(); + } + + if (snapshot.state !== 'password_page' || !snapshot.passwordInput) { + throw new Error('在密码页未找到密码输入框。URL: ' + location.href); + } + if (normalizedEmail && snapshot.displayedEmail && snapshot.displayedEmail !== normalizedEmail) { + throw new Error(`当前密码页邮箱为 ${snapshot.displayedEmail},与目标邮箱 ${email} 不一致,请先回到步骤 1 重新开始。`); } - if (!payload.password) throw new Error('未提供密码,步骤 3 需要可用密码。'); await humanPause(600, 1500); - fillInput(passwordInput, payload.password); + fillInput(snapshot.passwordInput, password); log('步骤 3:密码已填写'); - const submitBtn = document.querySelector('button[type="submit"]') + const submitBtn = snapshot.submitButton + || getSignupPasswordSubmitButton({ allowDisabled: true }) || await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null); // Report complete BEFORE submit, because submit causes page navigation diff --git a/content/sub2api-panel.js b/content/sub2api-panel.js index a38b082..eecf348 100644 --- a/content/sub2api-panel.js +++ b/content/sub2api-panel.js @@ -13,17 +13,24 @@ if (document.documentElement.getAttribute(SUB2API_PANEL_LISTENER_SENTINEL) !== ' document.documentElement.setAttribute(SUB2API_PANEL_LISTENER_SENTINEL, '1'); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.type === 'EXECUTE_STEP') { + if (message.type === 'EXECUTE_STEP' || message.type === 'REQUEST_OAUTH_URL') { resetStopState(); - handleStep(message.step, message.payload).then(() => { - sendResponse({ ok: true }); + const handler = message.type === 'REQUEST_OAUTH_URL' + ? requestOAuthUrl(message.payload) + : handleStep(message.step, message.payload); + handler.then((result) => { + sendResponse({ ok: true, ...(result || {}) }); }).catch((err) => { if (isStopError(err)) { - log(`步骤 ${message.step}:已被用户停止。`, 'warn'); + if (message.step) { + log(`步骤 ${message.step}:已被用户停止。`, 'warn'); + } sendResponse({ stopped: true, error: err.message }); return; } - reportError(message.step, err.message); + if (message.step) { + reportError(message.step, err.message); + } sendResponse({ error: err.message }); }); return true; @@ -66,6 +73,10 @@ async function handleStep(step, payload = {}) { } } +async function requestOAuthUrl(payload = {}) { + return step1_generateOpenAiAuthUrl(payload, { report: false }); +} + async function requestJson(origin, path, options = {}) { throwIfStopped(); const { @@ -287,7 +298,9 @@ function openAccountsPageSoon(origin) { }, 500); } -async function step1_generateOpenAiAuthUrl(payload = {}) { +async function step1_generateOpenAiAuthUrl(payload = {}, options = {}) { + const { report = true } = options; + const logStep = Number.isInteger(payload?.logStep) ? payload.logStep : 1; const redirectUri = normalizeRedirectUri(); const groupName = (payload.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME).trim() || SUB2API_DEFAULT_GROUP_NAME; @@ -295,8 +308,8 @@ async function step1_generateOpenAiAuthUrl(payload = {}) { const group = await getGroupByName(origin, token, groupName); const draftName = buildDraftAccountName(group.name || groupName); - log(`步骤 1:已登录 SUB2API,使用分组 ${group.name}(#${group.id})。`); - log(`步骤 1:正在向 SUB2API 生成 OpenAI Auth 链接,回调地址为 ${redirectUri}。`); + log(`步骤 ${logStep}:已登录 SUB2API,使用分组 ${group.name}(#${group.id})。`); + log(`步骤 ${logStep}:正在向 SUB2API 生成 OpenAI Auth 链接,回调地址为 ${redirectUri}。`); const authData = await requestJson(origin, '/api/v1/admin/openai/generate-auth-url', { method: 'POST', @@ -314,15 +327,19 @@ async function step1_generateOpenAiAuthUrl(payload = {}) { throw new Error('SUB2API 未返回完整的 auth_url / session_id。'); } - log(`步骤 1:已获取 SUB2API OAuth 链接:${oauthUrl.slice(0, 96)}...`, 'ok'); - reportComplete(1, { + log(`步骤 ${logStep}:已获取 SUB2API OAuth 链接:${oauthUrl.slice(0, 96)}...`, 'ok'); + const result = { oauthUrl, sub2apiSessionId: sessionId, sub2apiOAuthState: oauthState, sub2apiGroupId: group.id, sub2apiDraftName: draftName, - }); + }; + if (report) { + reportComplete(1, result); + } openAccountsPageSoon(origin); + return result; } async function step9_submitOpenAiCallback(payload = {}) { diff --git a/content/vps-panel.js b/content/vps-panel.js index df1ef4b..b936265 100644 --- a/content/vps-panel.js +++ b/content/vps-panel.js @@ -36,31 +36,41 @@ if (document.documentElement.getAttribute(VPS_PANEL_LISTENER_SENTINEL) !== '1') // Listen for commands from Background chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.type === 'EXECUTE_STEP') { + if (message.type === 'EXECUTE_STEP' || message.type === 'REQUEST_OAUTH_URL') { resetStopState(); const startedAt = Date.now(); - console.log(LOG_PREFIX, `EXECUTE_STEP received for step ${message.step}`, { + const actionLabel = message.type === 'REQUEST_OAUTH_URL' + ? 'REQUEST_OAUTH_URL' + : `EXECUTE_STEP received for step ${message.step}`; + console.log(LOG_PREFIX, actionLabel, { url: location.href, payloadKeys: Object.keys(message.payload || {}), snapshot: getVpsPanelSnapshot(), }); - handleStep(message.step, message.payload).then(() => { - console.log(LOG_PREFIX, `EXECUTE_STEP resolved for step ${message.step} after ${Date.now() - startedAt}ms`, { + const handler = message.type === 'REQUEST_OAUTH_URL' + ? requestOAuthUrl(message.payload) + : handleStep(message.step, message.payload); + handler.then((result) => { + console.log(LOG_PREFIX, `${actionLabel} resolved after ${Date.now() - startedAt}ms`, { url: location.href, snapshot: getVpsPanelSnapshot(), }); - sendResponse({ ok: true }); + sendResponse({ ok: true, ...(result || {}) }); }).catch(err => { - console.error(LOG_PREFIX, `EXECUTE_STEP rejected for step ${message.step} after ${Date.now() - startedAt}ms: ${err?.message || err}`, { + console.error(LOG_PREFIX, `${actionLabel} rejected after ${Date.now() - startedAt}ms: ${err?.message || err}`, { url: location.href, snapshot: getVpsPanelSnapshot(), }); if (isStopError(err)) { - log(`步骤 ${message.step}:已被用户停止。`, 'warn'); + if (message.step) { + log(`步骤 ${message.step}:已被用户停止。`, 'warn'); + } sendResponse({ stopped: true, error: err.message }); return; } - reportError(message.step, err.message); + if (message.step) { + reportError(message.step, err.message); + } sendResponse({ error: err.message }); }); return true; @@ -489,21 +499,27 @@ async function ensureOAuthManagementPage(vpsPassword, step = 1, timeout = 45000) throw new Error('无法进入 CPA 的 OAuth 管理页面,请检查面板是否正常加载。URL: ' + location.href); } +async function requestOAuthUrl(payload = {}) { + return step1_getOAuthLink(payload, { report: false }); +} + // ============================================================ // Step 1: Get OAuth Link // ============================================================ -async function step1_getOAuthLink(payload) { +async function step1_getOAuthLink(payload, options = {}) { + const { report = true } = options; const { vpsPassword } = payload || {}; + const logStep = Number.isInteger(payload?.logStep) ? payload.logStep : 1; console.log(LOG_PREFIX, '[Step 1] step1_getOAuthLink start', { url: location.href, hasVpsPassword: Boolean(vpsPassword), snapshot: getVpsPanelSnapshot(), }); - log('步骤 1:正在等待 CPA 面板加载并进入 OAuth 页面...'); + log(`步骤 ${logStep}:正在等待 CPA 面板加载并进入 OAuth 页面...`); - const { header, authUrlEl: existingAuthUrlEl } = await ensureOAuthManagementPage(vpsPassword, 1); + const { header, authUrlEl: existingAuthUrlEl } = await ensureOAuthManagementPage(vpsPassword, logStep); let authUrlEl = existingAuthUrlEl; console.log(LOG_PREFIX, '[Step 1] ensureOAuthManagementPage resolved', { url: location.href, @@ -523,7 +539,7 @@ async function step1_getOAuthLink(payload) { url: location.href, buttonText: getInlineTextSnippet(getActionText(loginBtn), 80), }); - log('步骤 1:OAuth 登录按钮当前不可用,正在等待授权链接出现...'); + log(`步骤 ${logStep}:OAuth 登录按钮当前不可用,正在等待授权链接出现...`); } else { await humanPause(500, 1400); simulateClick(loginBtn); @@ -531,7 +547,7 @@ async function step1_getOAuthLink(payload) { url: location.href, buttonText: getInlineTextSnippet(getActionText(loginBtn), 80), }); - log('步骤 1:已点击 OAuth 登录按钮,正在等待授权链接...'); + log(`步骤 ${logStep}:已点击 OAuth 登录按钮,正在等待授权链接...`); } try { @@ -543,7 +559,7 @@ async function step1_getOAuthLink(payload) { ); } } else { - log('步骤 1:CPA 面板上已显示授权链接。'); + log(`步骤 ${logStep}:CPA 面板上已显示授权链接。`); } const oauthUrl = (authUrlEl.textContent || '').trim(); @@ -551,12 +567,15 @@ async function step1_getOAuthLink(payload) { throw new Error(`拿到的 OAuth 链接无效:\"${oauthUrl.slice(0, 50)}\"。应为 http 开头的 URL。`); } - log(`步骤 1:已获取 OAuth 链接:${oauthUrl.slice(0, 80)}...`, 'ok'); + log(`步骤 ${logStep}:已获取 OAuth 链接:${oauthUrl.slice(0, 80)}...`, 'ok'); console.log(LOG_PREFIX, '[Step 1] reporting completion with oauthUrl', { url: location.href, oauthUrlPreview: oauthUrl.slice(0, 120), }); - reportComplete(1, { oauthUrl }); + if (report) { + reportComplete(1, { oauthUrl }); + } + return { oauthUrl }; } // ============================================================ diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 5caa3f4..a4144e6 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -524,17 +524,17 @@