diff --git a/README.md b/README.md index e5f912e..967bb11 100644 --- a/README.md +++ b/README.md @@ -579,9 +579,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 +600,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 43b89dc..83b9e09 100644 --- a/background.js +++ b/background.js @@ -6564,6 +6564,7 @@ function getMailConfig(state) { } if (provider === '2925') { return { + provider: '2925', source: 'mail-2925', url: 'https://2925.com/#/mailList', label: '2925 邮箱', diff --git a/background/mail-2925-session.js b/background/mail-2925-session.js index 4649231..7e8f2af 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, @@ -571,13 +528,12 @@ timeoutMs: forceRelogin ? 30000 : 25000, retryDelayMs: 800, responseTimeoutMs: forceRelogin ? 30000 : 25000, - logMessage: `步骤 0:2925 登录页通信异常,正在等待当前页面重新就绪后继续确认登录态...`, + logMessage: '步骤 0:2925 登录页通信异常,正在等待页面恢复...', } ); } catch (err) { - const stopped = await stopAutoRunForMail2925LoginFailure( - `2925:${actionLabel}失败(${getErrorMessage(err) || '20 秒内未进入收件箱'}),已按手动停止逻辑暂停自动流程。` - ); + const message = `2925:${actionLabel}失败(${getErrorMessage(err) || '20 秒内未进入收件箱'})。`; + 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 294e458..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,36 +98,28 @@ } 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 = ![ diff --git a/content/signup-page.js b/content/signup-page.js index 9b7548d..6f68fba 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; @@ -1198,7 +1206,7 @@ function getSignupPasswordTimeoutErrorPageState() { function getLoginTimeoutErrorPageState() { return getAuthTimeoutErrorPageState({ - pathPatterns: [/\/log-in(?:[/?#]|$)/i], + pathPatterns: getLoginAuthRetryPathPatterns(), }); } @@ -1263,13 +1271,6 @@ function inspectLoginAuthState() { consentReady, }; - if (verificationTarget) { - return { - ...baseState, - state: 'verification_page', - }; - } - if (retryState) { return { ...baseState, @@ -1277,6 +1278,13 @@ function inspectLoginAuthState() { }; } + if (verificationTarget) { + return { + ...baseState, + state: 'verification_page', + }; + } + if (addPhonePage) { return { ...baseState, @@ -1434,6 +1442,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; @@ -1633,7 +1721,10 @@ async function waitForVerificationSubmitOutcome(step, timeout) { if (retryState?.userAlreadyExistsBlocked) { throw createSignupUserAlreadyExistsError(); } - if (retryState && recoveryCount < maxRecoveryCount) { + if (retryState) { + if (recoveryCount >= maxRecoveryCount) { + throw new Error(`步骤 ${step}:验证码提交后连续进入认证重试页 ${maxRecoveryCount} 次,页面仍未恢复。URL: ${location.href}`); + } recoveryCount += 1; log(`步骤 ${step}:验证码提交后进入认证重试页,正在自动恢复(${recoveryCount}/${maxRecoveryCount})...`, 'warn'); await recoverCurrentAuthRetryPage({ @@ -1731,6 +1822,26 @@ async function waitForVerificationSubmitButton(codeInput, timeout = 5000) { 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(); @@ -1766,6 +1877,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(); @@ -1778,56 +1890,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++) { - const targetInput = singleInputs[i]; - try { - targetInput.focus?.(); - } catch {} - fillInput(singleInputs[i], code[i]); - try { - targetInput.dispatchEvent(new KeyboardEvent('keyup', { key: code[i], bubbles: true })); - } catch {} - await sleep(100); - } - const filled = await waitForSplitVerificationInputsFilled(singleInputs, code, 2500); - if (!filled) { - const current = Array.from(singleInputs) - .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(singleInputs[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; - } - // No input found — check if it's a 405 error and can be recovered if (is405MethodNotAllowedPage() && retry < maxRetries) { log(`步骤 ${step}:未找到验证码输入框且页面出现 405 错误,正在恢复...`, 'warn'); @@ -1839,6 +1909,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); } @@ -2108,7 +2223,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) { @@ -2139,8 +2262,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'); @@ -2186,8 +2312,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'); @@ -2211,8 +2340,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/manifest.json b/manifest.json index 791cbb0..733a5a4 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, "name": "多页面自动化", - "version": "5.0", - "version_name": "Pro5.0", + "version": "5.4", + "version_name": "Pro5.4", "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 b4f4081..ffd6bc0 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -34,17 +34,8 @@
- 公告 / 使用教程有更新了,可点上方“贡献/使用”查看。 -
- -+ 公告 / 使用教程有更新了,可点上方“贡献/使用”查看。 +
+ +