From 3c0bc41794fa70b2b3669e35dc43445a10998fa0 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Fri, 29 May 2026 21:03:47 +0800 Subject: [PATCH] fix: enhance OAuth session handling and add tests for state resets --- background.js | 95 ++++++++++++++++----- tests/auto-run-step6-restart.test.js | 47 ++++++++++ tests/background-cpa-state-plumbing.test.js | 60 ++++++++++++- 3 files changed, 182 insertions(+), 20 deletions(-) diff --git a/background.js b/background.js index 0ecbd43..149ac5e 100644 --- a/background.js +++ b/background.js @@ -9971,21 +9971,67 @@ function getDownstreamStateResets(step, state = {}) { gpcPageStatus: '', gpcPageStatusText: '', }; + const oauthRuntimeResets = { + oauthUrl: null, + cpaOAuthState: null, + cpaManagementOrigin: null, + sub2apiSessionId: null, + sub2apiOAuthState: null, + sub2apiGroupId: null, + sub2apiGroupIds: [], + sub2apiDraftName: null, + sub2apiProxyId: null, + codex2apiSessionId: null, + codex2apiOAuthState: null, + }; + const getNextStepKey = () => { + const numericStep = Number(step); + const currentNodeId = typeof getNodeIdByStepForState === 'function' + ? getNodeIdByStepForState(numericStep, state) + : ''; + if ( + currentNodeId + && typeof getNodeIdsForState === 'function' + && typeof getStepIdByNodeIdForState === 'function' + ) { + const nodeIds = getNodeIdsForState(state); + const currentIndex = nodeIds.indexOf(currentNodeId); + const nextNodeId = currentIndex >= 0 ? nodeIds[currentIndex + 1] : ''; + const nextStep = nextNodeId ? getStepIdByNodeIdForState(nextNodeId, state) : null; + if (Number.isInteger(nextStep) && nextStep > 0) { + return String(getStepExecutionKeyForState(nextStep, state) || '').trim(); + } + } + if (typeof getStepIdsForState === 'function') { + const stepIds = getStepIdsForState(state); + const currentIndex = stepIds.indexOf(numericStep); + const nextStep = currentIndex >= 0 ? stepIds[currentIndex + 1] : null; + if (Number.isInteger(nextStep) && nextStep > 0) { + return String(getStepExecutionKeyForState(nextStep, state) || '').trim(); + } + } + return ''; + }; + const nextStepKey = getNextStepKey(); + const isOAuthEntryRestartBoundary = nextStepKey === 'oauth-login' || nextStepKey === 'relogin-bound-email'; + const oauthEntryRuntimeResets = { + ...oauthRuntimeResets, + lastLoginCode: null, + loginVerificationRequestedAt: null, + oauthFlowDeadlineAt: null, + oauthFlowDeadlineSourceUrl: null, + pendingPhoneActivationConfirmation: null, + localhostUrl: null, + currentPhoneVerificationCode: '', + currentPhoneVerificationCountdownEndsAt: 0, + currentPhoneVerificationCountdownWindowIndex: 0, + currentPhoneVerificationCountdownWindowTotal: 0, + }; if (step <= 1) { return { ...plusRuntimeResets, - oauthUrl: null, - cpaOAuthState: null, - cpaManagementOrigin: null, - sub2apiSessionId: null, - sub2apiOAuthState: null, - sub2apiGroupId: null, - sub2apiGroupIds: [], - sub2apiDraftName: null, - sub2apiProxyId: null, - codex2apiSessionId: null, - codex2apiOAuthState: null, + ...oauthRuntimeResets, flowStartTime: null, password: null, lastEmailTimestamp: null, @@ -10056,6 +10102,7 @@ function getDownstreamStateResets(step, state = {}) { if (isEarlyRegistrationNode || isBillingNode || isApprovalNode) { return { ...(isEarlyRegistrationNode ? plusRuntimeResets : {}), + ...(isOAuthEntryRestartBoundary ? oauthRuntimeResets : {}), ...(isBillingNode ? { plusBillingCountryText: '', plusBillingAddress: null, @@ -10090,6 +10137,7 @@ function getDownstreamStateResets(step, state = {}) { } if (stepKey === 'plus-checkout-return' || stepKey === 'confirm-oauth') { return { + ...(isOAuthEntryRestartBoundary ? oauthRuntimeResets : {}), pendingPhoneActivationConfirmation: null, plusReturnUrl: '', localhostUrl: null, @@ -10099,12 +10147,10 @@ function getDownstreamStateResets(step, state = {}) { currentPhoneVerificationCountdownWindowTotal: 0, }; } - if ( - stepKey === 'oauth-login' - || stepKey === 'fetch-login-code' - || stepKey === 'relogin-bound-email' - || stepKey === 'fetch-bound-email-login-code' - ) { + if (stepKey === 'oauth-login' || stepKey === 'relogin-bound-email') { + return oauthEntryRuntimeResets; + } + if (stepKey === 'fetch-login-code' || stepKey === 'fetch-bound-email-login-code') { return { lastLoginCode: null, loginVerificationRequestedAt: null, @@ -10124,6 +10170,9 @@ function getDownstreamStateResets(step, state = {}) { localhostUrl: null, }; } + if (isOAuthEntryRestartBoundary) { + return oauthEntryRuntimeResets; + } return {}; } @@ -14988,6 +15037,10 @@ async function getPostStep6AutoRestartDecision(step, error) { const hasTransientTokenExchangeSignal = /token_exchange_user_error|invalid request\.?\s*please try again later/i.test(normalizedMessage); return mentionsTokenExchange && (hasTransientNetworkSignal || hasTransientTokenExchangeSignal); }; + const isPlatformVerifyOAuthSessionExpiredError = (errorMessage = '') => { + const normalizedMessage = String(errorMessage || ''); + return /OPENAI_OAUTH_SESSION_NOT_FOUND|session\s+not\s+found\s+or\s+expired|oauth\s+session\s+(?:not\s+found|expired)|missing\s+SUB2API\s+session_id|缺少\s*SUB2API\s*(?:session_id|会话信息)|SUB2API[\s\S]*(?:会话|session)[\s\S]*(?:过期|失效|不存在|not\s+found|expired)/i.test(normalizedMessage); + }; const isPhoneVerificationLocalFailure = (errorMessage = '') => { const normalizedMessage = String(errorMessage || ''); if (isPhoneSmsPlatformRateLimitFailure(normalizedMessage)) { @@ -15034,11 +15087,15 @@ async function getPostStep6AutoRestartDecision(step, error) { && confirmOauthStep > 0 && confirmOauthStep < normalizedStep && isPlatformVerifyTransientRetryError(errorMessage); + const shouldRestartFromOAuthLoginStep = currentNodeKey === 'platform-verify' + && isPlatformVerifyOAuthSessionExpiredError(errorMessage); const restartAnchorStep = shouldRetryFromConfirmStep ? confirmOauthStep - : (isBoundEmailReloginTailStep && Number.isFinite(boundEmailReloginStep) && boundEmailReloginStep > 0 + : (shouldRestartFromOAuthLoginStep + ? authChainStartStep + : (isBoundEmailReloginTailStep && Number.isFinite(boundEmailReloginStep) && boundEmailReloginStep > 0 ? boundEmailReloginStep - : authChainStartStep); + : authChainStartStep)); if (isPhoneSmsPlatformRateLimitFailure(errorMessage)) { return { shouldRestart: false, diff --git a/tests/auto-run-step6-restart.test.js b/tests/auto-run-step6-restart.test.js index 3208279..861342c 100644 --- a/tests/auto-run-step6-restart.test.js +++ b/tests/auto-run-step6-restart.test.js @@ -159,6 +159,7 @@ const bundle = [ extractFunction('startAutoRunNodeIdleLogWatchdog'), extractFunction('runAutoNodeActionWithIdleLogWatchdog'), extractFunction('executeNodeAndWaitWithAutoRunIdleLogWatchdog'), + extractFunction('getDownstreamStateResets'), extractFunction('getPostStep6AutoRestartDecision'), NODE_COMPAT_HELPERS, extractFunction('getAutoRunWorkflowNodeIds'), @@ -279,6 +280,7 @@ const events = { steps: [], logs: [], invalidations: [], + stateUpdates: [], cancellations: [], stopBroadcasts: 0, }; @@ -330,6 +332,10 @@ async function getTabId() { } async function invalidateDownstreamAfterStepRestart(step, options = {}) { events.invalidations.push({ step, options }); + const resets = getDownstreamStateResets(step, await getState()); + if (Object.keys(resets).length > 0) { + events.stateUpdates.push(resets); + } } function cancelPendingCommands(reason = '') { events.cancellations.push(reason); @@ -576,6 +582,47 @@ test('auto-run restarts from confirm-oauth step after transient step10 token_exc assert.ok(events.logs.some(({ message }) => /回到节点 confirm-oauth 重新开始授权流程/.test(message))); }); +test('auto-run restarts SUB2API expired oauth session from oauth-login and clears stale session state', async () => { + const harness = createHarness({ + failureStep: 10, + failureBudget: 1, + failureMessage: 'session not found or expired', + authState: { state: 'oauth_consent_page', url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent' }, + customState: { + panelMode: 'sub2api', + stepStatuses: { 3: 'completed' }, + stepsVersion: 'ultra2.0', + visibleStep: 10, + sub2apiSessionId: 'expired-session', + sub2apiOAuthState: 'expired-state', + sub2apiGroupId: 5, + sub2apiGroupIds: [5], + sub2apiDraftName: 'draft', + sub2apiProxyId: 7, + accountContributionEnabled: false, + }, + }); + + const events = await harness.run(); + + assert.deepStrictEqual(events.steps, [7, 8, 9, 10, 7, 8, 9, 10]); + assert.equal(events.invalidations.length, 1); + assert.deepStrictEqual(events.invalidations[0], { + step: 6, + options: { + logLabel: '节点 platform-verify 报错后准备回到 oauth-login 重试(第 1 次重开)', + }, + }); + const resetPatch = events.stateUpdates.find((updates) => updates.sub2apiSessionId === null); + assert.ok(resetPatch, 'expected oauth-login restart to clear stale SUB2API OAuth runtime'); + assert.equal(resetPatch.sub2apiOAuthState, null); + assert.equal(resetPatch.sub2apiGroupId, null); + assert.deepStrictEqual(resetPatch.sub2apiGroupIds, []); + assert.equal(resetPatch.sub2apiDraftName, null); + assert.equal(resetPatch.sub2apiProxyId, null); + assert.ok(events.logs.some(({ message }) => /回到节点 oauth-login 重新开始授权流程/.test(message))); +}); + test('auto-run restarts Plus/GPC oauth-login aggregate entry-open failure from step 10', async () => { const plusGpcSteps = { 6: { key: 'plus-checkout-create' }, diff --git a/tests/background-cpa-state-plumbing.test.js b/tests/background-cpa-state-plumbing.test.js index c894bd5..ff3b908 100644 --- a/tests/background-cpa-state-plumbing.test.js +++ b/tests/background-cpa-state-plumbing.test.js @@ -2,6 +2,54 @@ const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); +function extractFunction(source, name) { + const markers = [`async function ${name}(`, `function ${name}(`]; + const start = markers + .map((marker) => source.indexOf(marker)) + .find((index) => index >= 0); + if (start < 0) { + throw new Error(`missing function ${name}`); + } + + let parenDepth = 0; + let signatureEnded = false; + let braceStart = -1; + for (let i = start; i < source.length; i += 1) { + const ch = source[i]; + if (ch === '(') { + parenDepth += 1; + } else if (ch === ')') { + parenDepth -= 1; + if (parenDepth === 0) { + signatureEnded = true; + } + } else if (ch === '{' && signatureEnded) { + braceStart = i; + break; + } + } + + if (braceStart < 0) { + throw new Error(`missing body for function ${name}`); + } + + let depth = 0; + let end = braceStart; + for (; end < source.length; end += 1) { + const ch = source[end]; + if (ch === '{') depth += 1; + if (ch === '}') { + depth -= 1; + if (depth === 0) { + end += 1; + break; + } + } + } + + return source.slice(start, end); +} + test('background step-1 state plumbing persists and resets cpa oauth runtime keys', () => { const source = fs.readFileSync('background.js', 'utf8'); @@ -9,7 +57,17 @@ test('background step-1 state plumbing persists and resets cpa oauth runtime key assert.match(source, /cpaManagementOrigin:\s*null/); assert.match(source, /payload\.cpaOAuthState[^\n]*updates\.cpaOAuthState/); assert.match(source, /payload\.cpaManagementOrigin[^\n]*updates\.cpaManagementOrigin/); - assert.match(source, /if \(step <= 1\) \{[\s\S]*cpaOAuthState:\s*null,[\s\S]*cpaManagementOrigin:\s*null,/); + + const harness = new Function(` +function getStepExecutionKeyForState() { + return ''; +} +${extractFunction(source, 'getDownstreamStateResets')} +return { getDownstreamStateResets }; +`)(); + const resets = harness.getDownstreamStateResets(1, {}); + assert.equal(resets.cpaOAuthState, null); + assert.equal(resets.cpaManagementOrigin, null); }); test('message router step-1 handler stores cpa oauth runtime keys', async () => {