diff --git a/README.md b/README.md index e5f912e..43468ac 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 类似,但会使用稍微不同的关键词组合去找登录验证码邮件。 diff --git a/background.js b/background.js index 7686a8a..04f98d8 100644 --- a/background.js +++ b/background.js @@ -461,7 +461,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 = '') { @@ -2149,11 +2149,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 = {}) { @@ -2296,11 +2306,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 = {}) { @@ -6596,6 +6616,7 @@ function getMailConfig(state) { } if (provider === '2925') { return { + provider: '2925', source: 'mail-2925', url: 'https://2925.com/#/mailList', label: '2925 邮箱', 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..c78a350 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,28 @@ } } + 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; + } + } + async function setCurrentMail2925Account(accountId, options = {}) { const { logMessage = '', updateLastUsedAt = false } = options; const state = await getState(); @@ -361,7 +384,7 @@ origins: MAIL2925_COOKIE_ORIGINS, }); } catch (_) { - // Best-effort cleanup only. + // Best effort cleanup only. } } @@ -373,170 +396,15 @@ accountId = null, forceRelogin = false, actionLabel = '确保 2925 邮箱登录态', + allowLoginWhenOnLoginPage = true, } = options; - const account = await ensureMail2925AccountForFlow({ - allowAllocate: true, - preferredAccountId: accountId, - }); + let account = null; if (forceRelogin) { - const removedCount = await clearMail2925SessionCookies(); - await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info'); - } - - 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); + account = await ensureMail2925AccountForFlow({ + allowAllocate: true, + preferredAccountId: accountId, + }); } const sendLoginMessage = typeof sendToContentScriptResilient === 'function' @@ -551,9 +419,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)) { + await addLog('2925:当前邮箱页未跳转到登录页,将直接复用已登录会话。', 'info'); + return buildSuccessPayload(); + } + + if (!forceRelogin && !allowLoginWhenOnLoginPage) { + await failMailboxSession(`2925:${actionLabel}失败,当前页面已跳转到登录页,且当前未启用 2925 账号池,不执行自动登录。`); + } + + if (!account) { + 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, @@ -568,16 +525,15 @@ }, }, { - 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 +541,13 @@ } 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('流程已被用户停止。'); - } - throw new Error(`2925:${actionLabel}失败,当前页面仍未进入收件箱。`); + await failMailboxSession(`2925:${actionLabel}失败,登录后仍未进入收件箱。`); } await patchMail2925Account(account.id, { @@ -614,7 +556,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 +575,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 +602,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' ); @@ -663,7 +617,7 @@ 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 +627,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/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..c1d9124 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -24,15 +24,39 @@ throwIfStopped, } = deps; + 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 +64,7 @@ await chrome.tabs.update(signupTabId, { active: true }); throwIfStopped(); await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...'); + const prepareResult = await sendToContentScriptResilient( 'signup-page', { @@ -73,43 +98,40 @@ } 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), 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/verification-flow.js b/background/verification-flow.js index 56a428e..ff954b8 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/mail-2925.js b/content/mail-2925.js index 54970d2..0bbb977 100644 --- a/content/mail-2925.js +++ b/content/mail-2925.js @@ -629,6 +629,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); @@ -902,11 +935,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:提交账号密码后未进入收件箱。'); @@ -930,6 +964,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 +1030,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 b0f9d1e..f0cae4a 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -126,16 +126,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; @@ -1272,7 +1280,7 @@ function getSignupPasswordTimeoutErrorPageState() { function getLoginTimeoutErrorPageState() { return getAuthTimeoutErrorPageState({ - pathPatterns: [/\/log-in(?:[/?#]|$)/i], + pathPatterns: getLoginAuthRetryPathPatterns(), }); } @@ -1339,6 +1347,13 @@ function inspectLoginAuthState() { consentReady, }; + if (retryState) { + return { + ...baseState, + state: 'login_timeout_error_page', + }; + } + if (phoneVerificationPage) { return { ...baseState, @@ -1354,13 +1369,6 @@ function inspectLoginAuthState() { }; } - if (retryState) { - return { - ...baseState, - state: 'login_timeout_error_page', - }; - } - if (addPhonePage) { return { ...baseState, @@ -1519,6 +1527,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; @@ -1707,15 +1795,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(); @@ -1755,6 +1858,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('未提供验证码。'); @@ -1769,6 +1962,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(); @@ -1781,28 +1975,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'); @@ -1814,6 +1994,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); } @@ -1821,17 +2046,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); @@ -2084,7 +2308,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) { @@ -2115,8 +2347,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'); @@ -2162,8 +2397,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'); @@ -2187,8 +2425,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/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 c395652..b96d24b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, "name": "多页面自动化", - "version": "5.0", - "version_name": "Pro5.0", + "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 87558d9..f40f857 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -243,33 +243,35 @@ header { color: var(--orange); } -.contribution-entry { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 6px; - position: relative; +.contribution-update-layer { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 1400; } .contribution-update-hint { - position: relative; + --contribution-update-arrow-left: 20px; + position: absolute; + top: 0; + left: 0; display: flex; align-items: flex-start; gap: 8px; - max-width: 220px; - margin-left: 6px; + max-width: min(220px, calc(100vw - 24px)); padding: 8px 10px; background: color-mix(in srgb, var(--amber) 10%, var(--bg-base)); border: 1px solid color-mix(in srgb, var(--amber) 34%, var(--border)); border-radius: 10px; box-shadow: var(--shadow-sm); + pointer-events: auto; } .contribution-update-hint::before { content: ''; position: absolute; top: -7px; - left: 14px; + left: calc(var(--contribution-update-arrow-left) - 6px); width: 12px; height: 12px; background: inherit; @@ -436,7 +438,7 @@ header { .run-group { display: flex; - align-items: flex-start; + align-items: center; gap: 4px; flex-wrap: wrap; } @@ -1112,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; } @@ -1134,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 0215ae8..b79a3f4 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -34,18 +34,9 @@
-
- - -
- + +
+ +