From 1fcb5c5a29e6424b5384d43407e953d17c012651 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sun, 24 May 2026 21:46:49 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Step=205=20=E5=AF=BC?= =?UTF-8?q?=E8=88=AA=E4=BA=A4=E6=A3=92=E5=90=8E=E7=9A=84=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E6=94=B6=E5=B0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 122 +++++++++++++++--- flows/openai/content/openai-auth.js | 42 +++++- ...d-step5-post-completion-validation.test.js | 56 ++++++++ tests/step5-direct-complete.test.js | 23 +++- 4 files changed, 217 insertions(+), 26 deletions(-) diff --git a/background.js b/background.js index 948783d..02ef4c8 100644 --- a/background.js +++ b/background.js @@ -11028,38 +11028,98 @@ async function executeNodeViaCompletionSignal(nodeId, timeoutMs = 0) { error => ({ ok: false, error }), ); - let executeError = null; - try { - await executeNode(normalizedNodeId, { deferRetryableTransportError: true }); - } catch (err) { - executeError = err; - if (isStopError(err) || !isRetryableContentScriptTransportError(err)) { - notifyNodeError(normalizedNodeId, getErrorMessage(err)); + const executeResultPromise = executeNode(normalizedNodeId, { deferRetryableTransportError: true }).then( + () => ({ ok: true }), + error => ({ ok: false, error }), + ); + + let executeResult = null; + let completionResult = null; + const firstResult = await Promise.race([ + executeResultPromise.then(result => ({ type: 'execute', result })), + completionResultPromise.then(result => ({ type: 'completion', result })), + ]); + + if (firstResult.type === 'completion') { + completionResult = firstResult.result; + if (completionResult.ok) { + executeResultPromise.then((result) => { + if (!result.ok) { + const error = result.error; + if (isStopError(error) || !isRetryableContentScriptTransportError(error)) { + console.warn( + LOG_PREFIX, + `[executeNodeViaCompletionSignal] node ${normalizedNodeId} completed before execute returned, later execute error: ${getErrorMessage(error)}` + ); + } + } + }).catch(() => {}); + return completionResult.payload; } + executeResultPromise.then((result) => { + if (!result.ok) { + const error = result.error; + if (isStopError(error) || !isRetryableContentScriptTransportError(error)) { + console.warn( + LOG_PREFIX, + `[executeNodeViaCompletionSignal] node ${normalizedNodeId} completion waiter failed before execute returned, later execute error: ${getErrorMessage(error)}` + ); + } + } + }).catch(() => {}); + if (/等待超时/.test(getErrorMessage(completionResult.error)) && normalizedNodeId === 'fill-profile') { + const recoveredPayload = await completeStep5FromTabUrlAfterTransportError(completionResult.error).catch(() => null); + if (recoveredPayload) { + notifyNodeComplete(normalizedNodeId, recoveredPayload); + return recoveredPayload; + } + } + throw completionResult.error; + } else { + executeResult = firstResult.result; + if (!executeResult.ok) { + const err = executeResult.error; + if (isStopError(err) || !isRetryableContentScriptTransportError(err)) { + notifyNodeError(normalizedNodeId, getErrorMessage(err)); + } else if (normalizedNodeId === 'fill-profile') { + const recoveredPayload = await completeStep5FromTabUrlAfterTransportError(err).catch(() => null); + if (recoveredPayload) { + notifyNodeComplete(normalizedNodeId, recoveredPayload); + return recoveredPayload; + } + } + } + completionResult = await completionResultPromise; } - const completionResult = await completionResultPromise; if (completionResult.ok) { - if (executeError) { + if (executeResult && !executeResult.ok) { console.warn( LOG_PREFIX, - `[executeNodeViaCompletionSignal] node ${normalizedNodeId} completed after deferred execute error: ${getErrorMessage(executeError)}` + `[executeNodeViaCompletionSignal] node ${normalizedNodeId} completed after deferred execute error: ${getErrorMessage(executeResult.error)}` ); } return completionResult.payload; } - if (executeError && isRetryableContentScriptTransportError(executeError)) { + if (executeResult && !executeResult.ok && isRetryableContentScriptTransportError(executeResult.error)) { const completionMessage = getErrorMessage(completionResult.error); if (/等待超时/.test(completionMessage)) { - await finalizeDeferredNodeExecutionError(normalizedNodeId, executeError); - throw executeError; + if (normalizedNodeId === 'fill-profile') { + const recoveredPayload = await completeStep5FromTabUrlAfterTransportError(executeResult.error).catch(() => null); + if (recoveredPayload) { + notifyNodeComplete(normalizedNodeId, recoveredPayload); + return recoveredPayload; + } + } + await finalizeDeferredNodeExecutionError(normalizedNodeId, executeResult.error); + throw executeResult.error; } throw completionResult.error; } - if (executeError) { - throw executeError; + if (executeResult && !executeResult.ok) { + throw executeResult.error; } throw completionResult.error; @@ -11074,6 +11134,38 @@ async function executeStepViaCompletionSignal(step, timeoutMs = 0) { return executeNodeViaCompletionSignal(nodeId, timeoutMs); } +async function completeStep5FromTabUrlAfterTransportError(sourceError = null) { + const signupTabId = await getTabId('openai-auth').catch(() => null); + if (!Number.isInteger(signupTabId)) { + return null; + } + + const tab = await waitForTabStableComplete(signupTabId, { + timeoutMs: 30000, + retryDelayMs: 300, + stableMs: 1000, + initialDelayMs: 300, + }).catch(() => null); + const currentUrl = String(tab?.url || '').trim(); + if (!currentUrl || !isStep5CompletionChatgptUrl(currentUrl)) { + return null; + } + + const payload = { + profileSubmitted: true, + postSubmitChecked: true, + outcome: 'logged_in_home', + url: currentUrl, + recoveredFromTransportError: true, + }; + await addLog( + `步骤 5 [调试] 内容脚本通信中断,但后台确认标签页已进入 chatgpt.com,按提交成功收尾。原始错误:${getErrorMessage(sourceError)}`, + 'warn', + { step: 5, stepKey: 'fill-profile' } + ); + return payload; +} + function getLatestLogTimestamp(logs = [], fallback = 0) { if (!Array.isArray(logs) || !logs.length) { return Number.isFinite(Number(fallback)) ? Number(fallback) : 0; diff --git a/flows/openai/content/openai-auth.js b/flows/openai/content/openai-auth.js index 2658fd1..cde7b69 100644 --- a/flows/openai/content/openai-auth.js +++ b/flows/openai/content/openai-auth.js @@ -6528,18 +6528,27 @@ function getSerializableRect(el) { // Step 5: Fill Name & Birthday / Age // ============================================================ -function getStep5DirectCompletionPayload({ isAgeMode = false, navigationStarted = false, outcome = null } = {}) { +function getStep5DirectCompletionPayload({ isAgeMode = false, navigationStarted = false, navigationEventType = '', outcome = null } = {}) { const payload = { profileSubmitted: true, - postSubmitChecked: true, + postSubmitChecked: !navigationStarted, }; if (isAgeMode) { payload.ageMode = true; } if (navigationStarted) { payload.navigationStarted = true; + payload.handoffToBackground = true; + const resolvedNavigationEventType = String(navigationEventType || '').trim(); + if (resolvedNavigationEventType) { + payload.navigationEventType = resolvedNavigationEventType; + } + if (typeof location !== 'undefined' && location?.href) { + payload.url = location.href; + } } if (outcome?.state) { + payload.postSubmitChecked = true; payload.outcome = outcome.state; } if (outcome?.url) { @@ -6825,6 +6834,7 @@ function installStep5NavigationCompletionReporter(completeOnce) { if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') { return () => {}; } + let reportedNavigation = false; const debugLog = typeof logStep5SubmitDebug === 'function' ? logStep5SubmitDebug : (message, options = {}) => { @@ -6838,6 +6848,25 @@ function installStep5NavigationCompletionReporter(completeOnce) { const onNavigationStarted = (event) => { const eventType = String(event?.type || 'navigation').trim() || 'navigation'; + if (reportedNavigation) { + return; + } + reportedNavigation = true; + if (typeof completeOnce === 'function') { + try { + completeOnce({ + navigationStarted: true, + navigationEventType: eventType, + }); + } catch (error) { + if (typeof log === 'function') { + log(`步骤 5 [调试] 导航交棒信号发送失败:${error?.message || error}`, 'warn', { + step: 5, + stepKey: 'fill-profile', + }); + } + } + } debugLog(`检测到页面开始导航(event=${eventType})。`, { level: 'warn', }); @@ -7240,13 +7269,18 @@ async function step5_fillNameBirthday(payload) { const completionPayload = getStep5DirectCompletionPayload({ isAgeMode, navigationStarted: Boolean(extra.navigationStarted), + navigationEventType: extra.navigationEventType || '', outcome: extra.outcome || null, }); + reportedCompletionPayload = completionPayload; + if (extra?.navigationStarted && typeof reportNodeComplete === 'function') { + reportNodeComplete('fill-profile', completionPayload); + } else { + reportComplete(5, completionPayload); + } debugLog(`准备发送完成信号(reason=${completionReason},isAgeMode=${isAgeMode})。`, { level: extra?.navigationStarted ? 'warn' : 'info', }); - reportedCompletionPayload = completionPayload; - reportComplete(5, completionPayload); return completionPayload; } diff --git a/tests/background-step5-post-completion-validation.test.js b/tests/background-step5-post-completion-validation.test.js index 690e8a0..bb35654 100644 --- a/tests/background-step5-post-completion-validation.test.js +++ b/tests/background-step5-post-completion-validation.test.js @@ -201,3 +201,59 @@ return { true ); }); + +test('step 5 recovers from bfcache transport close when tab already reached chatgpt', async () => { + const api = new Function(` +const logs = []; +let waitCalls = 0; + +async function getTabId(source) { + return source === 'openai-auth' ? 42 : null; +} + +async function waitForTabStableComplete(tabId) { + waitCalls += 1; + if (tabId !== 42) throw new Error('unexpected tab id'); + return { url: 'https://chatgpt.com/' }; +} + +async function addLog(message, level, meta) { + logs.push({ message, level, meta }); +} + +function getErrorMessage(error) { + return String(error?.message || error || ''); +} + +${extractFunction('parseUrlSafely')} +${extractFunction('isSignupEntryHost')} +${extractFunction('isLikelyLoggedInChatgptHomeUrl')} +${extractFunction('isStep5CompletionChatgptUrl')} +${extractFunction('completeStep5FromTabUrlAfterTransportError')} + +return { + async run() { + return completeStep5FromTabUrlAfterTransportError(new Error('The page keeping the extension port is moved into back/forward cache, so the message channel is closed.')); + }, + snapshot() { + return { logs, waitCalls }; + }, +}; +`)(); + + const result = await api.run(); + const snapshot = api.snapshot(); + + assert.deepStrictEqual(result, { + profileSubmitted: true, + postSubmitChecked: true, + outcome: 'logged_in_home', + url: 'https://chatgpt.com/', + recoveredFromTransportError: true, + }); + assert.equal(snapshot.waitCalls, 1); + assert.equal( + snapshot.logs.some(({ message }) => /通信中断,但后台确认标签页已进入 chatgpt\.com/.test(message)), + true + ); +}); diff --git a/tests/step5-direct-complete.test.js b/tests/step5-direct-complete.test.js index db01ebb..4c34257 100644 --- a/tests/step5-direct-complete.test.js +++ b/tests/step5-direct-complete.test.js @@ -1109,7 +1109,7 @@ return { assert.equal(api.isCompletion('https://chatgpt.com/add-phone'), false); }); -test('step 5 navigation reporter does not complete on beforeunload alone', () => { +test('step 5 navigation reporter hands off to background on beforeunload', () => { const api = new Function(` const events = []; const listeners = new Map(); @@ -1147,22 +1147,31 @@ ${extractFunction('installStep5NavigationCompletionReporter')} return { run() { - let completionCount = 0; - const cleanup = installStep5NavigationCompletionReporter(() => { - completionCount += 1; - events.push({ type: 'complete' }); + const cleanup = installStep5NavigationCompletionReporter((payload) => { + events.push({ type: 'complete', payload }); }); const beforeUnload = listeners.get('beforeunload'); if (beforeUnload) { beforeUnload({ type: 'beforeunload' }); } cleanup(); - return { completionCount, events }; + return { events }; }, }; `)(); const result = api.run(); - assert.equal(result.completionCount, 0); + assert.deepStrictEqual( + result.events.filter((entry) => entry.type === 'complete'), + [ + { + type: 'complete', + payload: { + navigationStarted: true, + navigationEventType: 'beforeunload', + }, + }, + ] + ); assert.equal(result.events.some((entry) => entry.type === 'log' && /检测到页面开始导航/.test(entry.message)), true); });