diff --git a/README.md b/README.md index 9a2faa0..2099fb0 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ 4. 通过后再执行步骤或 `Auto` 5. 当前项目中,`Mail = Hotmail` 时会直接使用账号池里的邮箱作为注册邮箱,不再走 `Duck / Cloudflare` 自动生成 -### 方案 D:`2925 账号池` +### 方案 E:`2925 账号池` 1. `Mail` 选择 `2925` 2. 在 `2925 账号池` 中添加 `邮箱 / 密码` @@ -645,7 +645,7 @@ Step 8 默认要求当前认证页已经处于登录验证码页。 - 等待按钮可点击 - 获取按钮坐标 - 通过 Chrome `debugger` 的输入事件点击该按钮 -- 点击后会持续检查页面是否真正离开当前状态;如果出现认证页 `重试` 页面,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再重新执行当前轮的“继续”点击 +- 点击后会持续检查页面是否真正离开当前状态;如果点击后出现认证页 `重试` 页面,则直接报错,不会在 Step 9 内部点击 `重试` - 同时监听 `chrome.webNavigation.onBeforeNavigate` - 一旦捕获本地回调地址,就把结果保存到 `Callback` @@ -661,9 +661,9 @@ Step 8 默认要求当前认证页已经处于登录验证码页。 - 步骤 10 会拒绝任何不是真实 `/auth/callback`,或缺少 `code` / `state` 的本地回调地址 - 成功后的清理只会针对 `/auth` 这一类真实回调标签页,不会再泛化清理任意 localhost 路径 -- 侧边栏可切换“本地 CPA”策略,默认是 `全部回调` -- 选择 `全部回调` 时,即使 CPA 部署在本地,也会执行步骤 10 -- 选择 `跳过第10步` 时,仅当本地 CPA 且步骤 9 已拿到回调地址时,才会直接跳过步骤 10 +- 侧边栏可切换“回调方式”,默认是 `服务器部署` +- 选择 `服务器部署` 时,即使 CPA 部署在本地,也会执行步骤 10 +- 选择 `本地部署` 时,仅当本地 CPA 且步骤 9 已拿到回调地址时,才会直接跳过步骤 10 回到 CPA 面板: diff --git a/background.js b/background.js index ca48d74..caad46d 100644 --- a/background.js +++ b/background.js @@ -147,11 +147,12 @@ const HOTMAIL_MAILBOXES = ['INBOX', 'Junk']; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; const CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX = 'CF_SECURITY_BLOCKED::'; const CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE = '您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器'; +const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::'; const HUMAN_STEP_DELAY_MIN = 700; const HUMAN_STEP_DELAY_MAX = 2200; const STEP6_MAX_ATTEMPTS = 3; const STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS = 8; -const OAUTH_FLOW_TIMEOUT_MS = 6 * 60 * 1000; +const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000; const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000; const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000; const DEFAULT_SUB2API_URL = 'https://sub2api.hisence.fun/admin/accounts'; @@ -271,6 +272,7 @@ const PERSISTED_SETTING_DEFAULTS = { accountRunHistoryHelperBaseUrl: DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL, gmailBaseEmail: '', mail2925BaseEmail: '', + currentMail2925AccountId: '', emailPrefix: '', inbucketHost: '', inbucketMailbox: '', @@ -283,6 +285,7 @@ const PERSISTED_SETTING_DEFAULTS = { cloudflareTempEmailAdminAuth: '', cloudflareTempEmailCustomAuth: '', cloudflareTempEmailReceiveMailbox: '', + cloudflareTempEmailUseRandomSubdomain: false, cloudflareTempEmailDomain: '', cloudflareTempEmailDomains: [], hotmailAccounts: [], @@ -839,6 +842,7 @@ function getCloudflareTempEmailConfig(state = {}) { adminAuth: String(state.cloudflareTempEmailAdminAuth || ''), customAuth: String(state.cloudflareTempEmailCustomAuth || ''), receiveMailbox: normalizeCloudflareTempEmailReceiveMailbox(state.cloudflareTempEmailReceiveMailbox), + useRandomSubdomain: Boolean(state.cloudflareTempEmailUseRandomSubdomain), domain: normalizeCloudflareTempEmailDomain(state.cloudflareTempEmailDomain), domains: normalizeCloudflareTempEmailDomains(state.cloudflareTempEmailDomains), }; @@ -911,6 +915,7 @@ function normalizePersistentSettingValue(key, value) { return normalizeEmailGenerator(value); case 'autoDeleteUsedIcloudAlias': case 'accountRunHistoryTextEnabled': + case 'cloudflareTempEmailUseRandomSubdomain': return Boolean(value); case 'icloudHostPreference': return normalizeIcloudHost(value) || 'auto'; @@ -918,6 +923,7 @@ function normalizePersistentSettingValue(key, value) { return normalizeAccountRunHistoryHelperBaseUrl(value); case 'gmailBaseEmail': case 'mail2925BaseEmail': + case 'currentMail2925AccountId': case 'emailPrefix': return String(value || '').trim(); case 'inbucketHost': @@ -4043,6 +4049,17 @@ function getTerminalSecurityBlockedTitle(error) { return 'Cloudflare 风控拦截'; } +function isBrowserSwitchRequiredError(error) { + return getErrorMessage(error).startsWith(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX); +} + +function getBrowserSwitchRequiredMessage(error) { + const message = getErrorMessage(error); + return message.startsWith(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX) + ? message.slice(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX.length).trim() + : message; +} + function broadcastSecurityBlockedAlert(title = '流程已完全停止', message = CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE, alertText = '检测到 Cloudflare 风控,请暂停当前操作。') { chrome.runtime.sendMessage({ type: 'SECURITY_BLOCKED_ALERT', @@ -4066,6 +4083,13 @@ async function handleCloudflareSecurityBlocked(error) { return message; } +async function handleBrowserSwitchRequired(error) { + const message = getBrowserSwitchRequiredMessage(error) + || '检测到第 10 步的特殊冲突状态,请更换浏览器后重新进行注册登录。'; + await requestStop({ logMessage: message }); + return message; +} + function isVerificationMailPollingError(error) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.isVerificationMailPollingError) { return loggingStatus.isVerificationMailPollingError(error); @@ -5373,6 +5397,10 @@ async function executeStep(step, options = {}) { await handleCloudflareSecurityBlocked(err); throw new Error(STOP_ERROR_MESSAGE); } + if (isBrowserSwitchRequiredError(err)) { + await handleBrowserSwitchRequired(err); + throw new Error(STOP_ERROR_MESSAGE); + } if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step) && isRetryableContentScriptTransportError(err))) { await setStepStatus(step, 'failed'); await addLog(`步骤 ${step} 失败:${err.message}`, 'error'); @@ -6885,7 +6913,7 @@ async function startOAuthFlowTimeoutWindow(options = {}) { oauthFlowDeadlineAt: deadlineAt, oauthFlowDeadlineSourceUrl: normalizeOAuthFlowSourceUrl(options.oauthUrl), }); - await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,开始 6 分钟倒计时。`, 'info'); + await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,开始 ${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟倒计时。`, 'info'); return deadlineAt; } @@ -7220,7 +7248,6 @@ async function getStep8PageState(tabId, responseTimeoutMs = 1500) { async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) { const start = Date.now(); let recovered = false; - let retryRecovered = false; while (Date.now() - start < timeoutMs) { throwIfStopped(); @@ -7232,20 +7259,9 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) throw new Error('步骤 9:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。'); } if (pageState?.retryPage) { - await recoverAuthRetryPageOnTab(tabId, { - flow: 'auth', - logLabel: '步骤 9:检测到认证页重试页,正在点击“重试”恢复', - step: 8, - timeoutMs: Math.max(1000, Math.min(12000, timeoutMs)), - }); - retryRecovered = true; - await sleepWithStop(250); - continue; + throw new Error(`步骤 9:当前认证页已进入重试页,当前流程将直接报错。URL: ${pageState.url || 'unknown'}`); } if (pageState?.consentReady) { - if (retryRecovered) { - await addLog('步骤 9:认证页重试页已恢复,准备重新定位“继续”按钮...', 'info'); - } return pageState; } if (pageState === null && !recovered) { @@ -7410,18 +7426,7 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI throw new Error('步骤 9:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。'); } if (pageState?.retryPage) { - await recoverAuthRetryPageOnTab(tabId, { - flow: 'auth', - logLabel: '步骤 9:点击“继续”后进入重试页,正在点击“重试”恢复', - step: 8, - timeoutMs: Math.max(1000, Math.min(12000, timeoutMs)), - }); - return { - progressed: false, - reason: 'retry_page_recovered', - restartCurrentStep: true, - url: pageState.url || baselineUrl || '', - }; + throw new Error(`步骤 9:点击“继续”后页面进入认证页重试页,当前流程将直接报错。URL: ${pageState.url || baselineUrl || 'unknown'}`); } if (pageState === null) { if (!recovered) { @@ -7455,8 +7460,6 @@ function getStep8EffectLabel(effect) { switch (effect?.reason) { case 'url_changed': return `URL 已变化:${effect.url}`; - case 'retry_page_recovered': - return '页面进入重试页并已恢复,需要重新执行当前步骤'; case 'page_reloading': return '页面正在跳转或重载'; case 'left_consent_page': diff --git a/background/generated-email-helpers.js b/background/generated-email-helpers.js index 3d924a7..7db5c98 100644 --- a/background/generated-email-helpers.js +++ b/background/generated-email-helpers.js @@ -146,6 +146,7 @@ const requestedName = String(options.localPart || options.name || '').trim().toLowerCase() || generateCloudflareAliasLocalPart(); const payload = { enablePrefix: true, + enableRandomSubdomain: Boolean(config.useRandomSubdomain), name: requestedName, domain: config.domain, }; diff --git a/background/mail-2925-session.js b/background/mail-2925-session.js index c78a350..410fa97 100644 --- a/background/mail-2925-session.js +++ b/background/mail-2925-session.js @@ -168,6 +168,10 @@ } } + function normalizeMailboxEmail(value = '') { + return String(value || '').trim().toLowerCase(); + } + async function setCurrentMail2925Account(accountId, options = {}) { const { logMessage = '', updateLastUsedAt = false } = options; const state = await getState(); @@ -186,6 +190,7 @@ await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item))); } + await setPersistentSettings({ currentMail2925AccountId: nextAccount.id }); await setState({ currentMail2925AccountId: nextAccount.id }); broadcastDataUpdate({ currentMail2925AccountId: nextAccount.id }); if (logMessage) { @@ -210,6 +215,7 @@ await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item))); if (state.currentMail2925AccountId === account.id && nextAccount.enabled === false) { + await setPersistentSettings({ currentMail2925AccountId: '' }); await setState({ currentMail2925AccountId: null }); broadcastDataUpdate({ currentMail2925AccountId: null }); } @@ -224,6 +230,7 @@ await syncMail2925Accounts(nextAccounts); if (state.currentMail2925AccountId === accountId) { + await setPersistentSettings({ currentMail2925AccountId: '' }); await setState({ currentMail2925AccountId: null }); broadcastDataUpdate({ currentMail2925AccountId: null }); } @@ -239,6 +246,7 @@ await syncMail2925Accounts(nextAccounts); if (state.currentMail2925AccountId && !findMail2925Account(nextAccounts, state.currentMail2925AccountId)) { + await setPersistentSettings({ currentMail2925AccountId: '' }); await setState({ currentMail2925AccountId: null }); broadcastDataUpdate({ currentMail2925AccountId: null }); } @@ -397,10 +405,13 @@ forceRelogin = false, actionLabel = '确保 2925 邮箱登录态', allowLoginWhenOnLoginPage = true, + expectedMailboxEmail = '', } = options; + const normalizedExpectedMailboxEmail = normalizeMailboxEmail(expectedMailboxEmail); + let account = null; - if (forceRelogin) { + if (forceRelogin || (allowLoginWhenOnLoginPage && normalizedExpectedMailboxEmail)) { account = await ensureMail2925AccountForFlow({ allowAllocate: true, preferredAccountId: accountId, @@ -477,16 +488,16 @@ } } - if (!forceRelogin && !isMail2925LoginUrl(openedUrl)) { + if (!forceRelogin && !isMail2925LoginUrl(openedUrl) && !normalizedExpectedMailboxEmail) { await addLog('2925:当前邮箱页未跳转到登录页,将直接复用已登录会话。', 'info'); return buildSuccessPayload(); } - if (!forceRelogin && !allowLoginWhenOnLoginPage) { + if (!forceRelogin && isMail2925LoginUrl(openedUrl) && !allowLoginWhenOnLoginPage) { await failMailboxSession(`2925:${actionLabel}失败,当前页面已跳转到登录页,且当前未启用 2925 账号池,不执行自动登录。`); } - if (!account) { + if (!account && (forceRelogin || allowLoginWhenOnLoginPage)) { account = await ensureMail2925AccountForFlow({ allowAllocate: true, preferredAccountId: accountId, @@ -519,9 +530,10 @@ step: 0, source: 'background', payload: { - email: account.email, - password: account.password, + email: account?.email || '', + password: account?.password || '', forceLogin: forceRelogin, + allowLoginWhenOnLoginPage, }, }, { @@ -546,6 +558,28 @@ if (result?.limitReached) { throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '子邮箱已达上限邮箱'}`); } + const actualMailboxEmail = normalizeMailboxEmail(result?.mailboxEmail || ''); + if (normalizedExpectedMailboxEmail && actualMailboxEmail && actualMailboxEmail !== normalizedExpectedMailboxEmail) { + if (allowLoginWhenOnLoginPage) { + await addLog( + `2925:当前邮箱页显示账号 ${actualMailboxEmail},与目标账号 ${normalizedExpectedMailboxEmail} 不一致,准备登出当前账号并登录目标账号。`, + 'warn' + ); + return ensureMail2925MailboxSession({ + accountId: account?.id || accountId || null, + forceRelogin: true, + allowLoginWhenOnLoginPage: true, + expectedMailboxEmail: normalizedExpectedMailboxEmail, + actionLabel, + }); + } + await failMailboxSession( + `2925:${actionLabel}失败,当前邮箱页显示账号 ${actualMailboxEmail},与目标账号 ${normalizedExpectedMailboxEmail} 不一致,且当前未启用 2925 账号池。` + ); + } + if (normalizedExpectedMailboxEmail && !actualMailboxEmail && result?.currentView === 'mailbox') { + await addLog('2925:未能识别当前邮箱页顶部邮箱地址,已跳过邮箱一致性校验。', 'warn'); + } if (!result?.loggedIn) { await failMailboxSession(`2925:${actionLabel}失败,登录后仍未进入收件箱。`); } @@ -613,6 +647,7 @@ }); if (!nextAccount) { + await setPersistentSettings({ currentMail2925AccountId: '' }); await setState({ currentMail2925AccountId: null }); broadcastDataUpdate({ currentMail2925AccountId: null }); if (typeof requestStop === 'function') { diff --git a/background/steps/confirm-oauth.js b/background/steps/confirm-oauth.js index 1c9076c..d61f8b2 100644 --- a/background/steps/confirm-oauth.js +++ b/background/steps/confirm-oauth.js @@ -200,12 +200,6 @@ break; } - if (effect.restartCurrentStep) { - await addLog(`步骤 9:${getStep8EffectLabel(effect)},准备重新定位“继续”按钮并重试...`, 'warn'); - await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS); - continue; - } - if (round >= STEP8_MAX_ROUNDS) { throw new Error(`步骤 9:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`); } diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index c1d9124..066baee 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -24,6 +24,20 @@ throwIfStopped, } = deps; + function getExpectedMail2925MailboxEmail(state = {}) { + if (Boolean(state?.mail2925UseAccountPool)) { + const currentAccountId = String(state?.currentMail2925AccountId || '').trim(); + const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : []; + const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null; + const accountEmail = String(currentAccount?.email || '').trim().toLowerCase(); + if (accountEmail) { + return accountEmail; + } + } + + return String(state?.mail2925BaseEmail || '').trim().toLowerCase(); + } + async function focusOrOpenMailTab(mail) { const alive = await isTabAlive(mail.source); if (alive) { @@ -111,6 +125,7 @@ accountId: state.currentMail2925AccountId || null, forceRelogin: false, allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool), + expectedMailboxEmail: getExpectedMail2925MailboxEmail(state), actionLabel: '步骤 4:确认 2925 邮箱登录态', }); } else { diff --git a/background/steps/platform-verify.js b/background/steps/platform-verify.js index 72fa9f5..d4e01b4 100644 --- a/background/steps/platform-verify.js +++ b/background/steps/platform-verify.js @@ -159,7 +159,8 @@ source: 'background', payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword }, }, { - timeoutMs: 30000, + timeoutMs: 125000, + responseTimeoutMs: 125000, retryDelayMs: 700, logMessage: '步骤 10:CPA 面板通信未就绪,正在等待页面恢复...', }); diff --git a/content/activation-utils.js b/content/activation-utils.js index fbda8d9..c1ef6bf 100644 --- a/content/activation-utils.js +++ b/content/activation-utils.js @@ -47,7 +47,15 @@ return true; } - return /(?:认证失败|回调 URL 提交失败):\s*/i.test(text); + if (/请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(text)) { + return true; + } + + if (/bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out/i.test(text)) { + return true; + } + + return /(?:认证失败|回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[::]?\s*/i.test(text); } return { diff --git a/content/mail-2925.js b/content/mail-2925.js index 0bbb977..09e9581 100644 --- a/content/mail-2925.js +++ b/content/mail-2925.js @@ -515,8 +515,9 @@ function detectMail2925ViewState() { return { view: 'limit', limitMessage }; } - if (findMailItems().length > 0) { - return { view: 'mailbox', limitMessage: '' }; + const mailboxEmail = getMail2925DisplayedMailboxEmail(); + if (findMailItems().length > 0 || mailboxEmail) { + return { view: 'mailbox', limitMessage: '', mailboxEmail }; } if (findMail2925LoginPasswordInput() && findMail2925LoginEmailInput()) { @@ -531,6 +532,62 @@ function detectMail2925ViewState() { return { view: 'unknown', limitMessage: '' }; } +function getMail2925DisplayedMailboxEmail() { + const directSelectors = [ + '[class*="user"] [class*="mail"]', + '[class*="user"] [class*="email"]', + '[class*="account"] [class*="mail"]', + '[class*="account"] [class*="email"]', + '[class*="header"] [class*="mail"]', + '[class*="header"] [class*="email"]', + ]; + + for (const selector of directSelectors) { + const candidates = document.querySelectorAll(selector); + for (const candidate of candidates) { + if (!isVisibleNode(candidate) || isMailItemNode(candidate)) { + continue; + } + const email = extractEmails(candidate.textContent || candidate.innerText || '')[0] || ''; + if (email) { + return email; + } + } + } + + const topCandidates = Array.from(document.querySelectorAll('body *')) + .filter((node) => { + if (!isVisibleNode(node) || isMailItemNode(node)) { + return false; + } + const rect = typeof node.getBoundingClientRect === 'function' + ? node.getBoundingClientRect() + : null; + if (!rect) return false; + return rect.top >= 0 && rect.top <= Math.max(window.innerHeight * 0.35, 280); + }) + .map((node) => { + const email = extractEmails(node.textContent || node.innerText || '')[0] || ''; + return { node, email }; + }) + .filter((entry) => entry.email); + + if (!topCandidates.length) { + return ''; + } + + topCandidates.sort((left, right) => { + const leftRect = left.node.getBoundingClientRect(); + const rightRect = right.node.getBoundingClientRect(); + if (leftRect.top !== rightRect.top) { + return leftRect.top - rightRect.top; + } + return leftRect.left - rightRect.left; + }); + + return topCandidates[0]?.email || ''; +} + function isCheckboxChecked(node) { const checkbox = node?.matches?.('input[type="checkbox"], [role="checkbox"]') ? node @@ -873,6 +930,7 @@ async function ensureMail2925Session(payload = {}) { const email = String(payload?.email || '').trim(); const password = String(payload?.password || ''); const forceLogin = Boolean(payload?.forceLogin); + const allowLoginWhenOnLoginPage = payload?.allowLoginWhenOnLoginPage !== false; log(`步骤 0:2925 登录态检查开始,当前地址 ${location.href},forceLogin=${forceLogin ? 'true' : 'false'}`, 'info'); for (let attempt = 0; attempt < 10; attempt += 1) { @@ -893,9 +951,19 @@ async function ensureMail2925Session(payload = {}) { ok: true, loggedIn: true, currentView: 'mailbox', + mailboxEmail: currentState.mailboxEmail || '', }; } if (currentState.view === 'login') { + if (!forceLogin && !allowLoginWhenOnLoginPage) { + return { + ok: false, + loggedIn: false, + currentView: 'login', + requiresLogin: true, + mailboxEmail: '', + }; + } break; } await sleep(500); @@ -908,6 +976,7 @@ async function ensureMail2925Session(payload = {}) { ok: true, loggedIn: true, currentView: 'mailbox', + mailboxEmail: loginState.mailboxEmail || '', }; } if (loginState.view === 'limit') { @@ -919,6 +988,15 @@ async function ensureMail2925Session(payload = {}) { limitMessage: loginState.limitMessage, }; } + if (!forceLogin && !allowLoginWhenOnLoginPage && loginState.view === 'login') { + return { + ok: false, + loggedIn: false, + currentView: 'login', + requiresLogin: true, + mailboxEmail: '', + }; + } const emailInput = findMail2925LoginEmailInput(); const passwordInput = findMail2925LoginPasswordInput(); @@ -951,6 +1029,7 @@ async function ensureMail2925Session(payload = {}) { loggedIn: true, currentView: 'mailbox', usedCredentials: true, + mailboxEmail: finalState.mailboxEmail || getMail2925DisplayedMailboxEmail() || '', }; } diff --git a/content/signup-page.js b/content/signup-page.js index 6f68fba..53881b7 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -385,13 +385,82 @@ function getSignupEntryStateSummary(snapshot = inspectSignupEntryState()) { } function getSignupEntryDiagnostics() { + const view = typeof window !== 'undefined' ? window : globalThis; + const safeGetComputedStyle = (el) => { + if (!el || typeof view?.getComputedStyle !== 'function') { + return null; + } + try { + return view.getComputedStyle(el); + } catch { + return null; + } + }; + const buildRectSummary = (el) => { + const rect = typeof el?.getBoundingClientRect === 'function' + ? el.getBoundingClientRect() + : null; + return rect + ? { + width: Math.round(rect.width || 0), + height: Math.round(rect.height || 0), + } + : null; + }; + const buildVisibilityMeta = (el) => { + const style = safeGetComputedStyle(el); + return { + className: String(el?.className || '').slice(0, 200), + hidden: Boolean(el?.hidden), + ariaHidden: el?.getAttribute?.('aria-hidden') || '', + inert: typeof el?.hasAttribute === 'function' ? el.hasAttribute('inert') : false, + display: style?.display || '', + visibility: style?.visibility || '', + opacity: style?.opacity || '', + pointerEvents: style?.pointerEvents || '', + }; + }; + const findBlockingAncestor = (el) => { + let current = el?.parentElement || null; + while (current) { + const style = safeGetComputedStyle(current); + const rect = buildRectSummary(current); + const hidden = Boolean(current.hidden); + const ariaHidden = current.getAttribute?.('aria-hidden') || ''; + const inert = typeof current.hasAttribute === 'function' ? current.hasAttribute('inert') : false; + const blockedByStyle = Boolean( + style + && ( + style.display === 'none' + || style.visibility === 'hidden' + || style.opacity === '0' + || style.pointerEvents === 'none' + ) + ); + const blockedByRect = Boolean(rect && (rect.width === 0 || rect.height === 0)); + if (hidden || ariaHidden === 'true' || inert || blockedByStyle || blockedByRect) { + return { + tag: (current.tagName || '').toLowerCase(), + id: current.id || '', + className: String(current.className || '').slice(0, 200), + hidden, + ariaHidden, + inert, + display: style?.display || '', + visibility: style?.visibility || '', + opacity: style?.opacity || '', + pointerEvents: style?.pointerEvents || '', + rect, + }; + } + current = current.parentElement; + } + return null; + }; const actionCandidates = document.querySelectorAll( 'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]' ); const allActions = Array.from(actionCandidates).map((el) => { - const rect = typeof el?.getBoundingClientRect === 'function' - ? el.getBoundingClientRect() - : null; const text = getActionText(el); return { tag: (el.tagName || '').toLowerCase(), @@ -399,12 +468,7 @@ function getSignupEntryDiagnostics() { text: text.slice(0, 80), visible: isVisibleElement(el), enabled: isActionEnabled(el), - rect: rect - ? { - width: Math.round(rect.width || 0), - height: Math.round(rect.height || 0), - } - : null, + rect: buildRectSummary(el), }; }); const visibleActions = Array.from(actionCandidates) @@ -417,7 +481,20 @@ function getSignupEntryDiagnostics() { enabled: isActionEnabled(el), })) .filter((item) => item.text); - const signupLikeActions = allActions + const signupLikeActions = Array.from(actionCandidates) + .map((el) => { + const text = getActionText(el); + return { + tag: (el.tagName || '').toLowerCase(), + type: el.getAttribute?.('type') || '', + text: text.slice(0, 80), + visible: isVisibleElement(el), + enabled: isActionEnabled(el), + rect: buildRectSummary(el), + ...buildVisibilityMeta(el), + blockingAncestor: findBlockingAncestor(el), + }; + }) .filter((item) => item.text && SIGNUP_ENTRY_TRIGGER_PATTERN.test(item.text)) .slice(0, 12); @@ -425,15 +502,133 @@ function getSignupEntryDiagnostics() { url: location.href, title: document.title || '', readyState: document.readyState || '', + viewport: { + innerWidth: Math.round(Number(view?.innerWidth) || 0), + innerHeight: Math.round(Number(view?.innerHeight) || 0), + outerWidth: Math.round(Number(view?.outerWidth) || 0), + outerHeight: Math.round(Number(view?.outerHeight) || 0), + devicePixelRatio: Number(view?.devicePixelRatio) || 0, + }, hasEmailInput: Boolean(getSignupEmailInput()), hasPasswordInput: Boolean(getSignupPasswordInput()), bodyContainsSignupText: SIGNUP_ENTRY_TRIGGER_PATTERN.test(getPageTextSnapshot()), + signupLikeActionCounts: { + total: signupLikeActions.length, + visible: signupLikeActions.filter((item) => item.visible).length, + hidden: signupLikeActions.filter((item) => !item.visible).length, + }, signupLikeActions, visibleActions, bodyTextPreview: getPageTextSnapshot().slice(0, 240), }; } +function getSignupPasswordDiagnostics() { + const view = typeof window !== 'undefined' ? window : globalThis; + const safeGetComputedStyle = (el) => { + if (!el || typeof view?.getComputedStyle !== 'function') { + return null; + } + try { + return view.getComputedStyle(el); + } catch { + return null; + } + }; + const buildRectSummary = (el) => { + const rect = typeof el?.getBoundingClientRect === 'function' + ? el.getBoundingClientRect() + : null; + return rect + ? { + width: Math.round(rect.width || 0), + height: Math.round(rect.height || 0), + } + : null; + }; + const buildInputSummary = (el) => { + const style = safeGetComputedStyle(el); + return { + tag: (el?.tagName || '').toLowerCase(), + type: el?.getAttribute?.('type') || el?.type || '', + name: el?.getAttribute?.('name') || el?.name || '', + id: el?.id || '', + autocomplete: el?.getAttribute?.('autocomplete') || '', + placeholder: String(el?.getAttribute?.('placeholder') || '').slice(0, 80), + visible: isVisibleElement(el), + enabled: isActionEnabled(el), + valueLength: String(el?.value || '').length, + rect: buildRectSummary(el), + className: String(el?.className || '').slice(0, 200), + display: style?.display || '', + visibility: style?.visibility || '', + opacity: style?.opacity || '', + pointerEvents: style?.pointerEvents || '', + formAction: el?.form?.action || '', + }; + }; + const buildActionSummary = (el) => { + const style = safeGetComputedStyle(el); + return { + tag: (el?.tagName || '').toLowerCase(), + type: el?.getAttribute?.('type') || el?.type || '', + role: el?.getAttribute?.('role') || '', + text: getActionText(el).slice(0, 120), + visible: isVisibleElement(el), + enabled: isActionEnabled(el), + rect: buildRectSummary(el), + className: String(el?.className || '').slice(0, 200), + display: style?.display || '', + visibility: style?.visibility || '', + opacity: style?.opacity || '', + pointerEvents: style?.pointerEvents || '', + dataDdActionName: el?.getAttribute?.('data-dd-action-name') || '', + formAction: el?.form?.action || '', + }; + }; + const passwordInputs = Array.from(document.querySelectorAll( + 'input[type="password"], input[name*="password" i], input[autocomplete="new-password"], input[autocomplete="current-password"]' + )) + .map(buildInputSummary) + .slice(0, 8); + const actionCandidates = Array.from(document.querySelectorAll( + 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]' + )) + .map(buildActionSummary) + .filter((item) => item.text) + .slice(0, 16); + const visibleActions = actionCandidates.filter((item) => item.visible).slice(0, 12); + const submitButton = getSignupPasswordSubmitButton({ allowDisabled: true }); + const oneTimeCodeTrigger = findOneTimeCodeLoginTrigger(); + const retryState = getSignupPasswordTimeoutErrorPageState(); + + return { + url: location.href, + title: document.title || '', + readyState: document.readyState || '', + displayedEmail: getSignupPasswordDisplayedEmail(), + hasVisiblePasswordInput: Boolean(getSignupPasswordInput()), + passwordInputCount: passwordInputs.length, + visiblePasswordInputCount: passwordInputs.filter((item) => item.visible).length, + passwordInputs, + submitButton: submitButton ? buildActionSummary(submitButton) : null, + oneTimeCodeTrigger: oneTimeCodeTrigger ? buildActionSummary(oneTimeCodeTrigger) : null, + retryPage: Boolean(retryState), + retryEnabled: Boolean(retryState?.retryEnabled), + userAlreadyExistsBlocked: Boolean(retryState?.userAlreadyExistsBlocked), + visibleActions, + bodyTextPreview: getPageTextSnapshot().slice(0, 240), + }; +} + +function logSignupPasswordDiagnostics(context, level = 'warn') { + try { + log(`${context}:密码页诊断快照:${JSON.stringify(getSignupPasswordDiagnostics())}`, level); + } catch (error) { + console.warn('[MultiPage:signup-page] failed to build signup password diagnostics:', error?.message || error); + } +} + async function waitForSignupEntryState(options = {}) { const { timeout = 15000, @@ -620,6 +815,10 @@ async function step3_fillEmailPassword(payload) { snapshot = inspectSignupEntryState(); } + if (snapshot.state !== 'password_page' || !snapshot.passwordInput) { + logSignupPasswordDiagnostics('步骤 3:未能识别可填写的密码输入框'); + } + if (snapshot.state !== 'password_page' || !snapshot.passwordInput) { throw new Error('在密码页未找到密码输入框。URL: ' + location.href); } @@ -635,6 +834,12 @@ async function step3_fillEmailPassword(payload) { || getSignupPasswordSubmitButton({ allowDisabled: true }) || await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null); + if (!submitBtn) { + logSignupPasswordDiagnostics('步骤 3:未找到可提交的密码页按钮'); + } else if (typeof findOneTimeCodeLoginTrigger === 'function' && findOneTimeCodeLoginTrigger()) { + logSignupPasswordDiagnostics('步骤 3:当前密码页同时存在一次性验证码入口', 'info'); + } + // Report complete BEFORE submit, because submit causes page navigation // which kills the content script connection const signupVerificationRequestedAt = submitBtn ? Date.now() : null; @@ -1640,6 +1845,7 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { const start = Date.now(); let recoveryRound = 0; const maxRecoveryRounds = 3; + let passwordPageDiagnosticsLogged = false; while (Date.now() - start < timeout && recoveryRound < maxRecoveryRounds) { throwIfStopped(); @@ -1678,6 +1884,10 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { } if (snapshot.state === 'password') { + if (!passwordPageDiagnosticsLogged) { + passwordPageDiagnosticsLogged = true; + logSignupPasswordDiagnostics(`${prepareLogLabel}:页面仍停留在密码页`); + } if (!password) { throw new Error('当前回到了密码页,但没有可用密码,无法自动重新提交。'); } diff --git a/content/vps-panel.js b/content/vps-panel.js index 4e84d49..63af31c 100644 --- a/content/vps-panel.js +++ b/content/vps-panel.js @@ -19,7 +19,10 @@ //
// // +//
回调 URL 已提交,等待认证中...
+//
回调 URL 提交失败: ...
//
+//
等待认证中... / 认证成功! / 认证失败: ...
// // @@ -192,19 +195,19 @@ function isLocalhostOAuthCallbackUrl(rawUrl) { function getStatusBadgeSelectors() { return [ - '#root > div > div > div > main > div > div > div > div > div:nth-child(1) > div > div.OAuthPage-module__cardContent___1sXLA > div.status-badge', - '#root .OAuthPage-module__cardContent___1sXLA > .status-badge', - '.OAuthPage-module__cardContent___1sXLA > .status-badge', + '#root .OAuthPage-module__cardContent___1sXLA .status-badge', + '[class*="cardContent"] .status-badge', '.status-badge', ]; } function getStatusBadgeEntries() { + const searchRoot = findCodexOAuthCard() || document; const seen = new Set(); const entries = []; for (const selector of getStatusBadgeSelectors()) { - const candidates = document.querySelectorAll(selector); + const candidates = searchRoot.querySelectorAll(selector); for (const candidate of candidates) { if (seen.has(candidate)) continue; seen.add(candidate); @@ -238,21 +241,63 @@ function normalizeStep9StatusText(statusText) { } function isOAuthCallbackTimeoutFailure(statusText) { - return /认证失败:\s*(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded)/i.test(statusText || ''); + return /(?:认证失败\s*[::]?\s*)?(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded|OAuth flow timed out)/i.test(statusText || ''); +} + +function getStep10StatusBadgeLocation(element) { + if (element?.closest?.('[class*="callbackSection"]')) { + return 'callback'; + } + if (element?.closest?.('[class*="cardContent"]')) { + return 'main'; + } + return 'page'; +} + +function isStep10CallbackSubmittedStatus(statusText) { + const text = normalizeStep9StatusText(statusText); + return /回调\s*url\s*已提交.*等待认证中/i.test(text) + || /callback\s*url\s*submitted.*waiting/i.test(text); +} + +function isStep10CallbackFailureText(statusText) { + const text = normalizeStep9StatusText(statusText); + if (!text) return false; + return /(?:回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[::,,]?\s*/i.test(text) + || /请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(text); +} + +function isStep10MainWaitingStatus(statusText) { + const text = normalizeStep9StatusText(statusText); + return /等待认证中/i.test(text); +} + +function isStep10MainFailureText(statusText) { + const text = normalizeStep9StatusText(statusText); + if (!text) return false; + if (/^认证失败\s*[::]?\s*/i.test(text)) return true; + return /bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out|request failed with status code \d+|timeout of \d+ms exceeded|network error|failed to fetch/i.test(text); } function isStep9FailureText(statusText) { const text = normalizeStep9StatusText(statusText); if (!text) return false; if (isOAuthCallbackTimeoutFailure(text)) return true; + if (isStep10CallbackFailureText(text)) return true; + if (isStep10MainFailureText(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); + return /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)); + const text = normalizeStep9StatusText(statusText); + if (!text) return false; + return STEP9_SUCCESS_STATUSES.has(text) + || /^认证成功[!!]?$/i.test(text) + || /^Authentication successful!?$/i.test(text) + || /^Аутентификация успешна!?$/i.test(text); } function isStep9SuccessLikeStatus(statusText) { @@ -340,6 +385,7 @@ function createStep9Entry(candidate, selector) { return { element: candidate, selector, + location: getStep10StatusBadgeLocation(candidate), visible: isVisibleElement(candidate), text: normalizeStep9StatusText(candidate?.textContent || ''), className, @@ -364,36 +410,72 @@ function getStep9PageErrorSelectors() { } function getStep9PageErrorEntries() { + const cardRoot = findCodexOAuthCard(); + const searchRoots = [cardRoot, document].filter(Boolean); 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; + for (const root of searchRoots) { + for (const selector of getStep9PageErrorSelectors()) { + const candidates = root.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); + const entry = createStep9Entry(candidate, selector); + if (/\bstatus-badge\b/i.test(entry.className)) continue; + if (!isStep9FailureText(entry.text)) continue; + entries.push(entry); + } } } return entries; } +function formatStep10StatusSummaryValue(text, emptyText = '无') { + return text ? `"${getInlineTextSnippet(text, 80)}"` : emptyText; +} + +function isStep10BrowserSwitchRequiredConflict(diagnostics = {}) { + return Boolean(diagnostics?.hasExactSuccessVisibleBadge) + && /请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(String(diagnostics?.callbackFailureText || '')); +} + +function getStep10BrowserSwitchRequiredMessage(diagnostics = {}) { + const callbackFailureText = normalizeStep9StatusText(diagnostics?.callbackFailureText || ''); + return [ + '检测到 CPA 页面同时显示“认证成功”和“回调 URL 提交失败: 请更新CLI Proxy API或检查连接”。', + '这类冲突状态通常通过更换浏览器可以解决,请更换浏览器后重新进行注册登录。', + callbackFailureText ? `面板原文:${callbackFailureText}` : '', + ].filter(Boolean).join(' '); +} + function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSnippet = '') { const visibleEntries = entries.filter((entry) => entry.visible); - const successLikeEntries = visibleEntries.filter((entry) => isStep9SuccessLikeStatus(entry.text)); - const exactSuccessEntries = visibleEntries.filter((entry) => isStep9SuccessStatus(entry.text) && !entry.hasErrorVisualSignal); - const failureEntries = visibleEntries.filter((entry) => isStep9FailureText(entry.text)); + const callbackEntries = visibleEntries.filter((entry) => entry.location === 'callback'); + const mainEntries = visibleEntries.filter((entry) => entry.location === 'main'); + const successLikeEntries = mainEntries.filter((entry) => isStep9SuccessLikeStatus(entry.text)); + const exactSuccessEntries = mainEntries.filter((entry) => isStep9SuccessStatus(entry.text) && !entry.hasErrorVisualSignal); + const callbackSubmittedEntries = callbackEntries.filter((entry) => isStep10CallbackSubmittedStatus(entry.text) && !entry.hasErrorVisualSignal); + const callbackFailureEntries = callbackEntries.filter((entry) => isStep10CallbackFailureText(entry.text)); + const mainWaitingEntries = mainEntries.filter((entry) => isStep10MainWaitingStatus(entry.text) && !entry.hasErrorVisualSignal); + const mainFailureEntries = mainEntries.filter((entry) => isStep10MainFailureText(entry.text)); + const failureEntries = [...callbackFailureEntries, ...mainFailureEntries]; 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 selectedEntry = decisiveFailureEntry + || exactSuccessEntries[0] + || callbackSubmittedEntries[0] + || mainWaitingEntries[0] + || visibleEntries[0] + || null; const selectedText = selectedEntry?.text || ''; const visibleSummary = summarizeStatusBadgeEntries(visibleEntries); + const callbackSummary = summarizeStatusBadgeEntries(callbackEntries); + const mainSummary = summarizeStatusBadgeEntries(mainEntries); const successLikeSummary = summarizeStatusBadgeEntries(successLikeEntries); const exactSuccessSummary = summarizeStatusBadgeEntries(exactSuccessEntries); const failureSummary = summarizeStatusBadgeEntries(failureEntries); @@ -406,10 +488,20 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn selectedText, exactSuccessText: exactSuccessEntries[0]?.text || '', failureText: decisiveFailureEntry?.text || '', + failureSource: decisiveFailureEntry?.location || (pageErrorEntries.length ? 'page' : ''), visibleCount: visibleEntries.length, visibleSummary, + callbackSummary, + mainSummary, + callbackStatusText: callbackEntries[0]?.text || '', + callbackSubmittedText: callbackSubmittedEntries[0]?.text || '', + callbackFailureText: callbackFailureEntries[0]?.text || '', + mainStatusText: mainEntries[0]?.text || '', + mainWaitingText: mainWaitingEntries[0]?.text || '', + mainFailureText: mainFailureEntries[0]?.text || '', hasSuccessLikeVisibleBadge: successLikeEntries.length > 0, hasExactSuccessVisibleBadge: exactSuccessEntries.length > 0, + hasCallbackSubmittedBadge: callbackSubmittedEntries.length > 0, hasFailureVisibleBadge: allFailureEntries.length > 0, hasErrorStyledVisibleBadge: errorStyledEntries.length > 0, successLikeSummary, @@ -422,6 +514,8 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn selectedText, visibleCount: visibleEntries.length, visibleSummary, + callbackSummary, + mainSummary, successLikeSummary, exactSuccessSummary, failureSummary, @@ -429,8 +523,8 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn errorStyledSummary, }), summary: selectedText - ? `当前聚焦状态="${getInlineTextSnippet(selectedText, 80)}";可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix}` - : `当前未选中任何可见状态徽标;可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`, + ? `当前聚焦状态=${formatStep10StatusSummaryValue(selectedText)};回调提示=${formatStep10StatusSummaryValue(callbackEntries[0]?.text || '')};主状态=${formatStep10StatusSummaryValue(mainEntries[0]?.text || '')};页面错误=${formatStep10StatusSummaryValue(pageErrorEntries[0]?.text || '')};可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix}` + : `当前未选中任何可见状态;回调提示=${formatStep10StatusSummaryValue(callbackEntries[0]?.text || '')};主状态=${formatStep10StatusSummaryValue(mainEntries[0]?.text || '')};页面错误=${formatStep10StatusSummaryValue(pageErrorEntries[0]?.text || '')};可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`, }; } @@ -452,11 +546,129 @@ function getStatusBadgeText() { return diagnostics.selectedText; } +function extractStep10FailureDetail(statusText, sourceKind = '') { + const text = normalizeStep9StatusText(statusText); + if (!text) return ''; + if (sourceKind === 'callback' || isStep10CallbackFailureText(text)) { + return text.replace(/^(?:回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[::,,]?\s*/i, '').trim(); + } + if (sourceKind === 'main' || isStep10MainFailureText(text)) { + return text.replace(/^认证失败\s*[::]?\s*/i, '').trim(); + } + return text; +} + +function explainStep10Failure(statusText, sourceKind = 'unknown') { + const rawText = normalizeStep9StatusText(statusText); + const detail = extractStep10FailureDetail(rawText, sourceKind) || rawText; + const phaseLabel = sourceKind === 'callback' + ? '回调提交阶段' + : sourceKind === 'main' + ? '认证结果阶段' + : '页面状态阶段'; + + const rules = [ + { + code: 'callback_submit_api_unavailable', + pattern: /请更新\s*cli\s*proxy\s*api\s*或检查连接/i, + message: 'CPA 面板无法把回调提交给后台,通常是 CLI Proxy API 版本过旧、管理接口未启动,或当前面板与后端连接异常。', + }, + { + code: 'oauth_state_expired', + pattern: /unknown or expired state/i, + message: '当前 OAuth 会话在 CPA 中已不存在或已过期,通常是使用了旧回调链接、刷新过新的授权链接后仍提交旧链接,或 CPA 刚重启过。', + }, + { + code: 'oauth_not_pending', + pattern: /oauth flow is not pending/i, + message: '当前 OAuth 会话已经不在等待状态,通常是重复提交、提交过慢,或这轮认证此前已经结束。', + }, + { + code: 'callback_state_invalid', + pattern: /invalid state|state is required|missing_state/i, + message: '回调链接里的 state 缺失或无效,通常是复制了不完整的 localhost 回调链接,或提交了不属于这一轮的旧链接。', + }, + { + code: 'callback_missing_result', + pattern: /code or error is required/i, + message: '回调链接里既没有授权码,也没有错误信息,通常是复制的 localhost 回调链接不完整。', + }, + { + code: 'callback_invalid_url', + pattern: /invalid redirect_url/i, + message: '提交给 CPA 的回调链接格式无法解析,通常是粘贴内容不完整、带了多余字符,或并不是 localhost OAuth 回调地址。', + }, + { + code: 'callback_provider_mismatch', + pattern: /provider does not match state/i, + message: '这条回调不属于当前这次 Codex OAuth,会话与回调来源对不上,通常是混用了其他轮次或其他提供方的回调。', + }, + { + code: 'callback_persist_failed', + pattern: /failed to persist oauth callback/i, + message: 'CPA 已收到回调,但无法把回调结果写入本地缓存文件,通常是认证目录权限、磁盘或运行环境异常。', + }, + { + code: 'oauth_bad_request', + pattern: /^bad request$/i, + message: 'CPA 已收到回调,但 OpenAI OAuth 回调本身返回了错误。常见于用户取消授权、请求过期,或这条回调已经失效。', + }, + { + code: 'oauth_state_mismatch', + pattern: /state code error/i, + message: 'CPA 校验到回调里的 state 与当前 OAuth 会话不一致,通常是步骤 1 已刷新过新的授权链接,但步骤 10 仍提交旧回调。', + }, + { + code: 'oauth_code_exchange_failed', + pattern: /failed to exchange authorization code for tokens/i, + message: 'CPA 已收到授权码,但向 OpenAI 交换令牌失败。常见于 CPA 到 OpenAI 的网络或代理异常,或授权码已过期。', + }, + { + code: 'oauth_token_save_failed', + pattern: /failed to save authentication tokens/i, + message: 'CPA 已完成认证,但保存认证文件失败。常见于认证目录权限、磁盘写入,或 post-auth hook 异常。', + }, + { + code: 'oauth_callback_timeout', + pattern: /timeout waiting for oauth callback|oauth flow timed out/i, + message: 'CPA 长时间没有把这轮 OAuth 流程走完。常见于提交太晚、面板轮询异常,或后端状态没有及时刷新。', + }, + { + code: 'oauth_http_timeout', + pattern: /timeout of \d+ms exceeded/i, + message: 'CPA 面板在请求后台接口时超时,通常是 CLI Proxy API 响应过慢、接口未启动,或网络连接不稳定。', + }, + { + code: 'oauth_http_status_error', + pattern: /request failed with status code \d+/i, + message: 'CPA 面板请求后台接口时收到了异常 HTTP 状态码,通常是接口异常、反向代理配置错误,或当前会话已失效。', + }, + { + code: 'oauth_network_error', + pattern: /network error|failed to fetch/i, + message: 'CPA 面板与后台通信失败,通常是网络不通、管理接口未启动,或浏览器当前连接已断开。', + }, + ]; + + const matchedRule = rules.find((rule) => rule.pattern.test(detail) || rule.pattern.test(rawText)); + const message = matchedRule + ? matchedRule.message + : `CPA 在${phaseLabel}返回了未归类的失败,请结合面板原文进一步排查。`; + + return { + code: matchedRule?.code || 'oauth_unknown_failure', + phaseLabel, + rawText, + detail, + userMessage: `CPA 在${phaseLabel}返回失败:${message} 面板原文:${rawText}`, + }; +} + async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS) { const start = Date.now(); let lastDiagnosticsSignature = ''; let lastHeartbeatLoggedAt = 0; - let lastSuccessLikeMismatchSignature = ''; + let lastCallbackSubmittedSignature = ''; let lastSuccessFailureConflictSignature = ''; while (Date.now() - start < timeout) { @@ -475,26 +687,28 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS console.log(LOG_PREFIX, '[Step 9] still waiting for success badge', diagnostics); } - if (diagnostics.hasSuccessLikeVisibleBadge && !diagnostics.hasExactSuccessVisibleBadge) { - const mismatchSignature = JSON.stringify({ - selectedText: diagnostics.selectedText, - successLikeSummary: diagnostics.successLikeSummary, - visibleSummary: diagnostics.visibleSummary, - errorStyledSummary: diagnostics.errorStyledSummary, + if (diagnostics.hasCallbackSubmittedBadge && !diagnostics.hasExactSuccessVisibleBadge) { + const callbackSubmittedSignature = JSON.stringify({ + callbackStatusText: diagnostics.callbackStatusText, + mainStatusText: diagnostics.mainStatusText, }); - if (mismatchSignature !== lastSuccessLikeMismatchSignature) { - lastSuccessLikeMismatchSignature = mismatchSignature; - const errorStyledSuffix = diagnostics.hasErrorStyledVisibleBadge - ? `;错误样式徽标:${diagnostics.errorStyledSummary}` - : ''; + if (callbackSubmittedSignature !== lastCallbackSubmittedSignature) { + lastCallbackSubmittedSignature = callbackSubmittedSignature; log( - `步骤 10:检测到“认证成功”相关徽标,但未命中精确成功条件。当前聚焦="${getInlineTextSnippet(diagnostics.selectedText || '(空)', 80)}";成功相关徽标:${diagnostics.successLikeSummary}${errorStyledSuffix}`, - 'warn' + `步骤 10:CPA 已接受 localhost 回调,正在等待后台完成认证。回调提示=${formatStep10StatusSummaryValue(diagnostics.callbackStatusText)};主状态=${formatStep10StatusSummaryValue(diagnostics.mainStatusText)}`, + 'info' ); - console.warn(LOG_PREFIX, '[Step 9] success-like badge detected without exact match', diagnostics); + console.info(LOG_PREFIX, '[Step 9] callback accepted and waiting for auth completion', diagnostics); } } + if (isStep10BrowserSwitchRequiredConflict(diagnostics)) { + const browserSwitchMessage = getStep10BrowserSwitchRequiredMessage(diagnostics); + log(`步骤 10:${browserSwitchMessage}`, 'error'); + console.error(LOG_PREFIX, '[Step 9] browser-switch conflict detected', diagnostics); + throw new Error(`BROWSER_SWITCH_REQUIRED::${browserSwitchMessage}`); + } + if (diagnostics.hasExactSuccessVisibleBadge && diagnostics.hasFailureVisibleBadge) { const conflictSignature = JSON.stringify({ exactSuccessSummary: diagnostics.exactSuccessSummary, @@ -515,10 +729,11 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS } if (diagnostics.failureText) { + const failureExplanation = explainStep10Failure(diagnostics.failureText, diagnostics.failureSource || 'unknown'); if (isOAuthCallbackTimeoutFailure(diagnostics.failureText)) { - throw new Error(`STEP9_OAUTH_TIMEOUT::${diagnostics.failureText}`); + throw new Error(`STEP9_OAUTH_TIMEOUT::${failureExplanation.userMessage}`); } - throw new Error(`STEP9_OAUTH_RETRY::${diagnostics.failureText}`); + throw new Error(`STEP9_OAUTH_RETRY::${failureExplanation.userMessage}`); } if (diagnostics.exactSuccessText) { return diagnostics.exactSuccessText; @@ -530,10 +745,18 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS const finalText = finalDiagnostics.failureText || finalDiagnostics.selectedText; const diagnosticsSuffix = ` 当前诊断:${finalDiagnostics.summary}`; if (isOAuthCallbackTimeoutFailure(finalText)) { - throw new Error(`STEP9_OAUTH_TIMEOUT::${finalText}${diagnosticsSuffix}`); + const failureExplanation = explainStep10Failure(finalText, finalDiagnostics.failureSource || 'main'); + throw new Error(`STEP9_OAUTH_TIMEOUT::${failureExplanation.userMessage}${diagnosticsSuffix}`); } if (isStep9FailureText(finalText)) { - throw new Error(`STEP9_OAUTH_RETRY::${finalText}${diagnosticsSuffix}`); + const failureExplanation = explainStep10Failure(finalText, finalDiagnostics.failureSource || 'unknown'); + throw new Error(`STEP9_OAUTH_RETRY::${failureExplanation.userMessage}${diagnosticsSuffix}`); + } + if (finalDiagnostics.hasCallbackSubmittedBadge || finalDiagnostics.mainWaitingText) { + throw new Error( + 'STEP9_OAUTH_TIMEOUT::CPA 已接受回调,但 120 秒内仍未进入认证成功状态。通常是 CPA 后台处理过慢、面板轮询异常,或 CPA 到 OpenAI 的网络/代理存在问题。' + + diagnosticsSuffix + ); } throw new Error(finalText ? `CPA 面板状态未进入成功状态,当前为“${finalText}”。${diagnosticsSuffix}` @@ -583,6 +806,11 @@ function findCodexOAuthHeader() { }) || null; } +function findCodexOAuthCard() { + const header = findCodexOAuthHeader(); + return header?.closest('.card, [class*="card"]') || header || null; +} + function findOAuthCardLoginButton(header) { const card = header?.closest('.card, [class*="card"]') || header?.parentElement || document; const candidates = card.querySelectorAll('button.btn.btn-primary, button.btn-primary, button.btn'); diff --git a/docs/使用教程.md b/docs/使用教程.md new file mode 100644 index 0000000..e12a029 --- /dev/null +++ b/docs/使用教程.md @@ -0,0 +1,190 @@ +本教程用于说明相关项目地址、扩展更新方式,以及 `Clash Verge` 的 `🔁 非港轮询` 配置方法。 + +## 适用场景 + +- 需要拉取并部署 `cpa` 或 `sub2api` 项目 +- 已经安装本扩展,想更新到最新版本 +- 想用更方便的方式长期同步扩展更新 +- 需要在 `Clash Verge` 中启用 `🔁 非港轮询` + +## 准备内容 + +- 可以访问 `GitHub` +- 已安装好的扩展文件夹 +- 可以打开浏览器的 `扩展程序管理` 页面 +- 如需部署 `cpa`,部署环境必须可以访问 `OpenAI` +- 已安装 `Clash Verge`,并已导入可用订阅 + +## 操作步骤 + +### 第一部分:相关项目地址与部署说明 + +1. 查看项目地址 + `cpa` 项目地址: + `sub2api` 项目地址: + +2. 拉取项目到本地 + 先将你需要的项目拉取到本地目录。 + 可以使用 `git clone`,也可以直接下载项目压缩包后解压。 + +3. 让 AI 直接协助部署 + 部署教程这里不单独展开。 + 将项目拉取下来后,直接让 AI 帮你部署即可。 + +4. 确认 `cpa` 的部署环境 + 如果你部署的是 `cpa`,部署所在环境必须可以访问 `OpenAI`。 + 否则可能会出现第十步显示认证成功,但实际上没有生成认证文件的情况。 + +### 第二部分:更新扩展 + +1. 使用 `GitHub Desktop` 更新 + 先安装 `GitHub Desktop`。 + 把当前扩展仓库交给 `GitHub Desktop` 管理后,之后就可以随时更新。 + `GitHub Desktop` 的具体使用方法本教程不展开,需要时可直接询问豆包。 + +2. 使用 `git pull` 更新(最方便,最推荐) + 先在本地安装 `git`。 + 打开终端后执行 `cd 扩展文件夹路径` 进入当前扩展目录。 + 接着执行 `git pull` 拉取最新更新。 + 这是最方便、最推荐的更新方式。 + +3. 手动下载最新版本覆盖更新 + 点击左上角的版本号或 `更新` 按钮。 + 下载最新版本到本地后,直接覆盖现有扩展文件夹即可。 + +4. 更新完成后重新加载扩展 + 不论使用哪种更新方式,更新完成后都必须打开浏览器的 `扩展程序管理` 页面。 + 找到该扩展后,手动点击一次 `重新加载`。 + 这一步一定要做,否则浏览器可能仍在使用旧版本。 + +### 第三部分:配置 `Clash Verge` 的 `🔁 非港轮询` + +#### 第一步:添加扩展脚本 + +1. 打开 `Clash Verge`,进入左侧的 `订阅`(`Profiles`)界面。 +2. 在右上角或对应位置找到并双击打开 `全局扩展脚本`(`Merge/Script`)。 +3. 将里面的内容全部清空,替换为下方脚本代码。 +4. 点击保存,使用右上角保存按钮或按 `Ctrl+S`。 + +💻 脚本代码(如果遇到格式错误,可让豆包帮你修复后再粘贴) + +```javascript +function uniqPrepend(arr, items) { + if (!Array.isArray(arr)) arr = []; + for (var i = items.length - 1; i >= 0; i--) { + var item = items[i]; + var exists = false; + for (var j = 0; j < arr.length; j++) { + if (arr[j] === item) { + exists = true; + break; + } + } + if (!exists) arr.unshift(item); + } + return arr; +} + +function upsertGroup(groups, group) { + for (var i = 0; i < groups.length; i++) { + if (groups[i] && groups[i].name === group.name) { + groups[i] = group; + return groups; + } + } + groups.unshift(group); + return groups; +} + +function main(config, profileName) { + if (!config) return config; + + if (!Array.isArray(config["proxy-groups"])) { + config["proxy-groups"] = []; + } + + var groups = config["proxy-groups"]; + var LB_NAME = "🔁 非港轮询"; + + var excludeRegex = + "(?i)(" + + "香港|hong[ -]?kong|\\bhk\\b|\\bhkg\\b|🇭🇰" + + "|剩余流量|套餐到期|下次重置剩余|重置剩余|到期时间|流量重置" + + "|traffic|expire|expiration|subscription|subscribe|reset|plan" + + ")"; + + groups = upsertGroup(groups, { + name: LB_NAME, + type: "load-balance", + strategy: "round-robin", + "include-all-proxies": true, + "exclude-filter": excludeRegex, + url: "https://www.gstatic.com/generate_204", + interval: 300, + lazy: true, + "expected-status": 204 + }); + + var injected = false; + var entryNameRegex = /节点选择|代理|Proxy|PROXY|默认|GLOBAL|全局|选择/i; + + for (var i = 0; i < groups.length; i++) { + var g = groups[i]; + if (!g || g.type !== "select") continue; + + if (entryNameRegex.test(g.name || "")) { + if (!Array.isArray(g.proxies)) g.proxies = []; + g.proxies = uniqPrepend(g.proxies, [LB_NAME]); + injected = true; + } + } + + if (!injected) { + for (var k = 0; k < groups.length; k++) { + var g2 = groups[k]; + if (g2 && g2.type === "select") { + if (!Array.isArray(g2.proxies)) g2.proxies = []; + g2.proxies = uniqPrepend(g2.proxies, [LB_NAME]); + break; + } + } + } + + config["proxy-groups"] = groups; + return config; +} +``` + +#### 第二步:应用设置 + +1. 切换到左侧的 `代理`(`Proxies`)界面,也就是首页。 +2. 在顶部的分类中,通常会看到 `节点选择`、`Proxy` 或 `当前节点` 之类的分组。 +3. 在对应下拉框中选择 `🔁 非港轮询`。 +4. 确认右上角的 `代理模式`(`Mode`)已经设置为 `规则模式`(`Rule`)。 +5. 确认左侧 `设置` 中的 `系统代理`(`System Proxy`)已经开启。 + +## 常见问题 + +### 为什么第十步显示认证成功,但没有认证文件? + +这通常是因为 `cpa` 部署所在环境无法访问 `OpenAI`。请先确认部署环境可以正常访问 `OpenAI`,然后再重新执行相关认证步骤。 + +### 为什么更新完扩展后还是旧版本? + +如果你只是替换了本地文件,但没有去浏览器的 `扩展程序管理` 页面点击 `重新加载`,浏览器不会立即启用新版本。请更新完成后手动重新加载一次该扩展。 + +### `git pull` 提示不是 Git 仓库怎么办? + +这通常说明你进入的不是正确目录,或者当前文件夹不是通过 `git` 管理的版本。此时可以改用 `GitHub Desktop`,或者直接使用手动下载覆盖的方式更新。 + +### 为什么没有看到 `🔁 非港轮询`? + +请先确认脚本已经完整粘贴并保存,然后回到 `代理` 页面重新查看。若仍未出现,请继续确认当前订阅可用、`代理模式` 为 `规则模式`,并且 `系统代理` 已开启。 + +## 注意事项 + +- 不论使用哪种方式更新扩展,更新完成后都必须在浏览器的 `扩展程序管理` 页面重新加载一次该扩展。 +- 如需部署 `cpa`,请先确认部署环境可以访问 `OpenAI`,否则可能出现第十步认证成功但没有认证文件的问题。 +- 如果你长期更新扩展,优先使用 `git pull`,这是最方便、最推荐的方式。 +- 使用手动覆盖更新时,请直接覆盖当前扩展文件夹,避免同时保留多份不同版本的目录。 +- 在 `Clash Verge` 中粘贴脚本时,请先清空旧内容,再完整粘贴新代码并保存。 \ No newline at end of file diff --git a/manifest.json b/manifest.json index c58c1fa..ced3774 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "manifest_version": 3, - "name": "多页面自动化", + "name": "codex-oauth-automation-extension", "version": "5.8", "version_name": "Pro5.8", "description": "用于自动执行多步骤 OAuth 注册流程", diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index b5002c9..c96c47d 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -2404,6 +2404,19 @@ header { line-height: 1.55; color: var(--text-secondary); margin-bottom: 14px; + white-space: pre-line; +} + +.modal-message a, +.modal-alert a { + color: var(--blue); + text-decoration: underline; + text-underline-offset: 2px; +} + +.modal-message a:hover, +.modal-alert a:hover { + color: var(--cyan); } .modal-alert { diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index dd1e052..966a58c 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -4,7 +4,7 @@ - 多页面自动化面板 + codex-oauth-automation-extension
- 本地 CPA -
- - + 回调方式 +
+ +
- - - - -
+