diff --git a/README.md b/README.md index 2099fb0..0b17840 100644 --- a/README.md +++ b/README.md @@ -550,6 +550,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 - 使用自定义密码或自动生成密码 - 在密码页填写密码并提交注册表单 - 后台会在真正把 Step 3 记为完成前,再确认页面是否已经推进;如果此时出现认证页 `重试` 页面,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续后续链路 +- Step 3 收尾阶段如果页面切换导致旧内容脚本失联,后台单次消息等待不会再卡住超过当前收尾预算;若最终仍未恢复,则会输出中文的步骤级错误,而不是直接暴露底层英文通信超时 实际使用的密码会写入会话状态,并同步到侧边栏显示。 diff --git a/background.js b/background.js index bbc85c7..a2e2e93 100644 --- a/background.js +++ b/background.js @@ -6256,6 +6256,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe isGeneratedAliasProvider, isReusableGeneratedAliasEmail, isSignupEmailVerificationPageUrl, + isRetryableContentScriptTransportError, isHotmailProvider, isLuckmailProvider, isSignupPasswordPageUrl, diff --git a/background/signup-flow-helpers.js b/background/signup-flow-helpers.js index 3ba844b..dc64c9b 100644 --- a/background/signup-flow-helpers.js +++ b/background/signup-flow-helpers.js @@ -3,6 +3,7 @@ })(typeof self !== 'undefined' ? self : globalThis, function createSignupFlowHelpersModule() { function createSignupFlowHelpers(deps = {}) { const { + addLog, buildGeneratedAliasEmail, chrome, ensureContentScriptReadyOnTab, @@ -12,6 +13,7 @@ isGeneratedAliasProvider, isReusableGeneratedAliasEmail, isHotmailProvider, + isRetryableContentScriptTransportError = () => false, isLuckmailProvider, isSignupEmailVerificationPageUrl, isSignupPasswordPageUrl, @@ -164,20 +166,32 @@ logMessage: `步骤 ${step}:认证页仍在切换,正在等待页面恢复后继续确认提交流程...`, }); - const result = await sendToContentScriptResilient('signup-page', { - type: 'PREPARE_SIGNUP_VERIFICATION', - step, - source: 'background', - payload: { - password: password || '', - prepareSource: 'step3_finalize', - prepareLogLabel: '步骤 3 收尾', - }, - }, { - timeoutMs: 30000, - retryDelayMs: 700, - logMessage: `步骤 ${step}:密码已提交,正在确认是否进入下一页面,必要时自动恢复重试页...`, - }); + let result; + try { + result = await sendToContentScriptResilient('signup-page', { + type: 'PREPARE_SIGNUP_VERIFICATION', + step, + source: 'background', + payload: { + password: password || '', + prepareSource: 'step3_finalize', + prepareLogLabel: '步骤 3 收尾', + }, + }, { + timeoutMs: 30000, + retryDelayMs: 700, + logMessage: `步骤 ${step}:密码已提交,正在确认是否进入下一页面,必要时自动恢复重试页...`, + }); + } catch (error) { + if (isRetryableContentScriptTransportError(error)) { + const message = `步骤 ${step}:认证页在提交后切换过程中页面通信超时,未能重新就绪,暂时无法确认是否进入下一页面。请重试当前轮。`; + if (typeof addLog === 'function') { + await addLog(message, 'warn'); + } + throw new Error(message); + } + throw error; + } if (result?.error) { throw new Error(result.error); diff --git a/background/tab-runtime.js b/background/tab-runtime.js index 54f6dfc..51619cb 100644 --- a/background/tab-runtime.js +++ b/background/tab-runtime.js @@ -376,6 +376,17 @@ return 30000; } + function resolveResponseTimeoutMs(message, requestedResponseTimeoutMs, remainingTimeoutMs = null) { + const fallbackTimeoutMs = getContentScriptResponseTimeoutMs(message); + const requestedTimeoutMs = Number.isFinite(Number(requestedResponseTimeoutMs)) + ? Math.max(1, Math.floor(Number(requestedResponseTimeoutMs))) + : fallbackTimeoutMs; + if (!Number.isFinite(Number(remainingTimeoutMs))) { + return requestedTimeoutMs; + } + return Math.max(1, Math.min(requestedTimeoutMs, Math.floor(Number(remainingTimeoutMs)))); + } + function getMessageDebugLabel(source, message, tabId = null) { const parts = [source || 'unknown', message?.type || 'UNKNOWN']; if (Number.isInteger(message?.step)) parts.push(`step=${message.step}`); @@ -439,7 +450,13 @@ pendingCommands.delete(source); reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`)); }, timeout); - pendingCommands.set(source, { message, resolve, reject, timer }); + pendingCommands.set(source, { + message, + resolve, + reject, + timer, + responseTimeoutMs: timeout, + }); console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`); }); } @@ -449,7 +466,7 @@ if (pending) { clearTimeout(pending.timer); pendingCommands.delete(source); - sendTabMessageWithTimeout(tabId, source, pending.message).then(pending.resolve).catch(pending.reject); + sendTabMessageWithTimeout(tabId, source, pending.message, pending.responseTimeoutMs).then(pending.resolve).catch(pending.reject); console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`); } } @@ -564,13 +581,13 @@ if (!entry || !entry.ready) { throwIfStopped(); - return queueCommand(source, message); + return queueCommand(source, message, responseTimeoutMs); } const alive = await isTabAlive(source); throwIfStopped(); if (!alive) { - return queueCommand(source, message); + return queueCommand(source, message, responseTimeoutMs); } throwIfStopped(); @@ -592,12 +609,18 @@ while (Date.now() - start < timeoutMs) { throwIfStopped(); attempt += 1; + const remainingTimeoutMs = Math.max(1, timeoutMs - (Date.now() - start)); + const effectiveResponseTimeoutMs = resolveResponseTimeoutMs( + message, + responseTimeoutMs, + remainingTimeoutMs + ); try { return await sendToContentScript( source, message, - responseTimeoutMs !== undefined ? { responseTimeoutMs } : {} + { responseTimeoutMs: effectiveResponseTimeoutMs } ); } catch (err) { const retryable = isRetryableContentScriptTransportError(err); @@ -631,12 +654,18 @@ while (Date.now() - start < timeoutMs) { throwIfStopped(); + const remainingTimeoutMs = Math.max(1, timeoutMs - (Date.now() - start)); + const effectiveResponseTimeoutMs = resolveResponseTimeoutMs( + message, + responseTimeoutMs, + remainingTimeoutMs + ); try { return await sendToContentScript( mail.source, message, - responseTimeoutMs !== undefined ? { responseTimeoutMs } : {} + { responseTimeoutMs: effectiveResponseTimeoutMs } ); } catch (err) { if (!isRetryableContentScriptTransportError(err)) { @@ -684,6 +713,7 @@ queueCommand, registerTab, rememberSourceLastUrl, + resolveResponseTimeoutMs, reuseOrCreateTab, sendTabMessageWithTimeout, sendToContentScript, diff --git a/tests/background-signup-step2-branching.test.js b/tests/background-signup-step2-branching.test.js index 1d86de6..ceba171 100644 --- a/tests/background-signup-step2-branching.test.js +++ b/tests/background-signup-step2-branching.test.js @@ -175,8 +175,12 @@ test('signup flow helper reuses existing managed alias email when it is still co test('signup flow helper finalizes step 3 submit by reusing signup verification preparation', async () => { let ensureCalls = 0; const messages = []; + const logs = []; const helpers = signupFlowApi.createSignupFlowHelpers({ + addLog: async (message, level = 'info') => { + logs.push({ message, level }); + }, buildGeneratedAliasEmail: () => '', chrome: { tabs: { get: async () => ({ id: 31, url: 'https://auth.openai.com/create-account/password' }) } }, ensureContentScriptReadyOnTab: async (...args) => { @@ -188,6 +192,7 @@ test('signup flow helper finalizes step 3 submit by reusing signup verification isGeneratedAliasProvider: () => false, isReusableGeneratedAliasEmail: () => false, isHotmailProvider: () => false, + isRetryableContentScriptTransportError: () => false, isLuckmailProvider: () => false, isSignupEmailVerificationPageUrl: () => false, isSignupPasswordPageUrl: () => true, @@ -206,6 +211,7 @@ test('signup flow helper finalizes step 3 submit by reusing signup verification assert.deepStrictEqual(result, { ready: true, retried: 1 }); assert.equal(ensureCalls, 1); + assert.deepStrictEqual(logs, []); assert.deepStrictEqual(messages.find((item) => item.type === 'send')?.message, { type: 'PREPARE_SIGNUP_VERIFICATION', step: 3, @@ -217,3 +223,45 @@ test('signup flow helper finalizes step 3 submit by reusing signup verification }, }); }); + +test('signup flow helper rewrites retryable step 3 finalize transport timeout into a Chinese error', async () => { + const logs = []; + + const helpers = signupFlowApi.createSignupFlowHelpers({ + addLog: async (message, level = 'info') => { + logs.push({ message, level }); + }, + buildGeneratedAliasEmail: () => '', + chrome: { tabs: { get: async () => ({ id: 31, url: 'https://auth.openai.com/create-account/password' }) } }, + ensureContentScriptReadyOnTab: async () => {}, + ensureHotmailAccountForFlow: async () => ({}), + ensureLuckmailPurchaseForFlow: async () => ({}), + isGeneratedAliasProvider: () => false, + isReusableGeneratedAliasEmail: () => false, + isHotmailProvider: () => false, + isRetryableContentScriptTransportError: (error) => /did not respond in 45s/i.test(error?.message || String(error || '')), + isLuckmailProvider: () => false, + isSignupEmailVerificationPageUrl: () => false, + isSignupPasswordPageUrl: () => true, + reuseOrCreateTab: async () => 31, + sendToContentScriptResilient: async () => { + throw new Error('Content script on signup-page did not respond in 45s. Try refreshing the tab and retry.'); + }, + setEmailState: async () => {}, + SIGNUP_ENTRY_URL: 'https://chatgpt.com/', + SIGNUP_PAGE_INJECT_FILES: ['content/utils.js', 'content/signup-page.js'], + waitForTabUrlMatch: async () => null, + }); + + await assert.rejects( + () => helpers.finalizeSignupPasswordSubmitInTab(31, 'Secret123!', 3), + /步骤 3:认证页在提交后切换过程中页面通信超时,未能重新就绪,暂时无法确认是否进入下一页面。请重试当前轮。/ + ); + + assert.deepStrictEqual(logs, [ + { + message: '步骤 3:认证页在提交后切换过程中页面通信超时,未能重新就绪,暂时无法确认是否进入下一页面。请重试当前轮。', + level: 'warn', + }, + ]); +}); diff --git a/tests/background-tab-runtime-module.test.js b/tests/background-tab-runtime-module.test.js index 2398c5d..9a239cf 100644 --- a/tests/background-tab-runtime-module.test.js +++ b/tests/background-tab-runtime-module.test.js @@ -16,6 +16,41 @@ test('tab runtime module exposes a factory', () => { assert.equal(typeof api?.createTabRuntime, 'function'); }); +test('tab runtime caps per-attempt response timeout to the remaining resilient timeout budget', () => { + const source = fs.readFileSync('background/tab-runtime.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundTabRuntime;`)(globalScope); + + const runtime = api.createTabRuntime({ + LOG_PREFIX: '[test]', + addLog: async () => {}, + chrome: { + tabs: { + get: async () => ({ id: 1, url: 'https://example.com', status: 'complete' }), + query: async () => [], + }, + }, + getSourceLabel: (source) => source || 'unknown', + getState: async () => ({ tabRegistry: {}, sourceLastUrls: {} }), + matchesSourceUrlFamily: () => false, + normalizeLocalCpaStep9Mode: () => 'submit', + parseUrlSafely: () => null, + registerTab: async () => {}, + setState: async () => {}, + shouldBypassStep9ForLocalCpa: () => false, + throwIfStopped: () => {}, + }); + + assert.equal( + runtime.resolveResponseTimeoutMs({ type: 'PREPARE_SIGNUP_VERIFICATION' }, undefined, 30000), + 30000 + ); + assert.equal( + runtime.resolveResponseTimeoutMs({ type: 'PREPARE_SIGNUP_VERIFICATION' }, 12000, 5000), + 5000 + ); +}); + test('tab runtime waitForTabComplete waits until tab status becomes complete', async () => { const source = fs.readFileSync('background/tab-runtime.js', 'utf8'); const globalScope = {}; diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 366d38e..9211912 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -312,6 +312,7 @@ 6. 上报完成后再异步点击提交,避免页面跳转打断响应通道 7. 延迟提交真正触发前会再次检查 Stop 状态,避免用户已停止时页面仍继续自动提交 8. 后台在真正确认 Step 3 完成前,会额外检查提交后是否切换页面;如果出现认证页 `Try again / 重试` 页面,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续后续链路 +9. Step 3 收尾阶段如果页面切换导致旧内容脚本失联,后台会把单次消息等待收口到当前收尾预算内,优先尽快重试重连;若最终仍未恢复,则输出中文的步骤级错误,而不是直接暴露底层英文通信超时 ### Step 4 / Step 8