diff --git a/.gitignore b/.gitignore index ea8cd9f..1d65524 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /data/account-run-history.json .npm-test.log .omx/ +/node_modules \ No newline at end of file diff --git a/README.md b/README.md index a083c13..dba0351 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ 3. 在“2925 基邮箱”右侧打开 `号池` 开关后,基邮箱输入框会切成下拉框,只能从 2925 账号池中选择邮箱;关闭开关则回退到原来的手填基邮箱路线 4. 可先点 `使用此账号` 让当前别名基邮箱切到这条 2925 账号,再点 `登录` 手动验证网页邮箱登录态 5. 只有在 `号池` 开关开启时,自动流程执行到 Step 4 / Step 8 前才会自动检查 2925 登录态;如果未登录,会直接使用当前账号自动登录 -6. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间、把当前 2925 账号禁用 24 小时、自动切换到下一个可用账号并完成登录,然后直接结束当前尝试 +6. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间;如果还有下一个可用账号,就禁用当前账号 24 小时并自动切换登录;如果没有下一个可用账号,或当前未启用号池模式,则会直接复用现有“手动暂停 / 停止”逻辑终止自动流程 7. 如果你同时开启了 `Auto` 的自动重试,当前尝试结束后会按现有逻辑自动进入下一次尝试,不需要再手动介入 8. `Mail = 2925` 仍然走 Gmail / 2925 共用的别名邮箱链路;实际注册邮箱会基于当前 2925 账号邮箱生成,例如 `name@2925.com -> name123456@2925.com` diff --git a/background.js b/background.js index dc916ef..38a4150 100644 --- a/background.js +++ b/background.js @@ -5392,6 +5392,7 @@ const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMa normalizeMail2925Account, normalizeMail2925Accounts, pickMail2925AccountForRun, + requestStop, reuseOrCreateTab, sendToMailContentScriptResilient, setPersistentSettings, @@ -5453,6 +5454,14 @@ function isMail2925ThreadTerminatedError(error) { return /^MAIL2925_THREAD_TERMINATED::/.test(message); } +function isMail2925PoolExhaustedPauseError(error) { + if (typeof mail2925SessionManager !== 'undefined' && mail2925SessionManager?.isMail2925PoolExhaustedPauseError) { + return mail2925SessionManager.isMail2925PoolExhaustedPauseError(error); + } + const message = String(typeof error === 'string' ? error : error?.message || ''); + return /^MAIL2925_POOL_EXHAUSTED_PAUSE::/.test(message); +} + const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGeneratedEmailHelpers({ addLog, buildGeneratedAliasEmail, diff --git a/background/mail-2925-session.js b/background/mail-2925-session.js index cfa6a98..db484a6 100644 --- a/background/mail-2925-session.js +++ b/background/mail-2925-session.js @@ -14,6 +14,7 @@ normalizeMail2925Accounts, pickMail2925AccountForRun, getState, + requestStop, reuseOrCreateTab, sendToMailContentScriptResilient, setPersistentSettings, @@ -403,7 +404,12 @@ const state = await getState(); const currentAccount = getCurrentMail2925Account(state); if (!currentAccount) { - return buildMail2925ThreadTerminatedError(`步骤 ${step}:2925 检测到“${reason}”,但当前没有可识别的账号,已结束本次尝试。`); + if (typeof requestStop === 'function') { + await requestStop({ + logMessage: `步骤 ${step}:2925 检测到“${reason}”,且当前没有可识别账号,已按手动停止逻辑暂停流程。`, + }); + } + return new Error('流程已被用户停止。'); } const disabledUntil = Date.now() + Math.max(1, Number(MAIL2925_LIMIT_COOLDOWN_MS) || (24 * 60 * 60 * 1000)); @@ -426,9 +432,12 @@ if (!nextAccount) { await setState({ currentMail2925AccountId: null }); broadcastDataUpdate({ currentMail2925AccountId: null }); - return buildMail2925ThreadTerminatedError( - `步骤 ${step}:2925 账号 ${currentAccount.email} 已因“${reason}”禁用 24 小时,且当前没有可切换的下一个账号,本次尝试结束。` - ); + if (typeof requestStop === 'function') { + await requestStop({ + logMessage: `步骤 ${step}:2925 账号 ${currentAccount.email} 已因“${reason}”禁用 24 小时,且当前没有可切换的下一个账号,已按手动停止逻辑暂停流程。`, + }); + } + return new Error('流程已被用户停止。'); } await setCurrentMail2925Account(nextAccount.id); diff --git a/background/verification-flow.js b/background/verification-flow.js index 0b38cfe..9e20dd6 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -420,8 +420,7 @@ throw err; } if (mail?.provider === '2925' && typeof isMail2925LimitReachedError === 'function' && isMail2925LimitReachedError(err)) { - const latestState = await getState(); - if (latestState?.mail2925UseAccountPool && typeof handleMail2925LimitReachedError === 'function') { + if (typeof handleMail2925LimitReachedError === 'function') { throw await handleMail2925LimitReachedError(step, err); } throw err; @@ -564,8 +563,7 @@ throw err; } if (mail?.provider === '2925' && typeof isMail2925LimitReachedError === 'function' && isMail2925LimitReachedError(err)) { - const latestState = await getState(); - if (latestState?.mail2925UseAccountPool && typeof handleMail2925LimitReachedError === 'function') { + if (typeof handleMail2925LimitReachedError === 'function') { throw await handleMail2925LimitReachedError(step, err); } throw err; diff --git a/content/signup-page.js b/content/signup-page.js index f536b92..a6d44d8 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -346,6 +346,36 @@ function inspectSignupEntryState() { }; } +function getSignupEntryStateSummary(snapshot = inspectSignupEntryState()) { + const summary = { + state: snapshot?.state || 'unknown', + url: snapshot?.url || location.href, + hasEmailInput: Boolean(snapshot?.emailInput || getSignupEmailInput()), + hasPasswordInput: Boolean(snapshot?.passwordInput || getSignupPasswordInput()), + }; + + if (snapshot?.displayedEmail) { + summary.displayedEmail = snapshot.displayedEmail; + } + + if (snapshot?.signupTrigger) { + summary.signupTrigger = { + tag: (snapshot.signupTrigger.tagName || '').toLowerCase(), + text: getActionText(snapshot.signupTrigger).slice(0, 80), + }; + } + + if (snapshot?.continueButton) { + summary.continueButton = { + tag: (snapshot.continueButton.tagName || '').toLowerCase(), + text: getActionText(snapshot.continueButton).slice(0, 80), + enabled: isActionEnabled(snapshot.continueButton), + }; + } + + return summary; +} + function getSignupEntryDiagnostics() { const actionCandidates = document.querySelectorAll( 'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]' @@ -400,14 +430,24 @@ async function waitForSignupEntryState(options = {}) { const { timeout = 15000, autoOpenEntry = false, + step = 2, + logDiagnostics = false, } = options; const start = Date.now(); let lastTriggerClickAt = 0; + let clickAttempts = 0; + let lastState = ''; + let slowSnapshotLogged = false; while (Date.now() - start < timeout) { throwIfStopped(); const snapshot = inspectSignupEntryState(); + if (logDiagnostics && snapshot.state !== lastState) { + lastState = snapshot.state; + log(`步骤 ${step}:注册入口状态切换为 ${snapshot.state},状态快照:${JSON.stringify(getSignupEntryStateSummary(snapshot))}`); + } + if (snapshot.state === 'password_page' || snapshot.state === 'email_entry') { return snapshot; } @@ -419,16 +459,29 @@ async function waitForSignupEntryState(options = {}) { if (Date.now() - lastTriggerClickAt >= 1500) { lastTriggerClickAt = Date.now(); + clickAttempts += 1; + if (logDiagnostics) { + log(`步骤 ${step}:正在点击官网注册入口(第 ${clickAttempts} 次):"${getActionText(snapshot.signupTrigger).slice(0, 80)}"`); + } log('步骤 2:正在点击官网注册入口...'); await humanPause(350, 900); simulateClick(snapshot.signupTrigger); } } + if (logDiagnostics && !slowSnapshotLogged && Date.now() - start >= 5000) { + slowSnapshotLogged = true; + log(`步骤 ${step}:等待注册入口超过 5 秒,页面诊断快照:${JSON.stringify(getSignupEntryDiagnostics())}`, 'warn'); + } + await sleep(250); } - return inspectSignupEntryState(); + const finalSnapshot = inspectSignupEntryState(); + if (logDiagnostics) { + log(`步骤 ${step}:等待注册入口状态超时,最终状态快照:${JSON.stringify(getSignupEntryStateSummary(finalSnapshot))}`, 'warn'); + } + return finalSnapshot; } async function ensureSignupEntryReady(timeout = 15000) { @@ -471,6 +524,8 @@ async function fillSignupEmailAndContinue(email, step) { const snapshot = await waitForSignupEntryState({ timeout: 20000, autoOpenEntry: true, + step, + logDiagnostics: step === 2, }); if (snapshot.state === 'password_page') { @@ -485,6 +540,9 @@ async function fillSignupEmailAndContinue(email, step) { } if (snapshot.state !== 'email_entry' || !snapshot.emailInput) { + if (step === 2) { + log(`步骤 ${step}:未进入邮箱输入页,最终页面诊断快照:${JSON.stringify(getSignupEntryDiagnostics())}`, 'warn'); + } throw new Error(`步骤 ${step}:未找到可用的邮箱输入入口。URL: ${location.href}`); } diff --git a/tests/background-mail2925-session-module.test.js b/tests/background-mail2925-session-module.test.js index 69f2f90..a00319d 100644 --- a/tests/background-mail2925-session-module.test.js +++ b/tests/background-mail2925-session-module.test.js @@ -92,3 +92,70 @@ test('handleMail2925LimitReachedError disables current account and switches to t assert.equal(currentAccount.lastError, '子邮箱已达上限邮箱'); assert.ok(currentAccount.disabledUntil > Date.now()); }); + +test('handleMail2925LimitReachedError requests stop when no next mail2925 account is available', 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 = { + mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([ + { id: 'only', email: 'only@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 }, + ]), + currentMail2925AccountId: 'only', + }; + const events = { + stopCalls: [], + }; + + 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 () => ({ 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(currentState.currentMail2925AccountId, null); + assert.equal(events.stopCalls.length, 1); + assert.match(events.stopCalls[0].logMessage, /没有可切换的下一个账号/); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index bc5040d..3800b8e 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -527,8 +527,8 @@ 1. 用户在 sidepanel 的 2925 账号池中保存 `email / password` 2. 只有当 sidepanel 中的 `mail2925UseAccountPool` 开关开启时,别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail` 3. 手动点击 `登录` 或自动流程进入 Step 4 / Step 8 前,只有在号池模式开启时,后台才会确保当前 2925 账号已登录网页邮箱 -4. 一旦轮询期间出现“子邮箱已达上限邮箱”,且当前处于号池模式,后台会记录当前时间、把当前账号禁用 24 小时、自动切到下一个账号并重新登录 -5. 切号完成后,后台直接抛出“结束当前尝试”错误;如果 Auto 开启了自动重试,现有控制器会自动进入下一次尝试 +4. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先记录当前时间;若当前处于号池模式且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录 +5. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态 ### 7.5 iCloud