diff --git a/background.js b/background.js index 05e2216..1547774 100644 --- a/background.js +++ b/background.js @@ -6428,6 +6428,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter getPendingAutoRunTimerPlan, getSourceLabel, getState, + getTabId, getStopRequested: () => stopRequested, handleCloudflareSecurityBlocked, handleAutoRunLoopUnhandledError, @@ -6439,6 +6440,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter isLocalhostOAuthCallbackUrl, isLuckmailProvider, isStopError, + isTabAlive, launchAutoRunTimerPlan, listIcloudAliases, listLuckmailPurchasesForManagement, diff --git a/background/message-router.js b/background/message-router.js index b8580fd..c6d675d 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -40,6 +40,7 @@ getPendingAutoRunTimerPlan, getSourceLabel, getState, + getTabId, getStopRequested, handleAutoRunLoopUnhandledError, importSettingsBundle, @@ -50,6 +51,7 @@ isLocalhostOAuthCallbackUrl, isLuckmailProvider, isStopError, + isTabAlive, launchAutoRunTimerPlan, listIcloudAliases, listLuckmailPurchasesForManagement, @@ -108,6 +110,23 @@ return appendAccountRunRecord(status, state, reason); } + async function ensureManualStepPrerequisites(step) { + if (step !== 4) { + return; + } + + const signupTabId = typeof getTabId === 'function' + ? await getTabId('signup-page') + : null; + const signupTabAlive = signupTabId && typeof isTabAlive === 'function' + ? await isTabAlive('signup-page') + : Boolean(signupTabId); + + if (!signupTabId || !signupTabAlive) { + throw new Error('手动执行步骤 4 前,请先执行步骤 1 或步骤 2,确保认证页仍然打开并停留在验证码页。'); + } + } + async function handleStepData(step, payload) { switch (step) { case 1: { @@ -402,6 +421,9 @@ await ensureManualInteractionAllowed('手动执行步骤'); } const step = message.payload.step; + if (message.source === 'sidepanel') { + await ensureManualStepPrerequisites(step); + } if (message.source === 'sidepanel') { await invalidateDownstreamAfterStepRestart(step, { logLabel: `步骤 ${step} 重新执行` }); } diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index 066baee..77dbeaa 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -72,7 +72,7 @@ const signupTabId = await getTabId('signup-page'); if (!signupTabId) { - throw new Error('认证页面标签页已关闭,无法继续步骤 4。'); + throw new Error('认证页面标签页已关闭,无法继续步骤 4。请先执行步骤 1 或步骤 2,重新打开认证页后再试。'); } await chrome.tabs.update(signupTabId, { active: true }); diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js index f66eeee..e4d27eb 100644 --- a/tests/background-message-router-step2-skip.test.js +++ b/tests/background-message-router-step2-skip.test.js @@ -15,6 +15,8 @@ function createRouter(overrides = {}) { notifyCompletions: [], notifyErrors: [], securityBlocks: [], + invalidations: [], + executedSteps: [], }; const router = api.createMessageRouter({ @@ -41,7 +43,9 @@ function createRouter(overrides = {}) { disableUsedLuckmailPurchases: async () => {}, doesStepUseCompletionSignal: () => false, ensureManualInteractionAllowed: async () => ({}), - executeStep: async () => {}, + executeStep: async (step) => { + events.executedSteps.push(step); + }, executeStepViaCompletionSignal: async () => {}, exportSettingsBundle: async () => ({}), fetchGeneratedEmail: async () => '', @@ -55,6 +59,7 @@ function createRouter(overrides = {}) { getPendingAutoRunTimerPlan: () => null, getSourceLabel: () => '', getState: async () => overrides.state || { stepStatuses: { 3: 'pending' } }, + getTabId: overrides.getTabId || (async () => null), getStopRequested: () => false, handleAutoRunLoopUnhandledError: async () => {}, handleCloudflareSecurityBlocked: overrides.handleCloudflareSecurityBlocked || (async (error) => { @@ -63,13 +68,16 @@ function createRouter(overrides = {}) { return message.replace(/^CF_SECURITY_BLOCKED::/, '') || message; }), importSettingsBundle: async () => {}, - invalidateDownstreamAfterStepRestart: async () => {}, + invalidateDownstreamAfterStepRestart: async (step, options) => { + events.invalidations.push({ step, options }); + }, isCloudflareSecurityBlockedError: overrides.isCloudflareSecurityBlockedError || ((error) => /^CF_SECURITY_BLOCKED::/.test(typeof error === 'string' ? error : error?.message || '')), isAutoRunLockedState: () => false, isHotmailProvider: () => false, isLocalhostOAuthCallbackUrl: () => true, isLuckmailProvider: () => false, isStopError: () => false, + isTabAlive: overrides.isTabAlive || (async () => false), launchAutoRunTimerPlan: async () => {}, listIcloudAliases: async () => [], listLuckmailPurchasesForManagement: async () => [], @@ -226,3 +234,22 @@ test('message router stops the flow and surfaces cloudflare security block error error: '您已触发Cloudflare 安全防护系统', }); }); + +test('message router blocks manual step 4 execution when signup page tab is missing', async () => { + const { router, events } = createRouter({ + getTabId: async () => null, + isTabAlive: async () => false, + }); + + await assert.rejects( + () => router.handleMessage({ + type: 'EXECUTE_STEP', + source: 'sidepanel', + payload: { step: 4 }, + }, {}), + /手动执行步骤 4 前,请先执行步骤 1 或步骤 2/ + ); + + assert.deepStrictEqual(events.invalidations, []); + assert.deepStrictEqual(events.executedSteps, []); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 9ab4f83..6c93894 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -333,12 +333,13 @@ - `2925` provider 会关闭 Step 4 / 8 的自动重发间隔 25 秒节流;每次“重新发送验证码”之间,会在邮箱页内部执行一轮固定 15 次的刷新轮询,不再因 OAuth 剩余时间预算而缩短。 - 当 provider 为 `2925` 时,Step 4 会优先直接打开当前 2925 邮箱页,并先比对页面顶部显示的邮箱地址是否与当前目标邮箱一致:如果一致,就直接复用当前已登录页面;如果不一致且启用了 2925 账号池,则会先清理 cookie 再登录当前选中的账号;如果不一致且未启用账号池,则直接复用现有停止逻辑结束流程。Step 8 不再额外承接这套登录态处理。 -- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 当前既不依赖时间窗,也不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中的匹配邮件。 +- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 在 Step 4 / Step 8 会固定使用“步骤开始时间向前回看 10 分钟”的时间窗,不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中落在该固定时间窗内的匹配邮件。 - 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。 - 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。 - `2925` 内容脚本会把每一封实际打开检测的邮件立即删除;同一验证码步骤启动后,试过的验证码会按“步骤 ID + 启动时间”隔离缓存,不会在本次步骤里重复提交;如果再次遇到相同验证码,对应邮件也会在读取后立即删除,避免后续反复打开。 - `2925` 在 provide 模式下仍保持宽松匹配:只要邮件内容命中 ChatGPT / OpenAI 验证码过滤条件,就会尝试该邮件。 - `2925` 在 receive 模式下会恢复“弱目标邮箱匹配”:只有当邮件里显式出现了其他收件邮箱时才会跳过;如果邮件里没有明确写出邮箱,仍允许继续尝试该验证码。 +- 手动点击 Step 4 重新执行时,后台会先检查 `signup-page` 认证页标签是否仍然存在;如果步骤 1 / 2 打开的认证页已经关闭,就会直接提示“请先执行步骤 1 或步骤 2,确保认证页仍然打开并停留在验证码页”,不会先重置后续步骤再报技术错误。 - 当验证码最终提交成功后,后台会异步向 2925 邮箱页发送 `DELETE_ALL_EMAILS`,执行“全选 + 删除”清理剩余邮件,不阻塞主流程。 - 如果 `2925` 邮箱页在轮询期间出现“子邮箱已达上限邮箱”,后台会记录当前时间,把当前 2925 账号禁用 24 小时,自动切到下一个可用账号并完成登录,然后直接报错结束当前尝试;如果 Auto 开启了自动重试,现有控制器会按原逻辑进入下一次尝试。