diff --git a/background/mail-2925-session.js b/background/mail-2925-session.js index 2c9f900..7e8f2af 100644 --- a/background/mail-2925-session.js +++ b/background/mail-2925-session.js @@ -86,10 +86,9 @@ function isMail2925LimitReachedError(error) { const message = getErrorMessage(error); - const normalized = message.toLowerCase(); return message.startsWith(MAIL2925_LIMIT_ERROR_PREFIX) - || normalized.includes('子邮箱已达上限') - || normalized.includes('已达上限邮箱'); + || message.includes('子邮箱已达上限') + || message.includes('已达上限邮箱'); } function isMail2925ThreadTerminatedError(error) { @@ -385,7 +384,7 @@ origins: MAIL2925_COOKIE_ORIGINS, }); } catch (_) { - // 这里只做尽力清理。 + // Best effort cleanup only. } } @@ -399,10 +398,14 @@ actionLabel = '确保 2925 邮箱登录态', allowLoginWhenOnLoginPage = true, } = options; - const account = await ensureMail2925AccountForFlow({ - allowAllocate: true, - preferredAccountId: accountId, - }); + + let account = null; + if (forceRelogin) { + account = await ensureMail2925AccountForFlow({ + allowAllocate: true, + preferredAccountId: accountId, + }); + } const sendLoginMessage = typeof sendToContentScriptResilient === 'function' ? sendToContentScriptResilient @@ -416,13 +419,14 @@ } ); - const buildSuccessPayload = async (result = {}) => ({ - account: await ensureMail2925AccountForFlow({ - allowAllocate: false, - preferredAccountId: account.id, - }), + const buildSuccessPayload = () => ({ + account, mail: getMail2925MailConfig(), - result, + result: { + loggedIn: true, + currentView: 'mailbox', + usedExistingSession: true, + }, }); const failMailboxSession = async (message) => { @@ -471,31 +475,24 @@ if (matchedLoginTab?.url) { openedUrl = String(matchedLoginTab.url || '').trim(); } - 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 && !isMail2925LoginUrl(openedUrl)) { await addLog('2925:当前邮箱页未跳转到登录页,将直接复用已登录会话。', 'info'); - return buildSuccessPayload({ - loggedIn: true, - currentView: 'mailbox', - currentUrl: openedUrl, - usedExistingSession: true, - }); + 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, @@ -531,7 +528,7 @@ timeoutMs: forceRelogin ? 30000 : 25000, retryDelayMs: 800, responseTimeoutMs: forceRelogin ? 30000 : 25000, - logMessage: '步骤 0:2925 登录页通信异常,正在等待当前页面重新就绪后继续确认登录态...', + logMessage: '步骤 0:2925 登录页通信异常,正在等待页面恢复...', } ); } catch (err) { @@ -547,7 +544,7 @@ 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) { await failMailboxSession(`2925:${actionLabel}失败,登录后仍未进入收件箱。`); @@ -559,10 +556,18 @@ }); await setState({ currentMail2925AccountId: account.id }); broadcastDataUpdate({ currentMail2925AccountId: account.id }); + const finalUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl(); await addLog(`2925:登录态确认成功,当前地址=${finalUrl || 'unknown'}`, 'ok'); - return buildSuccessPayload(result); + return { + account: await ensureMail2925AccountForFlow({ + allowAllocate: false, + preferredAccountId: account.id, + }), + mail: getMail2925MailConfig(), + result, + }; } async function handleMail2925LimitReachedError(step, error) { @@ -570,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('流程已被用户停止。'); @@ -601,7 +617,7 @@ broadcastDataUpdate({ currentMail2925AccountId: null }); if (typeof requestStop === 'function') { await requestStop({ - logMessage: `步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,且当前没有可切换的下一个账号。`, + logMessage: `步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,但当前没有可切换的下一个账号。`, }); } return new Error('流程已被用户停止。'); @@ -616,7 +632,7 @@ }); await addLog(`步骤 ${step}:2925 已切换到下一个账号 ${nextAccount.email}。`, 'warn'); return buildMail2925ThreadTerminatedError( - `步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,已切换到 ${nextAccount.email},当前尝试结束,等待下一次重试。` + `步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,已切换到 ${nextAccount.email},当前尝试结束,等待下一轮重试。` ); } diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index e3f368b..9e602e7 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -295,24 +295,24 @@ function getManagedAliasProviderUiCopy(provider = selectMailProvider.value) { } if (String(provider || '').trim().toLowerCase() === GMAIL_PROVIDER) { return { - baseLabel: 'Gmail 原邮箱', + baseLabel: '基邮箱', basePlaceholder: '例如 yourname@gmail.com', buttonLabel: '生成', successVerb: '生成', label: 'Gmail +tag 邮箱', placeholder: '点击生成 Gmail +tag 邮箱,或手动填写完整邮箱', - hint: '先填写 Gmail 原邮箱后点“生成”,也可以直接手动填写完整的 Gmail 邮箱。', + hint: '先填写基邮箱后点“生成”,也可以直接手动填写完整的 Gmail 邮箱。', }; } if (String(provider || '').trim().toLowerCase() === '2925') { return { - baseLabel: '2925 基邮箱', + baseLabel: '基邮箱', basePlaceholder: '例如 yourname@2925.com', buttonLabel: '生成', successVerb: '生成', label: '2925 邮箱', placeholder: '点击生成 2925 邮箱,或手动填写完整邮箱', - hint: '先填写 2925 基邮箱后点“生成”,也可以直接手动填写完整的 2925 邮箱。', + hint: '先填写基邮箱后点“生成”,也可以直接手动填写完整的 2925 邮箱。', }; } return null; diff --git a/tests/background-mail2925-entry-behavior.test.js b/tests/background-mail2925-entry-behavior.test.js index dbec938..58fb798 100644 --- a/tests/background-mail2925-entry-behavior.test.js +++ b/tests/background-mail2925-entry-behavior.test.js @@ -72,6 +72,67 @@ test('ensureMail2925MailboxSession reuses current mailbox page without sending l assert.equal(result.result.usedExistingSession, true); }); +test('ensureMail2925MailboxSession does not require account-pool accounts when pool is off and mailbox page stays on mailList', async () => { + let currentState = { + autoRunning: false, + mail2925UseAccountPool: false, + mail2925Accounts: [], + currentMail2925AccountId: null, + }; + let sendCalls = 0; + + const manager = api.createMail2925SessionManager({ + addLog: async () => {}, + broadcastDataUpdate: () => {}, + chrome: { + tabs: { + get: async () => ({ id: 9, url: 'https://2925.com/#/mailList' }), + }, + cookies: { + getAll: async () => [], + remove: async () => ({ ok: true }), + }, + browsingData: { + removeCookies: async () => {}, + }, + }, + ensureContentScriptReadyOnTab: async () => {}, + 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, + sendToMailContentScriptResilient: async () => { + sendCalls += 1; + return { loggedIn: true }; + }, + setPersistentSettings: async (payload) => { + currentState = { ...currentState, ...payload }; + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + throwIfStopped: () => {}, + upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList, + }); + + const result = await manager.ensureMail2925MailboxSession({ + accountId: null, + forceRelogin: false, + allowLoginWhenOnLoginPage: false, + actionLabel: '步骤 4:确认 2925 邮箱登录态', + }); + + assert.equal(sendCalls, 0); + assert.equal(result.result.usedExistingSession, true); + assert.equal(result.account, null); +}); + test('ensureMail2925MailboxSession stops immediately when login page is detected and account pool is off', async () => { let currentState = { autoRunning: true, diff --git a/tests/background-mail2925-relogin-wait.test.js b/tests/background-mail2925-relogin-wait.test.js index c95d9f1..1f3cd4a 100644 --- a/tests/background-mail2925-relogin-wait.test.js +++ b/tests/background-mail2925-relogin-wait.test.js @@ -78,5 +78,5 @@ test('ensureMail2925MailboxSession waits 3 seconds before and after opening logi assert.deepStrictEqual(events.openedUrls, ['https://2925.com/login/']); assert.deepStrictEqual(events.sleeps, [3000, 3000]); - assert.equal(events.readyCalls, 2); + assert.equal(events.readyCalls, 1); }); diff --git a/tests/background-mail2925-session-module.test.js b/tests/background-mail2925-session-module.test.js index 714c9ba..19188a7 100644 --- a/tests/background-mail2925-session-module.test.js +++ b/tests/background-mail2925-session-module.test.js @@ -17,6 +17,7 @@ test('handleMail2925LimitReachedError disables current account and switches to t const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope); let currentState = { + mail2925UseAccountPool: true, mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([ { id: 'current', email: 'current@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 }, { id: 'next', email: 'next@2925.com', password: 'p2', enabled: true, lastUsedAt: 20 }, @@ -99,6 +100,7 @@ test('handleMail2925LimitReachedError requests stop when no next mail2925 accoun const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope); let currentState = { + mail2925UseAccountPool: true, mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([ { id: 'only', email: 'only@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 }, ]), @@ -225,3 +227,70 @@ test('ensureMail2925MailboxSession requests stop when auto run is active and log assert.equal(events.stopCalls.length, 1); assert.match(events.stopCalls[0].logMessage, /登录后仍未进入收件箱/); }); + +test('handleMail2925LimitReachedError stops immediately when account pool is off even if another account exists', async () => { + const source = fs.readFileSync('background/mail-2925-session.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope); + + let currentState = { + mail2925UseAccountPool: false, + mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([ + { id: 'current', email: 'current@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 }, + { id: 'next', email: 'next@2925.com', password: 'p2', enabled: true, lastUsedAt: 20 }, + ]), + currentMail2925AccountId: 'current', + }; + const events = { + stopCalls: [], + sessionChecks: 0, + }; + + const manager = api.createMail2925SessionManager({ + addLog: async () => {}, + broadcastDataUpdate: () => {}, + chrome: { + cookies: { + getAll: async () => [], + remove: async () => ({ ok: true }), + }, + browsingData: { + removeCookies: async () => {}, + }, + }, + findMail2925Account: mail2925Utils.findMail2925Account, + getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus, + getState: async () => currentState, + isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable, + MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS, + normalizeMail2925Account: mail2925Utils.normalizeMail2925Account, + normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts, + pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun, + requestStop: async (options = {}) => { + events.stopCalls.push(options); + }, + reuseOrCreateTab: async () => 1, + sendToMailContentScriptResilient: async () => { + events.sessionChecks += 1; + return { loggedIn: true }; + }, + setPersistentSettings: async (payload) => { + currentState = { ...currentState, ...payload }; + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + throwIfStopped: () => {}, + upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList, + }); + + const error = await manager.handleMail2925LimitReachedError( + 4, + new Error('MAIL2925_LIMIT_REACHED::子邮箱已达上限邮箱') + ); + + assert.equal(error.message, '流程已被用户停止。'); + assert.equal(events.sessionChecks, 0); + assert.equal(events.stopCalls.length, 1); + assert.equal(currentState.currentMail2925AccountId, 'current'); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 4567a48..88385c0 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -331,7 +331,7 @@ 补充行为: - `2925` provider 会关闭 Step 4 / 8 的自动重发间隔 25 秒节流;每次“重新发送验证码”之间,会在邮箱页内部执行一轮固定 15 次的刷新轮询,不再因 OAuth 剩余时间预算而缩短。 -- 当 provider 为 `2925` 时,Step 4 会优先直接打开当前 2925 邮箱页:如果页面仍停留在收件箱,就直接复用当前已登录页面;如果页面已经跳到登录页,则只有在启用了 2925 账号池时才会自动登录,未启用账号池时会直接报错停止。Step 8 不再额外承接这套登录态处理。 +- 当 provider 为 `2925` 时,Step 4 会优先直接打开当前 2925 邮箱页:如果页面仍停留在收件箱,就直接复用当前已登录页面;如果页面已经跳到登录页,则只有在启用了 2925 账号池时才会自动登录,未启用账号池时会直接复用现有停止逻辑结束流程。Step 8 不再额外承接这套登录态处理。 - 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 当前既不依赖时间窗,也不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中的匹配邮件。 - 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。 - 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。 @@ -555,8 +555,8 @@ 1. 用户在 sidepanel 的 2925 账号池中保存 `email / password` 2. 只有当 sidepanel 中的 `mail2925UseAccountPool` 开关开启时,别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail` -3. 手动点击 `登录` 或自动流程进入 Step 4 前,后台会先打开当前 2925 邮箱页:如果仍停留在收件箱则直接复用;如果跳到登录页,则仅在号池模式开启时才自动登录,关闭号池时直接停止 -4. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先记录当前时间;若当前处于号池模式且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录 +3. 手动点击 `登录` 或自动流程进入 Step 4 前,后台会先打开当前 2925 邮箱页:如果仍停留在收件箱则直接复用;如果跳到登录页,则仅在号池模式开启时才自动登录,关闭号池时直接调用现有停止逻辑结束流程 +4. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先判断是否启用了号池模式:若已启用且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录;若未启用,则直接调用现有停止逻辑结束流程 5. 如果登录提交后 20 秒内仍未进入收件箱,且当前正处于自动运行中,则后台会直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态;这类情况常见于图片验证、行为验证或其他阻断登录的中间页 6. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态 7. sidepanel 中 2925 账号池的新增表单也走与 Hotmail 相同的共享交互:默认收起,头部按钮切换“添加账号 / 取消添加”,操作行右侧提供“批量导入”,保存成功后自动收起并清空。