diff --git a/README.md b/README.md index e5f912e..ffef8ff 100644 --- a/README.md +++ b/README.md @@ -81,33 +81,34 @@ ## 安装 1. 打开 `chrome://extensions/` +2. 开启“开发者模式” +3. 点击“加载已解压的扩展程序” +4. 选择本项目目录 +5. 打开扩展侧边栏 ## 2026-04-17 更新补充:Gmail / 2925 别名邮箱 -本次版本对 `Gmail` 与 `2925` 的注册邮箱逻辑做了统一整理: +本次版本对 `Gmail` 与 `2925 provide` 的注册邮箱逻辑做了统一整理: -- `Gmail` 与 `2925` 现在都走同一套“别名邮箱”逻辑。 +- `Gmail` 与 `2925 provide` 现在都走同一套“别名邮箱”逻辑。 - 两者都不再使用“只填前缀再特殊拼接”的界面交互。 - 两者都要求先填写“基邮箱”: - `Gmail`:例如 `name@gmail.com` - - `2925`:例如 `name@2925.com` + - `2925`(仅 provide 模式):例如 `name@2925.com` - 侧边栏里的“注册邮箱”输入框对这两种模式都已开放,可直接手动填写完整邮箱。 - 侧边栏里的 `获取 / 生成` 按钮对这两种模式也可用,行为与 Duck / Cloudflare 一样,都是“可自动生成,也可手动覆盖”。 +- 当 `Mail = 2925` 且模式切到 `接收邮箱` 时,不再走别名基邮箱链路,而是回退到普通“邮箱生成 / 手动填写注册邮箱”路线,2925 只负责后续收信。 具体行为: - `Gmail` 会基于完整基邮箱生成 `name+tag@gmail.com` -- `2925` 会基于完整基邮箱生成 `name123456@2925.com` +- `2925` 仅在 provide 模式下会基于完整基邮箱生成 `name123456@2925.com` - 如果当前“注册邮箱”里已经是与当前基邮箱兼容的完整邮箱,流程会优先复用,不会强行重新生成 注意: - `2925` 旧的“只填前缀”使用方式已经不再推荐,应该改为填写完整基邮箱 -- 如果你手动填写了与当前 `Gmail / 2925` 基邮箱不匹配的完整邮箱,侧边栏会在保存或执行 Step 3 时拦截 -2. 开启“开发者模式” -3. 点击“加载已解压的扩展程序” -4. 选择本项目目录 -5. 打开扩展侧边栏 +- 如果你手动填写了与当前 `Gmail / 2925 provide` 基邮箱不匹配的完整邮箱,侧边栏会在保存或执行 Step 3 时拦截 ## 快速开始 @@ -143,12 +144,15 @@ 1. `Mail` 选择 `2925` 2. 在 `2925 账号池` 中添加 `邮箱 / 密码` -3. 在“2925 基邮箱”右侧打开 `号池` 开关后,基邮箱输入框会切成下拉框,只能从 2925 账号池中选择邮箱;关闭开关则回退到原来的手填基邮箱路线 -4. 可先点 `使用此账号` 让当前别名基邮箱切到这条 2925 账号,再点 `登录` 手动验证网页邮箱登录态 -5. 只有在 `号池` 开关开启时,自动流程执行到 Step 4 / Step 8 前才会自动检查 2925 登录态;如果未登录,会先清理登录 cookie、等待 `3 秒`,再打开登录页,并在页面打开后再等待 `3 秒`,然后使用当前账号自动登录;点击登录后若 `20 秒`内仍未进入收件箱,则会判定当前登录失败 -6. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间;如果还有下一个可用账号,就禁用当前账号 24 小时并自动切换登录;如果没有下一个可用账号,或当前未启用号池模式,则会直接复用现有“手动暂停 / 停止”逻辑终止自动流程 -7. 如果你同时开启了 `Auto` 的自动重试,当前尝试结束后会按现有逻辑自动进入下一次尝试,不需要再手动介入 -8. `Mail = 2925` 仍然走 Gmail / 2925 共用的别名邮箱链路;实际注册邮箱会基于当前 2925 账号邮箱生成,例如 `name@2925.com -> name123456@2925.com` +3. 先根据你的用途选择 `2925 模式` + - `提供邮箱`:注册邮箱本身就是 2925 别名,会显示“别名基邮箱”输入 + - `接收邮箱`:注册邮箱回退到普通“邮箱生成 / 手动填写”路线,2925 只负责收信 +4. `2925 号池` 现在是独立配置行;开启 `号池` 后可从下拉框中选择当前 2925 账号。若当前处于 `提供邮箱` 模式,这个账号也会同步作为别名基邮箱 +5. 可先点 `使用此账号` 让当前 2925 账号切到这条记录,再点 `登录` 手动验证网页邮箱登录态 +6. 只有在 `号池` 开关开启时,自动流程执行到 Step 4 / Step 8 前才会自动检查 2925 登录态;如果未登录,会先清理登录 cookie、等待 `3 秒`,再打开登录页,并在页面打开后再等待 `3 秒`,然后使用当前账号自动登录;填写完账号密码后会额外等待 `1 秒` 再点击登录,点击后若 `40 秒`内仍未进入收件箱,则会判定当前登录失败 +7. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间;如果还有下一个可用账号,就禁用当前账号 24 小时并自动切换登录;如果没有下一个可用账号,或当前未启用号池模式,则会直接复用现有“手动暂停 / 停止”逻辑终止自动流程 +8. 如果你同时开启了 `Auto` 的自动重试,当前尝试结束后会按现有逻辑自动进入下一次尝试,不需要再手动介入 +9. 只有 `Mail = 2925` 且模式为 `提供邮箱` 时,才会继续走 Gmail / 2925 共用的别名邮箱链路;例如 `name@2925.com -> name123456@2925.com` ## 侧边栏配置说明 @@ -579,9 +583,11 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 - 已刷新到最新 OAuth 链接 - 认证页已经真正进入登录验证码页面 +- 在真正把 Step 7 记为完成前,还会再做一轮收尾确认;如果页面只是短暂进入登录验证码页、随后又掉进登录重试页,则不会直接进入 Step 8,而是先按共享恢复逻辑处理并重跑 Step 7 - 如遇登录超时报错,会先尝试通过共享恢复逻辑最多自动点击 5 次认证页上的 `重试` 恢复当前页面;若仍未恢复,再按既有逻辑重跑整个 Step 7 - 如遇登录页长时间停滞,会由后台刷新 OAuth 后重跑整个 Step 7 - 如果重试页内容中出现 `max_check_attempts`,会立刻完全停止流程,并在侧边栏复用现有确认弹窗提示这是 Cloudflare 风控拦截,确认按钮显示为“我知道了” +- Step 8 不负责替代 Step 7 的收尾确认;它默认消费的是“已经由 Step 7 确认稳定进入”的登录验证码页,只在后台入口做防御性状态兜底 支持: @@ -598,7 +604,7 @@ Step 8 默认要求当前认证页已经处于登录验证码页。 - 打开邮箱并轮询登录验证码 - 填写并提交登录验证码 - 验证码链路失败后按有限次数回退到 Step 7 -- 如果进入登录超时报错/重试页,会直接报错并回到 Step 7,不会在 Step 8 内部点击 `重试` +- 如果进入登录超时报错/重试页,包括 `/email-verification` 上的 `405 / Route Error` 登录重试页,会直接报错并回到 Step 7,不会在 Step 8 内部点击 `重试` - 如果重试页内容中出现 `max_check_attempts`,会直接完全停止整个流程,并复用现有确认弹窗提醒先等待 15 到 30 分钟或更换浏览器,确认按钮显示为“我知道了” 与 Step 4 类似,但会使用稍微不同的关键词组合去找登录验证码邮件。 @@ -617,7 +623,7 @@ Step 8 默认要求当前认证页已经处于登录验证码页。 - 等待按钮可点击 - 获取按钮坐标 - 通过 Chrome `debugger` 的输入事件点击该按钮 -- 点击后会持续检查页面是否真正离开当前状态;如果出现认证页 `重试` 页面,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再重新执行当前轮的“继续”点击 +- 点击后会持续检查页面是否真正离开当前状态;如果点击后出现认证页 `重试` 页面,则直接报错,不会在 Step 9 内部点击 `重试` - 同时监听 `chrome.webNavigation.onBeforeNavigate` - 一旦捕获本地回调地址,就把结果保存到 `Callback` @@ -633,9 +639,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 43b89dc..8c8eb1c 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'; @@ -268,6 +269,7 @@ const PERSISTED_SETTING_DEFAULTS = { accountRunHistoryHelperBaseUrl: DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL, gmailBaseEmail: '', mail2925BaseEmail: '', + currentMail2925AccountId: '', emailPrefix: '', inbucketHost: '', inbucketMailbox: '', @@ -450,7 +452,7 @@ function normalizeRunCount(value) { if (!Number.isFinite(numeric)) { return 1; } - return Math.min(50, Math.max(1, Math.floor(numeric))); + return Math.max(1, Math.floor(numeric)); } function normalizeAutoRunTimerKind(value = '') { @@ -902,6 +904,7 @@ function normalizePersistentSettingValue(key, value) { return normalizeAccountRunHistoryHelperBaseUrl(value); case 'gmailBaseEmail': case 'mail2925BaseEmail': + case 'currentMail2925AccountId': case 'emailPrefix': return String(value || '').trim(); case 'inbucketHost': @@ -2132,11 +2135,21 @@ function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) { const provider = typeof stateOrProvider === 'string' ? stateOrProvider : stateOrProvider?.mailProvider; + const resolvedMail2925Mode = mail2925Mode !== undefined + ? normalizeMail2925Mode(mail2925Mode) + : getMail2925Mode(stateOrProvider); const utils = (typeof self !== 'undefined' ? self : globalThis).MultiPageManagedAliasUtils || null; + if (utils?.usesManagedAliasGeneration) { + return utils.usesManagedAliasGeneration(provider, { mail2925Mode: resolvedMail2925Mode }); + } if (utils?.isManagedAliasProvider) { + if (String(provider || '').trim().toLowerCase() === '2925') { + return utils.isManagedAliasProvider(provider) && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE; + } return utils.isManagedAliasProvider(provider); } - return provider === GMAIL_PROVIDER || provider === '2925'; + return provider === GMAIL_PROVIDER + || (provider === '2925' && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE); } function shouldUseCustomRegistrationEmail(state = {}) { @@ -2279,11 +2292,21 @@ function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) { const provider = typeof stateOrProvider === 'string' ? stateOrProvider : stateOrProvider?.mailProvider; + const resolvedMail2925Mode = mail2925Mode !== undefined + ? normalizeMail2925Mode(mail2925Mode) + : getMail2925Mode(stateOrProvider); const utils = getManagedAliasUtils(); + if (utils?.usesManagedAliasGeneration) { + return utils.usesManagedAliasGeneration(provider, { mail2925Mode: resolvedMail2925Mode }); + } if (utils?.isManagedAliasProvider) { + if (String(provider || '').trim().toLowerCase() === '2925') { + return utils.isManagedAliasProvider(provider) && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE; + } return utils.isManagedAliasProvider(provider); } - return provider === GMAIL_PROVIDER || provider === '2925'; + return provider === GMAIL_PROVIDER + || (provider === '2925' && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE); } function shouldUseCustomRegistrationEmail(state = {}) { @@ -3980,6 +4003,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', @@ -4003,6 +4037,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); @@ -5306,6 +5347,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'); @@ -6564,6 +6609,7 @@ function getMailConfig(state) { } if (provider === '2925') { return { + provider: '2925', source: 'mail-2925', url: 'https://2925.com/#/mailList', label: '2925 邮箱', @@ -6815,7 +6861,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; } @@ -7150,7 +7196,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(); @@ -7162,20 +7207,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) { @@ -7340,18 +7374,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) { @@ -7385,8 +7408,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 be98af5..3d924a7 100644 --- a/background/generated-email-helpers.js +++ b/background/generated-email-helpers.js @@ -195,6 +195,9 @@ ...(state || {}), mailProvider: provider, }; + if (options.mail2925Mode !== undefined) { + mergedState.mail2925Mode = String(options.mail2925Mode || '').trim(); + } if (options.gmailBaseEmail !== undefined) { mergedState.gmailBaseEmail = String(options.gmailBaseEmail || '').trim(); } @@ -227,7 +230,10 @@ async function fetchGeneratedEmail(state, options = {}) { const currentState = state || await getState(); const provider = String(options.mailProvider || currentState.mailProvider || '').trim().toLowerCase(); - if (isGeneratedAliasProvider?.(provider)) { + const mail2925Mode = options.mail2925Mode !== undefined + ? options.mail2925Mode + : currentState.mail2925Mode; + if (isGeneratedAliasProvider?.(provider, mail2925Mode)) { return fetchManagedAliasEmail(currentState, options); } const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator); diff --git a/background/mail-2925-session.js b/background/mail-2925-session.js index 4649231..410fa97 100644 --- a/background/mail-2925-session.js +++ b/background/mail-2925-session.js @@ -6,16 +6,16 @@ addLog, broadcastDataUpdate, chrome, + ensureContentScriptReadyOnTab, findMail2925Account, getMail2925AccountStatus, + getState, + isAutoRunLockedState, isMail2925AccountAvailable, MAIL2925_LIMIT_COOLDOWN_MS, normalizeMail2925Account, normalizeMail2925Accounts, pickMail2925AccountForRun, - getState, - isAutoRunLockedState, - ensureContentScriptReadyOnTab, requestStop, reuseOrCreateTab, sendToContentScriptResilient, @@ -87,7 +87,8 @@ function isMail2925LimitReachedError(error) { const message = getErrorMessage(error); return message.startsWith(MAIL2925_LIMIT_ERROR_PREFIX) - || /子邮箱.{0,12}已达上限|已达上限邮箱|子邮箱上限|邮箱已达上限/i.test(message); + || message.includes('子邮箱已达上限') + || message.includes('已达上限邮箱'); } function isMail2925ThreadTerminatedError(error) { @@ -135,7 +136,7 @@ try { const state = await getState(); const tabId = Number(state?.tabRegistry?.[MAIL2925_SOURCE]?.tabId || 0); - if (!Number.isInteger(tabId) || tabId <= 0) { + if (!Number.isInteger(tabId) || tabId <= 0 || typeof chrome.tabs?.get !== 'function') { return ''; } const tab = await chrome.tabs.get(tabId); @@ -145,6 +146,32 @@ } } + async function getMail2925TabUrlById(tabId) { + try { + if (!Number.isInteger(Number(tabId)) || Number(tabId) <= 0 || typeof chrome.tabs?.get !== 'function') { + return ''; + } + const tab = await chrome.tabs.get(Number(tabId)); + return String(tab?.url || '').trim(); + } catch { + return ''; + } + } + + function isMail2925LoginUrl(rawUrl = '') { + try { + const parsed = new URL(String(rawUrl || '')); + return (parsed.hostname === '2925.com' || parsed.hostname === 'www.2925.com') + && /^\/login\/?$/.test(parsed.pathname); + } catch { + return false; + } + } + + function normalizeMailboxEmail(value = '') { + return String(value || '').trim().toLowerCase(); + } + async function setCurrentMail2925Account(accountId, options = {}) { const { logMessage = '', updateLastUsedAt = false } = options; const state = await getState(); @@ -163,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) { @@ -187,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 }); } @@ -201,6 +230,7 @@ await syncMail2925Accounts(nextAccounts); if (state.currentMail2925AccountId === accountId) { + await setPersistentSettings({ currentMail2925AccountId: '' }); await setState({ currentMail2925AccountId: null }); broadcastDataUpdate({ currentMail2925AccountId: null }); } @@ -216,6 +246,7 @@ await syncMail2925Accounts(nextAccounts); if (state.currentMail2925AccountId && !findMail2925Account(nextAccounts, state.currentMail2925AccountId)) { + await setPersistentSettings({ currentMail2925AccountId: '' }); await setState({ currentMail2925AccountId: null }); broadcastDataUpdate({ currentMail2925AccountId: null }); } @@ -361,7 +392,7 @@ origins: MAIL2925_COOKIE_ORIGINS, }); } catch (_) { - // Best-effort cleanup only. + // Best effort cleanup only. } } @@ -373,170 +404,18 @@ accountId = null, forceRelogin = false, actionLabel = '确保 2925 邮箱登录态', + allowLoginWhenOnLoginPage = true, + expectedMailboxEmail = '', } = options; - const account = await ensureMail2925AccountForFlow({ - allowAllocate: true, - preferredAccountId: accountId, - }); - if (forceRelogin) { - const removedCount = await clearMail2925SessionCookies(); - await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info'); - } + const normalizedExpectedMailboxEmail = normalizeMailboxEmail(expectedMailboxEmail); - if (forceRelogin && typeof sleepWithStop === 'function') { - await sleepWithStop(3000); - } - - throwIfStopped(); - const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL; - const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, { - inject: MAIL2925_INJECT, - injectSource: MAIL2925_INJECT_SOURCE, - }); - if (forceRelogin && typeof sleepWithStop === 'function') { - await sleepWithStop(3000); - } - - let result; - try { - result = await sendToMailContentScriptResilient( - getMail2925MailConfig(), - { - type: 'ENSURE_MAIL2925_SESSION', - step: 0, - source: 'background', - payload: { - email: account.email, - password: account.password, - forceLogin: forceRelogin, - }, - }, - { - timeoutMs: forceRelogin ? 30000 : 25000, - responseTimeoutMs: forceRelogin ? 30000 : 25000, - maxRecoveryAttempts: 2, - } - ); - } catch (err) { - const failedUrl = await getMail2925CurrentTabUrl(); - await addLog(`2925:ENSURE_MAIL2925_SESSION 通信失败,当前地址=${failedUrl || 'unknown'};原因=${getErrorMessage(err) || 'unknown'}`, 'warn'); - const stopped = await stopAutoRunForMail2925LoginFailure( - `2925:${actionLabel}失败(${getErrorMessage(err) || '20 秒内未进入收件箱'}),已按手动停止逻辑暂停自动流程。` - ); - if (stopped) { - throw new Error('流程已被用户停止。'); - } - throw err; - } - - if (!result?.loggedIn) { - const notLoggedInUrl = await getMail2925CurrentTabUrl(); - await addLog(`2925:20 秒登录等待结束但仍未进入收件箱,当前地址=${notLoggedInUrl || 'unknown'}`, 'warn'); - const stopped = await stopAutoRunForMail2925LoginFailure( - `2925:${actionLabel}失败(20 秒内未进入收件箱),已按手动停止逻辑暂停自动流程。` - ); - if (stopped) { - throw new Error('流程已被用户停止。'); - } - throw new Error(`2925:${actionLabel}失败,当前页面仍未进入收件箱。`); - } - - if (result?.error) { - const resultErrorUrl = await getMail2925CurrentTabUrl(); - await addLog(`2925:登录页返回业务错误,当前地址=${resultErrorUrl || 'unknown'};错误=${result.error}`, 'warn'); - const stopped = await stopAutoRunForMail2925LoginFailure( - `2925:${actionLabel}失败(${result.error}),已按手动停止逻辑暂停自动流程。` - ); - if (stopped) { - throw new Error('流程已被用户停止。'); - } - throw new Error(result.error); - } - if (result?.limitReached) { - throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '2925 子邮箱已达上限邮箱'}`); - } - if (!result?.loggedIn) { - throw new Error(`2925:${actionLabel}失败,当前页面仍未进入收件箱。`); - } - - await patchMail2925Account(account.id, { - lastLoginAt: Date.now(), - lastError: '', - }); - await setState({ currentMail2925AccountId: account.id }); - broadcastDataUpdate({ currentMail2925AccountId: account.id }); - - return { - account: await ensureMail2925AccountForFlow({ - allowAllocate: false, - preferredAccountId: account.id, - }), - mail: getMail2925MailConfig(), - result, - }; - } - - // Override the earlier version with a simpler login-page-only flow. - async function ensureMail2925MailboxSession(options = {}) { - const { - accountId = null, - forceRelogin = false, - actionLabel = '确保 2925 邮箱登录态', - } = options; - const account = await ensureMail2925AccountForFlow({ - allowAllocate: true, - preferredAccountId: accountId, - }); - - if (forceRelogin) { - const removedCount = await clearMail2925SessionCookies(); - await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info'); - if (typeof sleepWithStop === 'function') { - await addLog('2925:清理 cookie 后等待 3 秒,再打开登录页...', 'info'); - await sleepWithStop(3000); - } - } - - throwIfStopped(); - await addLog(`2925:准备打开登录页 ${MAIL2925_LOGIN_URL}(forceRelogin=${forceRelogin ? 'true' : 'false'})`, 'info'); - const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL; - const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, { - inject: MAIL2925_INJECT, - injectSource: MAIL2925_INJECT_SOURCE, - }); - const openedUrl = await getMail2925CurrentTabUrl(); - await addLog(`2925:打开页后当前标签地址:${openedUrl || 'unknown'}`, 'info'); - - if (forceRelogin && typeof waitForTabUrlMatch === 'function') { - const matchedLoginTab = await waitForTabUrlMatch( - tabId, - (url) => { - try { - const parsed = new URL(String(url || '')); - return (parsed.hostname === '2925.com' || parsed.hostname === 'www.2925.com') - && /^\/login\/?$/.test(parsed.pathname); - } catch { - return false; - } - }, - { timeoutMs: 15000, retryDelayMs: 300 } - ); - await addLog(`2925:等待最终落到登录页结果:${matchedLoginTab?.url || 'timeout'}`, matchedLoginTab ? 'info' : 'warn'); - if (matchedLoginTab && typeof ensureContentScriptReadyOnTab === 'function') { - await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, tabId, { - inject: MAIL2925_INJECT, - injectSource: MAIL2925_INJECT_SOURCE, - timeoutMs: 20000, - retryDelayMs: 800, - logMessage: '步骤 0:2925 登录页内容脚本未就绪,正在等待页面稳定后继续登录...', - }); - } - } - - if (forceRelogin && typeof sleepWithStop === 'function') { - await addLog('2925:登录页已打开,等待 3 秒后开始检查输入框并执行登录...', 'info'); - await sleepWithStop(3000); + let account = null; + if (forceRelogin || (allowLoginWhenOnLoginPage && normalizedExpectedMailboxEmail)) { + account = await ensureMail2925AccountForFlow({ + allowAllocate: true, + preferredAccountId: accountId, + }); } const sendLoginMessage = typeof sendToContentScriptResilient === 'function' @@ -551,9 +430,98 @@ } ); + const buildSuccessPayload = () => ({ + account, + mail: getMail2925MailConfig(), + result: { + loggedIn: true, + currentView: 'mailbox', + usedExistingSession: true, + }, + }); + + const failMailboxSession = async (message) => { + const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`); + if (stopped) { + throw new Error('流程已被用户停止。'); + } + throw new Error(message); + }; + + if (forceRelogin) { + const removedCount = await clearMail2925SessionCookies(); + await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info'); + if (typeof sleepWithStop === 'function') { + await addLog('2925:清理 cookie 后等待 3 秒,再打开登录页...', 'info'); + await sleepWithStop(3000); + } + } + + throwIfStopped(); + const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL; + await addLog( + forceRelogin + ? `2925:准备打开登录页 ${MAIL2925_LOGIN_URL}(强制重登录)` + : `2925:准备打开邮箱页 ${MAIL2925_URL}(登录页自动登录=${allowLoginWhenOnLoginPage ? '开启' : '关闭'})`, + 'info' + ); + const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, { + inject: MAIL2925_INJECT, + injectSource: MAIL2925_INJECT_SOURCE, + }); + + let openedUrl = await getMail2925TabUrlById(tabId); + if (!openedUrl) { + openedUrl = await getMail2925CurrentTabUrl(); + } + await addLog(`2925:打开页后当前标签地址:${openedUrl || 'unknown'}`, 'info'); + + if (forceRelogin && typeof waitForTabUrlMatch === 'function') { + const matchedLoginTab = await waitForTabUrlMatch( + tabId, + (url) => isMail2925LoginUrl(url), + { timeoutMs: 15000, retryDelayMs: 300 } + ); + await addLog(`2925:等待最终落到登录页结果:${matchedLoginTab?.url || '超时'}`, matchedLoginTab ? 'info' : 'warn'); + if (matchedLoginTab?.url) { + openedUrl = String(matchedLoginTab.url || '').trim(); + } + } + + if (!forceRelogin && !isMail2925LoginUrl(openedUrl) && !normalizedExpectedMailboxEmail) { + await addLog('2925:当前邮箱页未跳转到登录页,将直接复用已登录会话。', 'info'); + return buildSuccessPayload(); + } + + if (!forceRelogin && isMail2925LoginUrl(openedUrl) && !allowLoginWhenOnLoginPage) { + await failMailboxSession(`2925:${actionLabel}失败,当前页面已跳转到登录页,且当前未启用 2925 账号池,不执行自动登录。`); + } + + if (!account && (forceRelogin || allowLoginWhenOnLoginPage)) { + account = await ensureMail2925AccountForFlow({ + allowAllocate: true, + preferredAccountId: accountId, + }); + } + + if (typeof ensureContentScriptReadyOnTab === 'function') { + await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, tabId, { + inject: MAIL2925_INJECT, + injectSource: MAIL2925_INJECT_SOURCE, + timeoutMs: 20000, + retryDelayMs: 800, + logMessage: '步骤 0:2925 登录页内容脚本未就绪,正在等待页面稳定后继续登录...', + }); + } + + if (forceRelogin && typeof sleepWithStop === 'function') { + await addLog('2925:登录页已打开,等待 3 秒后开始检查输入框并执行登录...', 'info'); + await sleepWithStop(3000); + } + let result; try { - const beforeSendUrl = await getMail2925CurrentTabUrl(); + const beforeSendUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl(); await addLog(`2925:发送 ENSURE_MAIL2925_SESSION 前当前地址:${beforeSendUrl || 'unknown'}`, 'info'); result = await sendLoginMessage( MAIL2925_SOURCE, @@ -562,22 +530,22 @@ step: 0, source: 'background', payload: { - email: account.email, - password: account.password, + email: account?.email || '', + password: account?.password || '', forceLogin: forceRelogin, + allowLoginWhenOnLoginPage, }, }, { - timeoutMs: forceRelogin ? 30000 : 25000, + timeoutMs: 50000, retryDelayMs: 800, - responseTimeoutMs: forceRelogin ? 30000 : 25000, - logMessage: `步骤 0:2925 登录页通信异常,正在等待当前页面重新就绪后继续确认登录态...`, + responseTimeoutMs: 50000, + logMessage: '步骤 0:2925 登录页通信异常,正在等待页面恢复...', } ); } catch (err) { - const stopped = await stopAutoRunForMail2925LoginFailure( - `2925:${actionLabel}失败(${getErrorMessage(err) || '20 秒内未进入收件箱'}),已按手动停止逻辑暂停自动流程。` - ); + const message = `2925:${actionLabel}失败(${getErrorMessage(err) || '40 秒内未进入收件箱'})。`; + const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`); if (stopped) { throw new Error('流程已被用户停止。'); } @@ -585,27 +553,35 @@ } if (result?.error) { - const stopped = await stopAutoRunForMail2925LoginFailure( - `2925:${actionLabel}失败(${result.error}),已按手动停止逻辑暂停自动流程。` - ); - if (stopped) { - throw new Error('流程已被用户停止。'); - } - throw new Error(result.error); + await failMailboxSession(`2925:${actionLabel}失败(${result.error})。`); } - if (result?.limitReached) { - throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '2925 子邮箱已达上限邮箱'}`); + throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '子邮箱已达上限邮箱'}`); } - - if (!result?.loggedIn) { - const stopped = await stopAutoRunForMail2925LoginFailure( - `2925:${actionLabel}失败(20 秒内未进入收件箱),已按手动停止逻辑暂停自动流程。` - ); - if (stopped) { - throw new Error('流程已被用户停止。'); + 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, + }); } - throw new Error(`2925:${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}失败,登录后仍未进入收件箱。`); } await patchMail2925Account(account.id, { @@ -614,7 +590,8 @@ }); await setState({ currentMail2925AccountId: account.id }); broadcastDataUpdate({ currentMail2925AccountId: account.id }); - const finalUrl = await getMail2925CurrentTabUrl(); + + const finalUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl(); await addLog(`2925:登录态确认成功,当前地址=${finalUrl || 'unknown'}`, 'ok'); return { @@ -632,10 +609,21 @@ || '子邮箱已达上限邮箱'; const state = await getState(); const currentAccount = getCurrentMail2925Account(state); + const poolEnabled = Boolean(state?.mail2925UseAccountPool); + + if (!poolEnabled) { + if (typeof requestStop === 'function') { + await requestStop({ + logMessage: `步骤 ${step}:2925 检测到“${reason}”,当前未启用账号池,已按手动停止逻辑暂停自动流程。`, + }); + } + return new Error('流程已被用户停止。'); + } + if (!currentAccount) { if (typeof requestStop === 'function') { await requestStop({ - logMessage: `步骤 ${step}:2925 检测到“${reason}”,且当前没有可识别账号,已按手动停止逻辑暂停流程。`, + logMessage: `步骤 ${step}:2925 检测到“${reason}”,但当前没有可识别的账号可供切换。`, }); } return new Error('流程已被用户停止。'); @@ -648,7 +636,7 @@ lastError: reason, }); await addLog( - `步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,已禁用 24 小时,恢复时间 ${new Date(disabledUntil).toLocaleString('zh-CN', { hour12: false })}。`, + `步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,已禁用到 ${new Date(disabledUntil).toLocaleString('zh-CN', { hour12: false })}。`, 'warn' ); @@ -659,11 +647,12 @@ }); if (!nextAccount) { + await setPersistentSettings({ currentMail2925AccountId: '' }); await setState({ currentMail2925AccountId: null }); broadcastDataUpdate({ currentMail2925AccountId: null }); if (typeof requestStop === 'function') { await requestStop({ - logMessage: `步骤 ${step}:2925 账号 ${currentAccount.email} 已因“${reason}”禁用 24 小时,且当前没有可切换的下一个账号,已按手动停止逻辑暂停流程。`, + logMessage: `步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,但当前没有可切换的下一个账号。`, }); } return new Error('流程已被用户停止。'); @@ -673,11 +662,12 @@ await ensureMail2925MailboxSession({ accountId: nextAccount.id, forceRelogin: true, + allowLoginWhenOnLoginPage: true, actionLabel: `步骤 ${step}:切换 2925 账号`, }); - await addLog(`步骤 ${step}:2925 已自动切换到下一个账号 ${nextAccount.email} 并完成登录,当前尝试将直接结束。`, 'warn'); + await addLog(`步骤 ${step}:2925 已切换到下一个账号 ${nextAccount.email}。`, 'warn'); return buildMail2925ThreadTerminatedError( - `步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”并已禁用 24 小时,已切换到 ${nextAccount.email},当前尝试结束,等待自动重试进入下一次尝试。` + `步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,已切换到 ${nextAccount.email},当前尝试结束,等待下一轮重试。` ); } 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-login-code.js b/background/steps/fetch-login-code.js index 9e16ab6..8bf53c4 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -9,7 +9,6 @@ chrome, CLOUDFLARE_TEMP_EMAIL_PROVIDER, confirmCustomVerificationStepBypass, - ensureMail2925MailboxSession, ensureStep8VerificationPageReady, getOAuthFlowRemainingMs, getOAuthFlowStepTimeoutMs, @@ -58,6 +57,28 @@ return String(value || '').trim().toLowerCase(); } + async function focusOrOpenMailTab(mail) { + const alive = await isTabAlive(mail.source); + if (alive) { + if (mail.navigateOnReuse) { + await reuseOrCreateTab(mail.source, mail.url, { + inject: mail.inject, + injectSource: mail.injectSource, + }); + return; + } + + const tabId = await getTabId(mail.source); + await chrome.tabs.update(tabId, { active: true }); + return; + } + + await reuseOrCreateTab(mail.source, mail.url, { + inject: mail.inject, + injectSource: mail.injectSource, + }); + } + async function runStep8Attempt(state) { const mail = getMailConfig(state); if (mail.error) throw new Error(mail.error); @@ -111,33 +132,11 @@ || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER ) { await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`); - } else if (mail.provider === '2925') { - if (state?.mail2925UseAccountPool && typeof ensureMail2925MailboxSession === 'function') { - await ensureMail2925MailboxSession({ - accountId: state.currentMail2925AccountId || null, - actionLabel: '步骤 8:确认 2925 邮箱登录态', - }); - } - await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`); } else { await addLog(`步骤 8:正在打开${mail.label}...`); - - const alive = await isTabAlive(mail.source); - if (alive) { - if (mail.navigateOnReuse) { - await reuseOrCreateTab(mail.source, mail.url, { - inject: mail.inject, - injectSource: mail.injectSource, - }); - } else { - const tabId = await getTabId(mail.source); - await chrome.tabs.update(tabId, { active: true }); - } - } else { - await reuseOrCreateTab(mail.source, mail.url, { - inject: mail.inject, - injectSource: mail.injectSource, - }); + await focusOrOpenMailTab(mail); + if (mail.provider === '2925') { + await addLog(`步骤 8:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info'); } } diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index e85b250..066baee 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -24,15 +24,53 @@ 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) { + if (mail.navigateOnReuse) { + await reuseOrCreateTab(mail.source, mail.url, { + inject: mail.inject, + injectSource: mail.injectSource, + }); + return; + } + + const tabId = await getTabId(mail.source); + await chrome.tabs.update(tabId, { active: true }); + return; + } + + await reuseOrCreateTab(mail.source, mail.url, { + inject: mail.inject, + injectSource: mail.injectSource, + }); + } + async function executeStep4(state) { const mail = getMailConfig(state); if (mail.error) throw new Error(mail.error); + const stepStartedAt = Date.now(); const verificationFilterAfterTimestamp = mail.provider === '2925' ? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS) : stepStartedAt; const verificationSessionKey = `4:${stepStartedAt}`; const signupTabId = await getTabId('signup-page'); + if (!signupTabId) { throw new Error('认证页面标签页已关闭,无法继续步骤 4。'); } @@ -40,6 +78,7 @@ await chrome.tabs.update(signupTabId, { active: true }); throwIfStopped(); await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...'); + const prepareResult = await sendToContentScriptResilient( 'signup-page', { @@ -73,43 +112,41 @@ } throwIfStopped(); - if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { + if ( + mail.provider === HOTMAIL_PROVIDER + || mail.provider === LUCKMAIL_PROVIDER + || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER + ) { await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); } else if (mail.provider === '2925') { - if (state?.mail2925UseAccountPool && typeof ensureMail2925MailboxSession === 'function') { + await addLog(`步骤 4:正在打开${mail.label}...`); + if (typeof ensureMail2925MailboxSession === 'function') { await ensureMail2925MailboxSession({ accountId: state.currentMail2925AccountId || null, + forceRelogin: false, + allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool), + expectedMailboxEmail: getExpectedMail2925MailboxEmail(state), actionLabel: '步骤 4:确认 2925 邮箱登录态', }); + } else { + await focusOrOpenMailTab(mail); } - await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); + await addLog(`步骤 4:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info'); } else { await addLog(`步骤 4:正在打开${mail.label}...`); - - const alive = await isTabAlive(mail.source); - if (alive) { - if (mail.navigateOnReuse) { - await reuseOrCreateTab(mail.source, mail.url, { - inject: mail.inject, - injectSource: mail.injectSource, - }); - } else { - const tabId = await getTabId(mail.source); - await chrome.tabs.update(tabId, { active: true }); - } - } else { - await reuseOrCreateTab(mail.source, mail.url, { - inject: mail.inject, - injectSource: mail.injectSource, - }); - } + await focusOrOpenMailTab(mail); } + const shouldRequestFreshCodeFirst = ![ + HOTMAIL_PROVIDER, + CLOUDFLARE_TEMP_EMAIL_PROVIDER, + ].includes(mail.provider); + await resolveVerificationStep(4, state, mail, { filterAfterTimestamp: verificationFilterAfterTimestamp, sessionKey: verificationSessionKey, disableTimeBudgetCap: mail.provider === '2925', - requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true, + requestFreshCodeFirst: shouldRequestFreshCodeFirst, resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925') ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, diff --git a/background/steps/platform-verify.js b/background/steps/platform-verify.js index 7edfe5d..97bc10d 100644 --- a/background/steps/platform-verify.js +++ b/background/steps/platform-verify.js @@ -79,7 +79,8 @@ source: 'background', payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword }, }, { - timeoutMs: 30000, + timeoutMs: 125000, + responseTimeoutMs: 125000, retryDelayMs: 700, logMessage: '步骤 10:CPA 面板通信未就绪,正在等待页面恢复...', }); diff --git a/background/verification-flow.js b/background/verification-flow.js index fc4f208..eba621d 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -113,12 +113,15 @@ function getVerificationPollPayload(step, state, overrides = {}) { const is2925Provider = state?.mailProvider === '2925'; + const mail2925MatchTargetEmail = is2925Provider + && String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive'; if (step === 4) { return { filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(4, state), senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'], subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'], targetEmail: state.email, + mail2925MatchTargetEmail, maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5, intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000, ...overrides, @@ -130,6 +133,7 @@ senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'], subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'], targetEmail: String(state?.step8VerificationTargetEmail || '').trim() || state.email, + mail2925MatchTargetEmail, maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5, intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000, ...overrides, 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 54970d2..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 @@ -629,6 +686,39 @@ function extractVerificationCode(text, strictChatGPTCodeOnly = false) { return null; } +function extractEmails(text = '') { + const matches = String(text || '').match(/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/gi) || []; + return [...new Set(matches.map((item) => item.toLowerCase()))]; +} + +function emailMatchesTarget(candidate, targetEmail) { + const normalizedCandidate = String(candidate || '').trim().toLowerCase(); + const normalizedTarget = String(targetEmail || '').trim().toLowerCase(); + return Boolean(normalizedCandidate && normalizedTarget && normalizedCandidate === normalizedTarget); +} + +function getTargetEmailMatchState(text, targetEmail) { + const normalizedTarget = String(targetEmail || '').trim().toLowerCase(); + if (!normalizedTarget) { + return { matches: true, hasExplicitEmail: false }; + } + + const normalizedText = String(text || '').toLowerCase(); + if (normalizedText.includes(normalizedTarget)) { + return { matches: true, hasExplicitEmail: true }; + } + + const extractedEmails = extractEmails(normalizedText); + if (!extractedEmails.length) { + return { matches: true, hasExplicitEmail: false }; + } + + return { + matches: extractedEmails.some((candidate) => emailMatchesTarget(candidate, normalizedTarget)), + hasExplicitEmail: true, + }; +} + function normalizeMinuteTimestamp(timestamp) { if (!Number.isFinite(timestamp) || timestamp <= 0) return 0; const date = new Date(timestamp); @@ -840,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) { @@ -860,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); @@ -875,6 +976,7 @@ async function ensureMail2925Session(payload = {}) { ok: true, loggedIn: true, currentView: 'mailbox', + mailboxEmail: loginState.mailboxEmail || '', }; } if (loginState.view === 'limit') { @@ -886,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(); @@ -902,11 +1013,12 @@ async function ensureMail2925Session(payload = {}) { await sleep(150); fillInput(passwordInput, password); await sleep(200); + await sleep(1000); log(`步骤 0:2925 已定位到登录表单,准备点击“登录”,当前地址 ${location.href}`, 'info'); simulateClick(loginButton); log(`步骤 0:2925 已点击“登录”,点击后地址 ${location.href}`, 'info'); - const finalState = await waitForMail2925View('mailbox', 20000); + const finalState = await waitForMail2925View('mailbox', 40000); log(`步骤 0:2925 登录等待结束,状态=${finalState.view},地址=${location.href}`, 'info'); if (finalState.view !== 'mailbox') { throw new Error('2925:提交账号密码后未进入收件箱。'); @@ -917,6 +1029,7 @@ async function ensureMail2925Session(payload = {}) { loggedIn: true, currentView: 'mailbox', usedCredentials: true, + mailboxEmail: finalState.mailboxEmail || getMail2925DisplayedMailboxEmail() || '', }; } @@ -930,6 +1043,8 @@ async function handlePollEmail(step, payload) { filterAfterTimestamp = 0, excludeCodes = [], strictChatGPTCodeOnly = false, + targetEmail = '', + mail2925MatchTargetEmail = false, } = payload || {}; const excludedCodeSet = new Set(excludeCodes.filter(Boolean)); const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0); @@ -994,9 +1109,21 @@ async function handlePollEmail(step, payload) { if (!matchesMailFilters(previewText, senderFilters, subjectFilters)) { continue; } + const previewTargetState = mail2925MatchTargetEmail + ? getTargetEmailMatchState(previewText, targetEmail) + : { matches: true, hasExplicitEmail: false }; + if (mail2925MatchTargetEmail && previewTargetState.hasExplicitEmail && !previewTargetState.matches) { + continue; + } const previewCode = extractVerificationCode(previewText, strictChatGPTCodeOnly); const openedText = await openMailAndDeleteAfterRead(item, step); + const openedTargetState = mail2925MatchTargetEmail + ? getTargetEmailMatchState(openedText, targetEmail) + : { matches: true, hasExplicitEmail: false }; + if (mail2925MatchTargetEmail && openedTargetState.hasExplicitEmail && !openedTargetState.matches) { + continue; + } const bodyCode = extractVerificationCode(openedText, strictChatGPTCodeOnly); const candidateCode = bodyCode || previewCode; diff --git a/content/signup-page.js b/content/signup-page.js index a6d44d8..53881b7 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -114,16 +114,24 @@ function isVisibleElement(el) { && rect.height > 0; } +function getVisibleSplitVerificationInputs() { + return Array.from(document.querySelectorAll('input[maxlength="1"]')) + .filter(isVisibleElement); +} + function getVerificationCodeTarget() { + const splitInputs = getVisibleSplitVerificationInputs(); const codeInput = document.querySelector(VERIFICATION_CODE_INPUT_SELECTOR); if (codeInput && isVisibleElement(codeInput)) { + const maxLength = Number(codeInput.getAttribute?.('maxlength') || codeInput.maxLength || 0); + if (maxLength === 1 && splitInputs.length >= 6) { + return { type: 'split', elements: splitInputs }; + } return { type: 'single', element: codeInput }; } - const singleInputs = Array.from(document.querySelectorAll('input[maxlength="1"]')) - .filter(isVisibleElement); - if (singleInputs.length >= 6) { - return { type: 'split', elements: singleInputs }; + if (splitInputs.length >= 6) { + return { type: 'split', elements: splitInputs }; } return null; @@ -377,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(), @@ -391,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) @@ -409,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); @@ -417,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, @@ -612,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); } @@ -627,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; @@ -1198,7 +1411,7 @@ function getSignupPasswordTimeoutErrorPageState() { function getLoginTimeoutErrorPageState() { return getAuthTimeoutErrorPageState({ - pathPatterns: [/\/log-in(?:[/?#]|$)/i], + pathPatterns: getLoginAuthRetryPathPatterns(), }); } @@ -1263,13 +1476,6 @@ function inspectLoginAuthState() { consentReady, }; - if (verificationTarget) { - return { - ...baseState, - state: 'verification_page', - }; - } - if (retryState) { return { ...baseState, @@ -1277,6 +1483,13 @@ function inspectLoginAuthState() { }; } + if (verificationTarget) { + return { + ...baseState, + state: 'verification_page', + }; + } + if (addPhonePage) { return { ...baseState, @@ -1434,6 +1647,86 @@ async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, messag }); } +async function finalizeStep6VerificationReady(options = {}) { + const { + logLabel = '步骤 7 收尾', + loginVerificationRequestedAt = null, + timeout = 12000, + via = 'verification_page_ready', + } = options; + const start = Date.now(); + const maxRounds = 3; + const settleDelayMs = 3000; + let round = 0; + + while (Date.now() - start < timeout && round < maxRounds) { + throwIfStopped(); + round += 1; + log(`${logLabel}:确认页面是否稳定停留在登录验证码阶段(第 ${round}/${maxRounds} 轮,先等待 3 秒)...`, 'info'); + await sleep(settleDelayMs); + + const rawSnapshot = inspectLoginAuthState(); + const snapshot = normalizeStep6Snapshot(rawSnapshot); + + if (snapshot.state === 'verification_page') { + log(`${logLabel}:登录验证码页面已稳定就绪。`, 'ok'); + return createStep6SuccessResult(snapshot, { + via, + loginVerificationRequestedAt, + }); + } + + if (snapshot.state === 'login_timeout_error_page') { + log(`${logLabel}:页面进入登录超时报错页,准备自动恢复后重试步骤 7。`, 'warn'); + return createStep6LoginTimeoutRecoverableResult( + 'login_timeout_error_page', + snapshot, + '登录验证码页面准备就绪前进入登录超时报错页。' + ); + } + + if (snapshot.state === 'password_page' || snapshot.state === 'email_page') { + return createStep6RecoverableResult('verification_page_unstable', snapshot, { + message: `页面曾进入登录验证码阶段,但又回到了${getLoginAuthStateLabel(snapshot)},准备重新执行步骤 7。`, + loginVerificationRequestedAt, + }); + } + + if (snapshot.state === 'add_phone_page') { + throw new Error(`登录验证码页面准备过程中页面进入手机号页面。URL: ${snapshot.url}`); + } + } + + const rawSnapshot = inspectLoginAuthState(); + const snapshot = normalizeStep6Snapshot(rawSnapshot); + if (snapshot.state === 'verification_page') { + log(`${logLabel}:登录验证码页面已稳定就绪。`, 'ok'); + return createStep6SuccessResult(snapshot, { + via, + loginVerificationRequestedAt, + }); + } + if (snapshot.state === 'login_timeout_error_page') { + log(`${logLabel}:页面进入登录超时报错页,准备自动恢复后重试步骤 7。`, 'warn'); + return createStep6LoginTimeoutRecoverableResult( + 'login_timeout_error_page', + snapshot, + '登录验证码页面准备就绪前进入登录超时报错页。' + ); + } + if (snapshot.state === 'password_page' || snapshot.state === 'email_page') { + return createStep6RecoverableResult('verification_page_unstable', snapshot, { + message: `页面曾进入登录验证码阶段,但又回到了${getLoginAuthStateLabel(snapshot)},准备重新执行步骤 7。`, + loginVerificationRequestedAt, + }); + } + + return createStep6RecoverableResult('verification_page_finalize_unknown', snapshot, { + message: '登录验证码页面状态在收尾确认阶段未稳定,准备重新执行步骤 7。', + loginVerificationRequestedAt, + }); +} + function normalizeStep6Snapshot(snapshot) { if (snapshot?.state !== 'oauth_consent_page') { return snapshot; @@ -1552,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(); @@ -1590,6 +1884,10 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { } if (snapshot.state === 'password') { + if (!passwordPageDiagnosticsLogged) { + passwordPageDiagnosticsLogged = true; + logSignupPasswordDiagnostics(`${prepareLogLabel}:页面仍停留在密码页`); + } if (!password) { throw new Error('当前回到了密码页,但没有可用密码,无法自动重新提交。'); } @@ -1622,15 +1920,30 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { async function waitForVerificationSubmitOutcome(step, timeout) { const resolvedTimeout = timeout ?? (step === 8 ? 30000 : 12000); const start = Date.now(); + let recoveryCount = 0; + const maxRecoveryCount = 2; while (Date.now() - start < resolvedTimeout) { throwIfStopped(); - if (step === 4) { - const signupRetryState = getCurrentAuthRetryPageState('signup'); - if (signupRetryState?.userAlreadyExistsBlocked) { - throw createSignupUserAlreadyExistsError(); + const retryFlow = step === 4 ? 'signup' : 'login'; + const retryState = getCurrentAuthRetryPageState(retryFlow); + if (retryState?.userAlreadyExistsBlocked) { + throw createSignupUserAlreadyExistsError(); + } + if (retryState) { + if (recoveryCount >= maxRecoveryCount) { + throw new Error(`步骤 ${step}:验证码提交后连续进入认证重试页 ${maxRecoveryCount} 次,页面仍未恢复。URL: ${location.href}`); } + recoveryCount += 1; + log(`步骤 ${step}:验证码提交后进入认证重试页,正在自动恢复(${recoveryCount}/${maxRecoveryCount})...`, 'warn'); + await recoverCurrentAuthRetryPage({ + flow: retryFlow, + logLabel: `步骤 ${step}:验证码提交后检测到认证重试页,正在点击“重试”恢复`, + step, + timeoutMs: 12000, + }); + continue; } const errorText = getVerificationErrorText(); @@ -1670,6 +1983,96 @@ async function waitForVerificationSubmitOutcome(step, timeout) { return { success: true, assumed: true }; } +function getVerificationSubmitButtonForTarget(codeInput, options = {}) { + const { allowDisabled = false } = options; + const form = codeInput?.form || codeInput?.closest?.('form') || null; + const isUsableAction = (element) => { + if (!element || !isVisibleElement(element)) return false; + return allowDisabled || isActionEnabled(element); + }; + + const findSubmitInRoot = (root) => { + if (!root?.querySelectorAll) return null; + + const directCandidates = root.querySelectorAll('button[type="submit"], input[type="submit"]'); + for (const element of directCandidates) { + if (isUsableAction(element)) { + return element; + } + } + + const textCandidates = root.querySelectorAll('button, [role="button"], input[type="button"], input[type="submit"]'); + return Array.from(textCandidates).find((element) => { + if (!isUsableAction(element)) return false; + const text = getActionText(element); + return /verify|confirm|submit|continue|确认|验证|继续/i.test(text); + }) || null; + }; + + return findSubmitInRoot(form) || findSubmitInRoot(document); +} + +async function waitForVerificationSubmitButton(codeInput, timeout = 5000) { + const start = Date.now(); + + while (Date.now() - start < timeout) { + throwIfStopped(); + if (is405MethodNotAllowedPage()) { + throw new Error('当前页面处于 405 错误恢复流程中,暂时无法定位验证码提交按钮。'); + } + + const button = getVerificationSubmitButtonForTarget(codeInput, { allowDisabled: false }); + if (button) { + return button; + } + + await sleep(150); + } + + return null; +} + +async function waitForVerificationCodeTarget(timeout = 10000) { + const start = Date.now(); + + while (Date.now() - start < timeout) { + throwIfStopped(); + if (is405MethodNotAllowedPage()) { + throw new Error('当前页面处于 405 错误恢复流程中,暂时无法定位验证码输入框。'); + } + + const target = getVerificationCodeTarget(); + if (target) { + return target; + } + + await sleep(150); + } + + throw new Error('未找到验证码输入框。URL: ' + location.href); +} + +async function waitForSplitVerificationInputsFilled(inputs, code, timeout = 2500) { + const expected = String(code || '').slice(0, 6); + const start = Date.now(); + + while (Date.now() - start < timeout) { + throwIfStopped(); + const current = Array.from(inputs || []) + .slice(0, expected.length) + .map((input) => String(input?.value || '').trim()) + .join(''); + + if (current === expected) { + return true; + } + + await sleep(100); + } + + return false; +} + async function fillVerificationCode(step, payload) { const { code } = payload; if (!code) throw new Error('未提供验证码。'); @@ -1684,6 +2087,7 @@ async function fillVerificationCode(step, payload) { // Retry with 405 error recovery if needed const maxRetries = 3; let codeInput = null; + let splitInputs = null; for (let retry = 0; retry <= maxRetries; retry++) { throwIfStopped(); @@ -1696,28 +2100,14 @@ async function fillVerificationCode(step, payload) { } try { - codeInput = await waitForElement(VERIFICATION_CODE_INPUT_SELECTOR, 10000); + const verificationTarget = await waitForVerificationCodeTarget(10000); + if (verificationTarget.type === 'split') { + splitInputs = verificationTarget.elements; + } else { + codeInput = verificationTarget.element; + } break; // Found it } catch { - // Check for multiple single-digit inputs (common pattern) - const singleInputs = document.querySelectorAll('input[maxlength="1"]'); - if (singleInputs.length >= 6) { - log(`步骤 ${step}:发现分开的单字符验证码输入框,正在逐个填写...`); - for (let i = 0; i < 6 && i < singleInputs.length; i++) { - fillInput(singleInputs[i], code[i]); - await sleep(100); - } - const outcome = await waitForVerificationSubmitOutcome(step); - if (outcome.invalidCode) { - log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn'); - } else if (outcome.addPhonePage) { - log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn'); - } else { - log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok'); - } - return outcome; - } - // No input found — check if it's a 405 error and can be recovered if (is405MethodNotAllowedPage() && retry < maxRetries) { log(`步骤 ${step}:未找到验证码输入框且页面出现 405 错误,正在恢复...`, 'warn'); @@ -1729,6 +2119,51 @@ async function fillVerificationCode(step, payload) { } } + if (splitInputs?.length >= 6) { + log(`步骤 ${step}:发现分开的单字符验证码输入框,正在逐个填写...`); + for (let i = 0; i < 6 && i < splitInputs.length; i++) { + const targetInput = splitInputs[i]; + try { + targetInput.focus?.(); + } catch {} + fillInput(splitInputs[i], code[i]); + try { + targetInput.dispatchEvent(new KeyboardEvent('keyup', { key: code[i], bubbles: true })); + } catch {} + await sleep(100); + } + const filled = await waitForSplitVerificationInputsFilled(splitInputs, code, 2500); + if (!filled) { + const current = Array.from(splitInputs) + .slice(0, 6) + .map((input) => String(input?.value || '').trim() || '_') + .join(''); + log(`步骤 ${step}:分格验证码输入框未稳定呈现目标值,当前页面值为 ${current},准备继续观察提交流程。`, 'warn'); + } else { + log(`步骤 ${step}:分格验证码输入框已稳定显示 ${code}。`, 'info'); + } + + await sleep(800); + const splitSubmitBtn = await waitForVerificationSubmitButton(splitInputs[0], 2000).catch(() => null); + if (splitSubmitBtn) { + await humanPause(450, 1200); + simulateClick(splitSubmitBtn); + log(`步骤 ${step}:分格验证码已提交`); + } else { + log(`步骤 ${step}:分格验证码页面未找到可点击提交按钮,继续等待页面自动推进。`, 'info'); + } + + const outcome = await waitForVerificationSubmitOutcome(step); + if (outcome.invalidCode) { + log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn'); + } else if (outcome.addPhonePage) { + log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn'); + } else { + log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok'); + } + return outcome; + } + if (!codeInput) { throw new Error('未找到验证码输入框。URL: ' + location.href); } @@ -1736,17 +2171,16 @@ async function fillVerificationCode(step, payload) { fillInput(codeInput, code); log(`步骤 ${step}:验证码已填写`); - // Report complete BEFORE submit (page may navigate away) - // Submit - await sleep(500); - const submitBtn = document.querySelector('button[type="submit"]') - || await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证/i, 5000).catch(() => null); + await sleep(800); + const submitBtn = await waitForVerificationSubmitButton(codeInput, 5000).catch(() => null); if (submitBtn) { await humanPause(450, 1200); simulateClick(submitBtn); log(`步骤 ${step}:验证码已提交`); + } else { + log(`步骤 ${step}:未找到可提交的验证码按钮,先等待页面自动推进或反馈结果。`, 'warn'); } const outcome = await waitForVerificationSubmitOutcome(step); @@ -1999,7 +2433,15 @@ async function step6SwitchToOneTimeCodeLogin(snapshot) { simulateClick(switchTrigger); log('步骤 7:已点击一次性验证码登录'); await sleep(1200); - return waitForStep6SwitchTransition(loginVerificationRequestedAt); + const result = await waitForStep6SwitchTransition(loginVerificationRequestedAt); + if (result?.step6Outcome === 'success') { + return finalizeStep6VerificationReady({ + logLabel: '步骤 7 收尾', + loginVerificationRequestedAt: result.loginVerificationRequestedAt || loginVerificationRequestedAt, + via: result.via || 'switch_to_one_time_code_login', + }); + } + return result; } async function step6LoginFromPasswordPage(payload, snapshot) { @@ -2030,8 +2472,11 @@ async function step6LoginFromPasswordPage(payload, snapshot) { const transition = await waitForStep6PasswordSubmitTransition(passwordSubmittedAt); if (transition.action === 'done') { - log('步骤 7:已进入登录验证码页面。', 'ok'); - return transition.result; + return finalizeStep6VerificationReady({ + logLabel: '步骤 7 收尾', + loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || passwordSubmittedAt, + via: transition.result.via || 'password_submit', + }); } if (transition.action === 'recoverable') { log(`步骤 7:${transition.result.message || '提交密码后仍未进入登录验证码页面,准备重新执行步骤 7。'}`, 'warn'); @@ -2077,8 +2522,11 @@ async function step6LoginFromEmailPage(payload, snapshot) { const transition = await waitForStep6EmailSubmitTransition(emailSubmittedAt); if (transition.action === 'done') { - log('步骤 7:已进入登录验证码页面。', 'ok'); - return transition.result; + return finalizeStep6VerificationReady({ + logLabel: '步骤 7 收尾', + loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || emailSubmittedAt, + via: transition.result.via || 'email_submit', + }); } if (transition.action === 'recoverable') { log(`步骤 7:${transition.result.message || '提交邮箱后仍未进入目标页面,准备重新执行步骤 7。'}`, 'warn'); @@ -2102,8 +2550,11 @@ async function step6_login(payload) { const snapshot = normalizeStep6Snapshot(await waitForKnownLoginAuthState(15000)); if (snapshot.state === 'verification_page') { - log('步骤 7:登录验证码页面已就绪。', 'ok'); - return createStep6SuccessResult(snapshot, { via: 'already_on_verification_page' }); + return finalizeStep6VerificationReady({ + logLabel: '步骤 7 收尾', + loginVerificationRequestedAt: null, + via: 'already_on_verification_page', + }); } if (snapshot.state === 'login_timeout_error_page') { 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..431d716 --- /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/managed-alias-utils.js b/managed-alias-utils.js index 5213097..51e8c69 100644 --- a/managed-alias-utils.js +++ b/managed-alias-utils.js @@ -3,6 +3,9 @@ })(typeof self !== 'undefined' ? self : globalThis, function createManagedAliasUtilsModule() { const GMAIL_PROVIDER = 'gmail'; const MAIL_2925_PROVIDER = '2925'; + const MAIL_2925_MODE_PROVIDE = 'provide'; + const MAIL_2925_MODE_RECEIVE = 'receive'; + const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE; const PROVIDER_CONFIGS = { [GMAIL_PROVIDER]: { @@ -64,10 +67,31 @@ return PROVIDER_CONFIGS[String(provider || '').trim().toLowerCase()] || null; } + function normalizeMail2925Mode(value = '') { + return String(value || '').trim().toLowerCase() === MAIL_2925_MODE_RECEIVE + ? MAIL_2925_MODE_RECEIVE + : DEFAULT_MAIL_2925_MODE; + } + function isManagedAliasProvider(provider = '') { return Boolean(getManagedAliasProviderConfig(provider)); } + function usesManagedAliasGeneration(provider = '', options = {}) { + const normalizedProvider = String(provider || '').trim().toLowerCase(); + if (!isManagedAliasProvider(normalizedProvider)) { + return false; + } + if (normalizedProvider !== MAIL_2925_PROVIDER) { + return true; + } + + const mail2925Mode = typeof options === 'string' + ? options + : options?.mail2925Mode; + return normalizeMail2925Mode(mail2925Mode) === MAIL_2925_MODE_PROVIDE; + } + function parseEmailParts(rawValue = '') { const value = String(rawValue || '').trim().toLowerCase(); const match = value.match(/^([^@\s]+)@([^@\s]+\.[^@\s]+)$/); @@ -136,11 +160,16 @@ return { buildManagedAliasEmail, + DEFAULT_MAIL_2925_MODE, getManagedAliasProviderConfig, getManagedAliasProviderUiCopy, isManagedAliasEmail, isManagedAliasProvider, + MAIL_2925_MODE_PROVIDE, + MAIL_2925_MODE_RECEIVE, + normalizeMail2925Mode, parseEmailParts, parseManagedAliasBaseEmail, + usesManagedAliasGeneration, }; }); diff --git a/manifest.json b/manifest.json index 791cbb0..ced3774 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, - "name": "多页面自动化", - "version": "5.0", - "version_name": "Pro5.0", + "name": "codex-oauth-automation-extension", + "version": "5.8", + "version_name": "Pro5.8", "description": "用于自动执行多步骤 OAuth 注册流程", "permissions": [ "sidePanel", diff --git a/sidepanel/account-pool-ui.js b/sidepanel/account-pool-ui.js new file mode 100644 index 0000000..642b55f --- /dev/null +++ b/sidepanel/account-pool-ui.js @@ -0,0 +1,58 @@ +(function attachSidepanelAccountPoolUi(globalScope) { + function createAccountPoolFormController(options = {}) { + const { + formShell = null, + toggleButton = null, + hiddenLabel = '添加账号', + visibleLabel = '取消添加', + onClear = null, + onFocus = null, + } = options; + + let visible = false; + + function sync() { + if (formShell) { + formShell.hidden = !visible; + } + if (toggleButton) { + toggleButton.textContent = visible ? visibleLabel : hiddenLabel; + toggleButton.setAttribute('aria-expanded', String(visible)); + } + } + + function setVisible(nextVisible, controlOptions = {}) { + const { + clearForm = false, + focusField = false, + } = controlOptions; + + visible = Boolean(nextVisible); + if (clearForm && typeof onClear === 'function') { + onClear(); + } + + sync(); + + if (visible && focusField && typeof onFocus === 'function') { + onFocus(); + } + } + + function isVisible() { + return visible; + } + + sync(); + + return { + isVisible, + setVisible, + sync, + }; + } + + globalScope.SidepanelAccountPoolUi = { + createAccountPoolFormController, + }; +})(window); diff --git a/sidepanel/hotmail-manager.js b/sidepanel/hotmail-manager.js index 2bef025..23fd45b 100644 --- a/sidepanel/hotmail-manager.js +++ b/sidepanel/hotmail-manager.js @@ -12,6 +12,7 @@ const expandedStorageKey = constants.expandedStorageKey || 'multipage-hotmail-list-expanded'; const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai'; const copyIcon = constants.copyIcon || ''; + const createAccountPoolFormController = globalScope.SidepanelAccountPoolUi?.createAccountPoolFormController; let actionInFlight = false; let listExpanded = false; @@ -177,6 +178,25 @@ dom.inputHotmailRefreshToken.value = ''; } + const formController = typeof createAccountPoolFormController === 'function' + ? createAccountPoolFormController({ + formShell: dom.hotmailFormShell, + toggleButton: dom.btnToggleHotmailForm, + hiddenLabel: '添加账号', + visibleLabel: '取消添加', + onClear: () => { + clearHotmailForm(); + }, + onFocus: () => { + dom.inputHotmailEmail?.focus?.(); + }, + }) + : { + isVisible: () => false, + setVisible() {}, + sync() {}, + }; + function renderHotmailAccounts() { if (!dom.hotmailAccountsList) return; const latestState = state.getLatestState(); @@ -318,7 +338,7 @@ } helpers.showToast(`已保存 Hotmail 账号 ${email}`, 'success', 1800); - clearHotmailForm(); + formController.setVisible(false, { clearForm: true }); } catch (err) { helpers.showToast(`保存 Hotmail 账号失败:${err.message}`, 'error'); } finally { @@ -474,6 +494,14 @@ setHotmailListExpanded(!listExpanded); }); + dom.btnToggleHotmailForm?.addEventListener('click', () => { + if (formController.isVisible()) { + formController.setVisible(false, { clearForm: true }); + return; + } + formController.setVisible(true, { focusField: true }); + }); + dom.btnHotmailUsageGuide?.addEventListener('click', async () => { await helpers.openConfirmModal({ title: '使用教程', @@ -514,6 +542,7 @@ dom.btnAddHotmailAccount?.addEventListener('click', handleAddHotmailAccount); dom.btnImportHotmailAccounts?.addEventListener('click', handleImportHotmailAccounts); dom.hotmailAccountsList?.addEventListener('click', handleAccountListClick); + formController.sync(); } return { diff --git a/sidepanel/mail-2925-manager.js b/sidepanel/mail-2925-manager.js index e7793b8..078f7df 100644 --- a/sidepanel/mail-2925-manager.js +++ b/sidepanel/mail-2925-manager.js @@ -12,6 +12,7 @@ const expandedStorageKey = constants.expandedStorageKey || 'multipage-mail2925-list-expanded'; const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai'; const copyIcon = constants.copyIcon || ''; + const createAccountPoolFormController = globalScope.SidepanelAccountPoolUi?.createAccountPoolFormController; let actionInFlight = false; let listExpanded = false; @@ -118,13 +119,29 @@ if (dom.inputMail2925Password) dom.inputMail2925Password.value = ''; } + const formController = typeof createAccountPoolFormController === 'function' + ? createAccountPoolFormController({ + formShell: dom.mail2925FormShell, + toggleButton: dom.btnToggleMail2925Form, + hiddenLabel: '添加账号', + visibleLabel: '取消添加', + onClear: () => { + stopEditingAccount({ clearForm: true }); + }, + onFocus: () => { + dom.inputMail2925Email?.focus?.(); + }, + }) + : { + isVisible: () => false, + setVisible() {}, + sync() {}, + }; + function syncEditUi() { if (dom.btnAddMail2925Account) { dom.btnAddMail2925Account.textContent = editingAccountId ? '保存修改' : '添加账号'; } - if (dom.btnCancelMail2925Edit) { - dom.btnCancelMail2925Edit.style.display = editingAccountId ? '' : 'none'; - } } function startEditingAccount(account) { @@ -132,6 +149,7 @@ editingAccountId = account.id; if (dom.inputMail2925Email) dom.inputMail2925Email.value = String(account.email || '').trim(); if (dom.inputMail2925Password) dom.inputMail2925Password.value = String(account.password || ''); + formController.setVisible(true, { focusField: false }); syncEditUi(); } @@ -234,7 +252,7 @@ } applyMail2925AccountMutation(response.account); - stopEditingAccount(); + formController.setVisible(false, { clearForm: true }); helpers.showToast( updatingExisting ? `已更新 2925 账号 ${email}` @@ -332,7 +350,7 @@ mail2925Accounts: [], currentMail2925AccountId: null, }); - stopEditingAccount(); + formController.setVisible(false, { clearForm: true }); refreshManagedAliasBaseEmail(); renderMail2925Accounts(); helpers.showToast(`已删除全部 ${response.deletedCount || 0} 个 2925 账号`, 'success', 2200); @@ -460,7 +478,7 @@ } state.syncLatestState(nextState); if (editingAccountId === accountId) { - stopEditingAccount(); + formController.setVisible(false, { clearForm: true }); } refreshManagedAliasBaseEmail(); renderMail2925Accounts(); @@ -479,6 +497,14 @@ setMail2925ListExpanded(!listExpanded); }); + dom.btnToggleMail2925Form?.addEventListener('click', () => { + if (formController.isVisible()) { + formController.setVisible(false, { clearForm: true }); + return; + } + formController.setVisible(true, { clearForm: !editingAccountId, focusField: true }); + }); + dom.btnDeleteAllMail2925Accounts?.addEventListener('click', async () => { if (actionInFlight) return; actionInFlight = true; @@ -493,12 +519,10 @@ }); dom.btnAddMail2925Account?.addEventListener('click', handleAddMail2925Account); - dom.btnCancelMail2925Edit?.addEventListener('click', () => { - stopEditingAccount(); - }); dom.btnImportMail2925Accounts?.addEventListener('click', handleImportMail2925Accounts); dom.mail2925AccountsList?.addEventListener('click', handleAccountListClick); syncEditUi(); + formController.sync(); } return { diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 06df28b..f40f857 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -1114,6 +1114,20 @@ header { visibility: hidden; } +.account-pool-form-shell { + display: flex; + flex-direction: column; + gap: 10px; +} + +.account-pool-actions-inline { + width: 100%; +} + +.account-pool-import-action { + margin-left: auto; +} + .hotmail-import-row { align-items: flex-start; } @@ -1136,7 +1150,7 @@ header { } .hotmail-list-shell.is-collapsed { - max-height: 236px; + max-height: 176px; } .hotmail-list-shell.is-expanded { diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 5a66eb0..835ae4b 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -4,7 +4,7 @@ - 多页面自动化面板 + codex-oauth-automation-extension - + - + 回调方式 +
+ +
-