diff --git a/background.js b/background.js index 4dcb7ca..bbc85c7 100644 --- a/background.js +++ b/background.js @@ -5498,6 +5498,7 @@ const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMa sleepWithStop, throwIfStopped, upsertMail2925AccountInList, + waitForTabComplete, waitForTabUrlMatch, }); diff --git a/background/mail-2925-session.js b/background/mail-2925-session.js index 4db55db..502362a 100644 --- a/background/mail-2925-session.js +++ b/background/mail-2925-session.js @@ -25,6 +25,7 @@ sleepWithStop, throwIfStopped, upsertMail2925AccountInList, + waitForTabComplete, waitForTabUrlMatch, } = deps; @@ -45,6 +46,9 @@ ]; const MAIL2925_LIMIT_ERROR_PREFIX = 'MAIL2925_LIMIT_REACHED::'; const MAIL2925_THREAD_TERMINATED_ERROR_PREFIX = 'MAIL2925_THREAD_TERMINATED::'; + const MAIL2925_LOGIN_MESSAGE_RETRY_WINDOW_MS = 15000; + const MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS = 120000; + const MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS = 120000; function getMail2925MailConfig() { return { @@ -95,6 +99,14 @@ return getErrorMessage(error).startsWith(MAIL2925_THREAD_TERMINATED_ERROR_PREFIX); } + function isRetryableMail2925TransportError(error) { + const message = getErrorMessage(error).toLowerCase(); + return message.includes('receiving end does not exist') + || message.includes('message port closed') + || message.includes('content script on') + || message.includes('did not respond'); + } + async function syncMail2925Accounts(accounts) { const normalized = normalizeMail2925Accounts(accounts); await setPersistentSettings({ mail2925Accounts: normalized }); @@ -399,6 +411,43 @@ return removedCount; } + async function recoverMail2925LoginPageAfterTransportError(tabId) { + const numericTabId = Number(tabId); + if (!Number.isInteger(numericTabId) || numericTabId <= 0) { + return; + } + + const currentUrl = (await getMail2925TabUrlById(numericTabId)) || await getMail2925CurrentTabUrl(); + await addLog( + `2925:登录提交后页面发生跳转或重载,正在等待当前标签页恢复后继续确认登录态。当前地址:${currentUrl || 'unknown'}`, + 'warn' + ); + + if (typeof waitForTabComplete === 'function') { + const completedTab = await waitForTabComplete(numericTabId, { + timeoutMs: MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS, + retryDelayMs: 300, + }); + await addLog( + `2925:登录跳转等待结束,当前标签地址:${String(completedTab?.url || '').trim() || 'unknown'}`, + completedTab?.url ? 'info' : 'warn' + ); + } + + if (typeof ensureContentScriptReadyOnTab === 'function') { + await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, numericTabId, { + inject: MAIL2925_INJECT, + injectSource: MAIL2925_INJECT_SOURCE, + timeoutMs: MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS, + retryDelayMs: 800, + logMessage: '步骤 0:2925 登录后页面仍在跳转,正在等待邮箱页重新就绪...', + }); + } + + const recoveredUrl = (await getMail2925TabUrlById(numericTabId)) || await getMail2925CurrentTabUrl(); + await addLog(`2925:登录跳转恢复后当前标签地址:${recoveredUrl || 'unknown'}`, 'info'); + } + async function ensureMail2925MailboxSession(options = {}) { const { accountId = null, @@ -520,10 +569,10 @@ } let result; - try { + const sendEnsureSessionRequest = async () => { const beforeSendUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl(); await addLog(`2925:发送 ENSURE_MAIL2925_SESSION 前当前地址:${beforeSendUrl || 'unknown'}`, 'info'); - result = await sendLoginMessage( + return sendLoginMessage( MAIL2925_SOURCE, { type: 'ENSURE_MAIL2925_SESSION', @@ -537,19 +586,34 @@ }, }, { - timeoutMs: 50000, + timeoutMs: MAIL2925_LOGIN_MESSAGE_RETRY_WINDOW_MS, retryDelayMs: 800, - responseTimeoutMs: 50000, + responseTimeoutMs: MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS, logMessage: '步骤 0:2925 登录页通信异常,正在等待页面恢复...', } ); + }; + try { + result = await sendEnsureSessionRequest(); } catch (err) { - const message = `2925:${actionLabel}失败(${getErrorMessage(err) || '40 秒内未进入收件箱'})。`; - const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`); - if (stopped) { - throw new Error('流程已被用户停止。'); + if (isRetryableMail2925TransportError(err)) { + try { + await recoverMail2925LoginPageAfterTransportError(tabId); + await addLog('2925:页面恢复完成,正在重新确认登录态...', 'info'); + result = await sendEnsureSessionRequest(); + } catch (recoveryErr) { + err = recoveryErr; + } + } + + if (!result) { + const message = `2925:${actionLabel}失败(${getErrorMessage(err) || '登录结果确认超时'})。`; + const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`); + if (stopped) { + throw new Error('流程已被用户停止。'); + } + throw err; } - throw err; } if (result?.error) { diff --git a/tests/background-mail2925-entry-behavior.test.js b/tests/background-mail2925-entry-behavior.test.js index ec75def..fe26b5c 100644 --- a/tests/background-mail2925-entry-behavior.test.js +++ b/tests/background-mail2925-entry-behavior.test.js @@ -267,6 +267,91 @@ test('ensureMail2925MailboxSession logs in when login page is detected and accou assert.equal(result.result.loggedIn, true); }); +test('ensureMail2925MailboxSession recovers after login-page navigation reload breaks the old content-script channel', async () => { + let currentState = { + autoRunning: false, + mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([ + { id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 }, + ]), + currentMail2925AccountId: 'acc-1', + }; + let tabUrl = 'https://2925.com/login/'; + const events = { + logs: [], + readyCalls: 0, + sendCalls: 0, + waitCompleteCalls: 0, + }; + + const manager = api.createMail2925SessionManager({ + addLog: async (message, level = 'info') => { + events.logs.push({ message, level }); + }, + broadcastDataUpdate: () => {}, + chrome: { + tabs: { + get: async () => ({ id: 9, url: tabUrl, status: tabUrl.includes('/#/mailList') ? 'complete' : 'loading' }), + }, + cookies: { + getAll: async () => [], + remove: async () => ({ ok: true }), + }, + browsingData: { + removeCookies: async () => {}, + }, + }, + ensureContentScriptReadyOnTab: async () => { + events.readyCalls += 1; + }, + findMail2925Account: mail2925Utils.findMail2925Account, + getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus, + getState: async () => currentState, + isAutoRunLockedState: () => false, + isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable, + MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS, + normalizeMail2925Account: mail2925Utils.normalizeMail2925Account, + normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts, + pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun, + reuseOrCreateTab: async () => 9, + sendToContentScriptResilient: async () => { + events.sendCalls += 1; + if (events.sendCalls === 1) { + throw new Error('Could not establish connection. Receiving end does not exist.'); + } + return { loggedIn: true, currentView: 'mailbox', mailboxEmail: 'acc1@2925.com' }; + }, + setPersistentSettings: async (payload) => { + currentState = { ...currentState, ...payload }; + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + throwIfStopped: () => {}, + upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList, + waitForTabComplete: async () => { + events.waitCompleteCalls += 1; + tabUrl = 'https://2925.com/#/mailList'; + return { id: 9, url: tabUrl, status: 'complete' }; + }, + }); + + const result = await manager.ensureMail2925MailboxSession({ + accountId: 'acc-1', + forceRelogin: false, + allowLoginWhenOnLoginPage: true, + actionLabel: '步骤 4:确认 2925 邮箱登录态', + }); + + assert.equal(events.sendCalls, 2); + assert.equal(events.readyCalls, 2); + assert.equal(events.waitCompleteCalls, 1); + assert.equal(result.result.loggedIn, true); + const combinedLogs = events.logs.map(({ message }) => message).join('\n'); + assert.match(combinedLogs, /登录提交后页面发生跳转或重载/); + assert.match(combinedLogs, /登录跳转恢复后当前标签地址:https:\/\/2925\.com\/#\/mailList/); + assert.match(combinedLogs, /页面恢复完成,正在重新确认登录态/); +}); + test('ensureMail2925MailboxSession relogs with selected account when mailbox page email mismatches and pool is on', async () => { let currentState = { autoRunning: false, diff --git a/tests/background-mail2925-relogin-wait.test.js b/tests/background-mail2925-relogin-wait.test.js index 161988b..bb6237e 100644 --- a/tests/background-mail2925-relogin-wait.test.js +++ b/tests/background-mail2925-relogin-wait.test.js @@ -8,11 +8,13 @@ test('background mail2925 session uses /login/ as relogin entry url', () => { assert.match(source, /const MAIL2925_LOGIN_URL = 'https:\/\/2925\.com\/login\/';/); }); -test('background mail2925 session keeps login message timeout above the 40-second mailbox wait', () => { +test('background mail2925 session keeps a long login response timeout and a separate page-recovery window', () => { const source = fs.readFileSync('background/mail-2925-session.js', 'utf8'); - assert.match(source, /timeoutMs:\s*50000,/); - assert.match(source, /responseTimeoutMs:\s*50000,/); - assert.match(source, /40 秒内未进入收件箱/); + assert.match(source, /const MAIL2925_LOGIN_MESSAGE_RETRY_WINDOW_MS = 15000;/); + assert.match(source, /const MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS = 120000;/); + assert.match(source, /const MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS = 120000;/); + assert.match(source, /responseTimeoutMs:\s*MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS,/); + assert.match(source, /recoverMail2925LoginPageAfterTransportError/); }); test('ensureMail2925MailboxSession waits 3 seconds before and after opening login page on force relogin', async () => { diff --git a/项目完整链路说明.md b/项目完整链路说明.md index eaaa883..366d38e 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -334,6 +334,7 @@ - `2925` provider 会关闭 Step 4 / 8 的自动重发间隔 25 秒节流;每次“重新发送验证码”之间,会在邮箱页内部执行一轮固定 15 次的刷新轮询,不再因 OAuth 剩余时间预算而缩短。 - 当 provider 为 `2925` 时,Step 4 会优先直接打开当前 2925 邮箱页,并先比对页面顶部显示的邮箱地址是否与当前目标邮箱一致:如果一致,就直接复用当前已登录页面;如果不一致且启用了 2925 账号池,则会先清理 cookie 再登录当前选中的账号;如果不一致且未启用账号池,则直接复用现有停止逻辑结束流程。Step 8 不再额外承接这套登录态处理。 +- `2925` 在执行自动登录后,如果登录页因为跳转或重载导致原内容脚本通信中断,后台不会立刻判失败;而是会等待当前标签页重新加载完成、重新确认内容脚本就绪后,再继续确认是否已经进入收件箱。这段登录恢复窗口当前按 2 分钟控制。 - 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 在 Step 4 / Step 8 会固定使用“步骤开始时间向前回看 10 分钟”的时间窗,不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中落在该固定时间窗内的匹配邮件。 - 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。 - 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。