From fae7b9bffd1358fea41b25e442280d00b6f26ab9 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Thu, 28 May 2026 06:52:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=89=8B=E6=9C=BA=E5=8F=B7?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E7=99=BB=E5=BD=95=E5=AF=86=E7=A0=81=E9=A1=B5?= =?UTF-8?q?=E4=B8=8E=E6=88=90=E5=8A=9F=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 1 + background/auto-run-controller.js | 73 ++++ background/message-router.js | 123 ++++++- flows/openai/content/openai-auth.js | 43 ++- ...phone-signup-success-email-cleanup.test.js | 313 ++++++++++++++++++ ...ckground-message-router-step2-skip.test.js | 173 +++++++++- tests/step3-direct-complete.test.js | 71 ++++ 项目完整链路说明.md | 9 +- 项目文件结构说明.md | 3 +- 9 files changed, 797 insertions(+), 12 deletions(-) create mode 100644 tests/auto-run-phone-signup-success-email-cleanup.test.js diff --git a/background.js b/background.js index 52182be..71fd75b 100644 --- a/background.js +++ b/background.js @@ -12526,6 +12526,7 @@ const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoR getAutoRunStatusPayload, getErrorMessage, getFirstUnfinishedNodeId, + getNodeIdsForState, getPendingAutoRunTimerPlan, getRunningNodeIds, getState, diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index 3fb405c..d75e784 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -19,6 +19,7 @@ getAutoRunStatusPayload, getErrorMessage, getFirstUnfinishedNodeId, + getNodeIdsForState, getPendingAutoRunTimerPlan, getRunningNodeIds, getState, @@ -65,6 +66,77 @@ return false; } + const EMPTY_REGISTRATION_EMAIL_STATE = Object.freeze({ + current: '', + previous: '', + source: '', + updatedAt: 0, + }); + const DONE_NODE_STATUSES = new Set(['completed', 'manual_completed', 'skipped']); + + function isPhoneSignupFlow(state = {}) { + const resolvedSignupMethod = String(state?.resolvedSignupMethod || '').trim().toLowerCase(); + if (resolvedSignupMethod === 'phone' || resolvedSignupMethod === 'email') { + return resolvedSignupMethod === 'phone'; + } + const signupMethod = String(state?.signupMethod || '').trim().toLowerCase(); + return signupMethod === 'phone'; + } + + function getFullWorkflowNodeIds(state = {}) { + if (typeof getNodeIdsForState === 'function') { + const nodeIds = getNodeIdsForState(state); + if (Array.isArray(nodeIds) && nodeIds.length) { + return Array.from(new Set( + nodeIds + .map((nodeId) => String(nodeId || '').trim()) + .filter(Boolean) + )); + } + } + return getKnownNodeIdsFromState(state); + } + + function isFullWorkflowDone(state = {}) { + const nodeIds = getFullWorkflowNodeIds(state); + if (!nodeIds.length) { + return false; + } + const nodeStatuses = state?.nodeStatuses || {}; + return nodeIds.every((nodeId) => DONE_NODE_STATUSES.has(String(nodeStatuses[nodeId] || 'pending').trim())); + } + + async function clearPhoneSignupRuntimeAfterRoundSuccess() { + const currentState = await getState(); + if (!isPhoneSignupFlow(currentState) || !isFullWorkflowDone(currentState)) { + return false; + } + + await setState({ + accountIdentifierType: null, + accountIdentifier: '', + currentPhoneActivation: null, + phoneNumber: '', + signupPhoneNumber: '', + signupPhoneActivation: null, + signupPhoneCompletedActivation: null, + signupPhoneVerificationRequestedAt: null, + signupPhoneVerificationPurpose: '', + currentPhoneVerificationCode: '', + currentPhoneVerificationCountdownEndsAt: 0, + currentPhoneVerificationCountdownWindowIndex: 0, + currentPhoneVerificationCountdownWindowTotal: 0, + email: null, + registrationEmailState: { ...EMPTY_REGISTRATION_EMAIL_STATE }, + step8VerificationTargetEmail: '', + lastEmailTimestamp: null, + lastSignupCode: '', + lastLoginCode: '', + bindEmailSubmitted: false, + }); + return true; + } + async function waitForRunningWorkflowNodesToFinish(payload = {}) { if (typeof waitForRunningNodesToFinish === 'function') { return waitForRunningNodesToFinish(payload); @@ -698,6 +770,7 @@ attemptRuns: attemptRun, continued: useExistingProgress, }); + await clearPhoneSignupRuntimeAfterRoundSuccess(); roundSummary.status = 'success'; roundSummary.finalFailureReason = ''; diff --git a/background/message-router.js b/background/message-router.js index 1fadb68..45b7ed8 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -523,6 +523,109 @@ || status === 'skipped'; } + function isPhoneSignupStepPayload(payload = {}, state = {}) { + const payloadIdentifierType = String(payload?.accountIdentifierType || '').trim().toLowerCase(); + if (payloadIdentifierType === 'email') { + return false; + } + const stateIdentifierType = String(state?.accountIdentifierType || '').trim().toLowerCase(); + return payloadIdentifierType === 'phone' + || Boolean(resolveSignupPhonePayload(payload)) + || stateIdentifierType === 'phone' + || Boolean(String(state?.signupPhoneNumber || '').trim()) + || Boolean(state?.signupPhoneActivation) + || Boolean(state?.signupPhoneCompletedActivation); + } + + function isLoginPasswordPagePayload(payload = {}) { + const mode = String(payload?.passwordPageMode || '').trim().toLowerCase(); + if (mode === 'login') { + return true; + } + const path = String(payload?.passwordPagePath || '').trim(); + if (/\/log-in\/password(?:[/?#]|$)/i.test(path)) { + return true; + } + const rawUrl = String(payload?.passwordPageUrl || '').trim(); + if (!rawUrl) { + return false; + } + try { + return /\/log-in\/password(?:[/?#]|$)/i.test(new URL(rawUrl).pathname || ''); + } catch { + return /\/log-in\/password(?:[/?#]|$)/i.test(rawUrl); + } + } + + function isSignupPasswordPagePayload(payload = {}) { + const mode = String(payload?.passwordPageMode || '').trim().toLowerCase(); + if (mode === 'signup') { + return true; + } + const path = String(payload?.passwordPagePath || '').trim(); + if (/\/(?:create-account|u\/signup|signup)\/password(?:[/?#]|$)/i.test(path)) { + return true; + } + const rawUrl = String(payload?.passwordPageUrl || '').trim(); + if (!rawUrl) { + return false; + } + try { + return /\/(?:create-account|u\/signup|signup)\/password(?:[/?#]|$)/i.test(new URL(rawUrl).pathname || ''); + } catch { + return /\/(?:create-account|u\/signup|signup)\/password(?:[/?#]|$)/i.test(rawUrl); + } + } + + function shouldSkipPhoneSignupRegistrationTailAfterPassword(payload = {}, state = {}) { + if (!isPhoneSignupStepPayload(payload, state) || isSignupPasswordPagePayload(payload) || !isLoginPasswordPagePayload(payload)) { + return false; + } + const nextState = String(payload?.state || payload?.successState || '').trim().toLowerCase(); + return Boolean(payload?.ready) + || Boolean(payload?.alreadyVerified) + || nextState === 'verification' + || nextState === 'verification_page' + || nextState === 'phone_verification_page' + || nextState === 'oauth_consent_page' + || nextState === 'logged_in_home'; + } + + async function skipPhoneSignupRegistrationTailAfterPassword(currentStep, payload = {}) { + const latestState = await getState(); + const skippedSteps = []; + for (const stepKey of ['fetch-signup-code', 'fill-profile', 'wait-registration-success']) { + const skippedStep = findStepByKeyAfter(currentStep, stepKey, latestState); + if (!skippedStep) { + continue; + } + const status = getNodeStatusByStep(skippedStep, latestState); + if (isStepProtectedFromAutoSkip(status)) { + continue; + } + await setNodeStatusByStep(skippedStep, 'skipped', latestState); + skippedSteps.push(skippedStep); + } + + await setState({ signupVerificationRequestedAt: null }); + if (skippedSteps.length) { + await addLog( + `步骤 ${currentStep}:手机号密码提交后已确认账号进入登录后续状态,已自动跳过步骤 ${skippedSteps.join('/')},流程将直接进入 OAuth 后续节点。`, + 'warn', + { step: currentStep, stepKey: 'fill-password' } + ); + } + payload.signupVerificationRequestedAt = null; + payload.passwordLoginFlow = true; + payload.skipRegistrationFlow = true; + return { + ...payload, + signupVerificationRequestedAt: null, + passwordLoginFlow: true, + skipRegistrationFlow: true, + }; + } + function findStepByKeyAfter(currentOrder, targetKey, state = {}) { const activeStepIds = typeof getStepIdsForState === 'function' ? getStepIdsForState(state) @@ -847,6 +950,13 @@ break; case 3: await syncStepAccountIdentityFromPayload(payload); + { + const latestState = await getState(); + if (shouldSkipPhoneSignupRegistrationTailAfterPassword(payload, latestState)) { + await skipPhoneSignupRegistrationTailAfterPassword(step, payload); + break; + } + } if (payload.signupVerificationRequestedAt) { await setState({ signupVerificationRequestedAt: payload.signupVerificationRequestedAt }); } @@ -967,9 +1077,16 @@ notifyNodeError(nodeId, '流程已被用户停止。'); return { ok: true }; } + let completionPayload = message.payload || {}; try { if (nodeId === 'fill-password' && typeof finalizeStep3Completion === 'function') { - await finalizeStep3Completion(message.payload || {}); + const finalizedPayload = await finalizeStep3Completion(message.payload || {}); + if (finalizedPayload && typeof finalizedPayload === 'object') { + completionPayload = { + ...completionPayload, + ...finalizedPayload, + }; + } } } catch (error) { if (typeof isCloudflareSecurityBlockedError === 'function' && isCloudflareSecurityBlockedError(error)) { @@ -1004,11 +1121,11 @@ stepKey: nodeId, }); } - await handleStepData(resolvedStep, message.payload); + await handleStepData(resolvedStep, completionPayload); if (isFinalNode && typeof appendAccountRunRecord === 'function') { await appendAccountRunRecord('success', completionState); } - notifyNodeComplete(nodeId, message.payload); + notifyNodeComplete(nodeId, completionPayload); return { ok: true }; } diff --git a/flows/openai/content/openai-auth.js b/flows/openai/content/openai-auth.js index 8479387..409ba62 100644 --- a/flows/openai/content/openai-auth.js +++ b/flows/openai/content/openai-auth.js @@ -2645,6 +2645,19 @@ async function step2_clickRegister(payload = {}) { // ============================================================ async function step3_fillEmailPassword(payload) { + const resolvePasswordPageInfo = (rawUrl = '') => { + const url = String(rawUrl || '').trim() || String(location.href || ''); + let path = String(location.pathname || '').trim(); + try { + path = new URL(url, 'https://auth.openai.com').pathname || path; + } catch { + // Keep the current pathname fallback; this only tags the completion payload. + } + const mode = /\/log-in\/password(?:[/?#]|$)/i.test(path) + ? 'login' + : (/\/(?:create-account|u\/signup|signup)\/password(?:[/?#]|$)/i.test(path) ? 'signup' : ''); + return { url, path, mode }; + }; const performOperationWithDelay = typeof getOperationDelayRunner === 'function' ? getOperationDelayRunner() : async (metadata, operation) => { @@ -2714,6 +2727,7 @@ async function step3_fillEmailPassword(payload) { throw new Error(`当前密码页邮箱为 ${snapshot.displayedEmail},与目标邮箱 ${email} 不一致,请先回到步骤 1 重新开始。`); } + const passwordPageInfo = resolvePasswordPageInfo(snapshot.url || location.href); await humanPause(600, 1500); await performOperationWithDelay({ stepKey: 'fill-password', kind: 'fill', label: 'signup-password' }, async () => { fillInput(snapshot.passwordInput, password); @@ -2730,7 +2744,8 @@ async function step3_fillEmailPassword(payload) { logSignupPasswordDiagnostics('步骤 3:当前密码页同时存在一次性验证码入口', 'info'); } - const signupVerificationRequestedAt = submitBtn ? Date.now() : null; + const isLoginPasswordPage = passwordPageInfo.mode === 'login'; + const signupVerificationRequestedAt = submitBtn && !isLoginPasswordPage ? Date.now() : null; const completionPayload = { email, phoneNumber: String(payload?.phoneNumber || '').trim(), @@ -2738,6 +2753,10 @@ async function step3_fillEmailPassword(payload) { accountIdentifier, signupVerificationRequestedAt, deferredSubmit: Boolean(submitBtn), + passwordPageUrl: passwordPageInfo.url, + passwordPagePath: passwordPageInfo.path, + passwordPageMode: passwordPageInfo.mode, + ...(isLoginPasswordPage ? { passwordLoginFlow: true } : {}), }; reportComplete(3, completionPayload); @@ -4893,6 +4912,13 @@ function inspectSignupVerificationState() { }; } + if (typeof isOAuthConsentPage === 'function' && isOAuthConsentPage()) { + return { + state: 'oauth_consent_page', + url: location.href, + }; + } + if (isSignupPasswordErrorPage()) { const timeoutPage = getSignupPasswordTimeoutErrorPageState(); return { @@ -4954,6 +4980,7 @@ async function waitForSignupVerificationTransition(timeout = 5000) { if ( snapshot.state === 'step5' || snapshot.state === 'logged_in_home' + || snapshot.state === 'oauth_consent_page' || snapshot.state === 'verification' || snapshot.state === 'contact_verification_server_error' || snapshot.state === 'error' @@ -5051,6 +5078,20 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { }; } + if (snapshot.state === 'oauth_consent_page') { + log(`${prepareLogLabel}:页面已直接进入 OAuth 授权页,本步骤按已完成处理。`, 'ok'); + return { + ready: true, + alreadyVerified: true, + state: 'oauth_consent_page', + skipLoginVerificationStep: true, + directOAuthConsentPage: true, + skipProfileStep: true, + retried: recoveryRound, + prepareSource, + }; + } + if (snapshot.state === 'verification') { await waitForDocumentLoadComplete(15000, `${prepareLogLabel}:注册验证码页面`); await waitForVerificationCodeTarget(15000); diff --git a/tests/auto-run-phone-signup-success-email-cleanup.test.js b/tests/auto-run-phone-signup-success-email-cleanup.test.js new file mode 100644 index 0000000..ce8a330 --- /dev/null +++ b/tests/auto-run-phone-signup-success-email-cleanup.test.js @@ -0,0 +1,313 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +function loadAutoRunControllerApi() { + const source = fs.readFileSync('background/auto-run-controller.js', 'utf8'); + const globalScope = {}; + return new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope); +} + +const FULL_NODE_IDS = [ + 'open-chatgpt', + 'submit-signup-email', + 'fill-password', + 'fetch-signup-code', + 'fill-profile', + 'wait-registration-success', + 'oauth-login', + 'fetch-login-code', + 'confirm-oauth', + 'platform-verify', +]; + +const EMPTY_REGISTRATION_EMAIL_STATE = { + current: '', + previous: '', + source: '', + updatedAt: 0, +}; + +const PHONE_NUMBER = '+6612345'; +const PHONE_ACTIVATION = { + activationId: 'signup-completed', + phoneNumber: PHONE_NUMBER, +}; + +function createNodeStatuses(doneNodeIds = []) { + const doneSet = new Set(doneNodeIds); + return Object.fromEntries(FULL_NODE_IDS.map((nodeId) => [ + nodeId, + doneSet.has(nodeId) ? 'completed' : 'pending', + ])); +} + +function createBaseState() { + return { + activeFlowId: 'openai', + flowId: 'openai', + signupMethod: 'phone', + resolvedSignupMethod: 'phone', + autoRunFallbackThreadIntervalMinutes: 0, + autoRunSkipFailures: false, + nodeStatuses: createNodeStatuses([]), + stepStatuses: {}, + }; +} + +function createHarness({ completedNodeIds = [], initialState = {} } = {}) { + const api = loadAutoRunControllerApi(); + let currentState = { + ...createBaseState(), + ...initialState, + }; + let runCalls = 0; + let sessionSeed = 1000; + const clone = (value) => JSON.parse(JSON.stringify(value)); + + async function getState() { + return clone(currentState); + } + + async function setState(updates = {}) { + currentState = { + ...currentState, + ...updates, + nodeStatuses: updates.nodeStatuses ? { ...updates.nodeStatuses } : currentState.nodeStatuses, + stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses, + }; + } + + async function resetState() { + currentState = { + activeFlowId: currentState.activeFlowId, + flowId: currentState.flowId, + signupMethod: currentState.signupMethod, + resolvedSignupMethod: currentState.resolvedSignupMethod, + autoRunFallbackThreadIntervalMinutes: currentState.autoRunFallbackThreadIntervalMinutes, + autoRunSkipFailures: currentState.autoRunSkipFailures, + nodeStatuses: createNodeStatuses([]), + stepStatuses: {}, + currentPhoneActivation: currentState.currentPhoneActivation, + phoneNumber: currentState.phoneNumber, + accountIdentifierType: currentState.accountIdentifierType, + accountIdentifier: currentState.accountIdentifier, + signupPhoneNumber: currentState.signupPhoneNumber, + signupPhoneActivation: currentState.signupPhoneActivation, + signupPhoneCompletedActivation: currentState.signupPhoneCompletedActivation, + signupPhoneVerificationRequestedAt: currentState.signupPhoneVerificationRequestedAt, + signupPhoneVerificationPurpose: currentState.signupPhoneVerificationPurpose, + currentPhoneVerificationCode: currentState.currentPhoneVerificationCode, + currentPhoneVerificationCountdownEndsAt: currentState.currentPhoneVerificationCountdownEndsAt, + currentPhoneVerificationCountdownWindowIndex: currentState.currentPhoneVerificationCountdownWindowIndex, + currentPhoneVerificationCountdownWindowTotal: currentState.currentPhoneVerificationCountdownWindowTotal, + email: currentState.email, + registrationEmailState: currentState.registrationEmailState, + step8VerificationTargetEmail: currentState.step8VerificationTargetEmail, + lastEmailTimestamp: currentState.lastEmailTimestamp, + lastSignupCode: currentState.lastSignupCode, + lastLoginCode: currentState.lastLoginCode, + bindEmailSubmitted: currentState.bindEmailSubmitted, + }; + } + + async function runAutoSequenceFromNode() { + runCalls += 1; + if (runCalls === 2) { + assert.equal(currentState.email, null); + assert.equal(currentState.currentPhoneActivation, null); + assert.equal(currentState.phoneNumber, ''); + assert.equal(currentState.signupPhoneNumber, ''); + assert.equal(currentState.accountIdentifierType, null); + assert.equal(currentState.accountIdentifier, ''); + } + + await setState({ + accountIdentifierType: 'phone', + accountIdentifier: PHONE_NUMBER, + currentPhoneActivation: PHONE_ACTIVATION, + phoneNumber: PHONE_NUMBER, + signupPhoneNumber: PHONE_NUMBER, + signupPhoneActivation: PHONE_ACTIVATION, + signupPhoneCompletedActivation: PHONE_ACTIVATION, + signupPhoneVerificationRequestedAt: 123, + signupPhoneVerificationPurpose: 'signup', + currentPhoneVerificationCode: '222222', + currentPhoneVerificationCountdownEndsAt: Date.now() + 60000, + currentPhoneVerificationCountdownWindowIndex: 1, + currentPhoneVerificationCountdownWindowTotal: 2, + email: 'bound.user@example.com', + registrationEmailState: { + current: 'bound.user@example.com', + previous: 'old.bound@example.com', + source: 'bind_email', + updatedAt: 123, + }, + step8VerificationTargetEmail: 'bound.user@example.com', + lastEmailTimestamp: 456, + lastSignupCode: '111111', + lastLoginCode: '222222', + bindEmailSubmitted: true, + nodeStatuses: createNodeStatuses(completedNodeIds), + }); + } + + const runtime = { + state: {}, + get() { + return { ...this.state }; + }, + set(updates = {}) { + this.state = { ...this.state, ...updates }; + }, + }; + + const controller = api.createAutoRunController({ + addLog: async () => {}, + AUTO_RUN_MAX_RETRIES_PER_ROUND: 3, + AUTO_RUN_RETRY_DELAY_MS: 3000, + AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry', + AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds', + broadcastAutoRunStatus: async () => {}, + broadcastStopToContentScripts: async () => {}, + cancelPendingCommands: () => {}, + clearStopRequest: () => {}, + createAutoRunSessionId: () => { + sessionSeed += 1; + return sessionSeed; + }, + getAutoRunStatusPayload: () => ({}), + getErrorMessage: (error) => error?.message || String(error || ''), + getFirstUnfinishedNodeId: () => 'open-chatgpt', + getNodeIdsForState: () => FULL_NODE_IDS.slice(), + getPendingAutoRunTimerPlan: () => null, + getRunningNodeIds: () => [], + getState, + getStopRequested: () => false, + hasSavedNodeProgress: () => false, + isRestartCurrentAttemptError: () => false, + isStopError: () => false, + launchAutoRunTimerPlan: async () => false, + normalizeAutoRunFallbackThreadIntervalMinutes: () => 0, + persistAutoRunTimerPlan: async () => {}, + resetState, + runAutoSequenceFromNode, + runtime, + setState, + sleepWithStop: async () => {}, + throwIfAutoRunSessionStopped: () => {}, + waitForRunningNodesToFinish: getState, + chrome: { + runtime: { + sendMessage: () => Promise.resolve(), + }, + }, + }); + + return { + controller, + getState: () => currentState, + }; +} + +test('auto-run clears phone signup and bound email runtime only after full workflow success', async () => { + const { controller, getState } = createHarness({ completedNodeIds: FULL_NODE_IDS }); + + await controller.autoRunLoop(1, { mode: 'restart', autoRunSkipFailures: false }); + + const state = getState(); + assert.equal(state.email, null); + assert.deepEqual(state.registrationEmailState, EMPTY_REGISTRATION_EMAIL_STATE); + assert.equal(state.step8VerificationTargetEmail, ''); + assert.equal(state.lastEmailTimestamp, null); + assert.equal(state.lastSignupCode, ''); + assert.equal(state.lastLoginCode, ''); + assert.equal(state.bindEmailSubmitted, false); + assert.equal(state.currentPhoneActivation, null); + assert.equal(state.currentPhoneVerificationCode, ''); + assert.equal(state.currentPhoneVerificationCountdownEndsAt, 0); + assert.equal(state.currentPhoneVerificationCountdownWindowIndex, 0); + assert.equal(state.currentPhoneVerificationCountdownWindowTotal, 0); + assert.equal(state.accountIdentifierType, null); + assert.equal(state.accountIdentifier, ''); + assert.equal(state.phoneNumber, ''); + assert.equal(state.signupPhoneNumber, ''); + assert.equal(state.signupPhoneActivation, null); + assert.equal(state.signupPhoneCompletedActivation, null); +}); + +test('auto-run keeps phone signup and bound email runtime when workflow is only partially done', async () => { + const { controller, getState } = createHarness({ + completedNodeIds: ['open-chatgpt', 'submit-signup-email', 'fill-password'], + }); + + await controller.autoRunLoop(1, { mode: 'restart', autoRunSkipFailures: false }); + + const state = getState(); + assert.equal(state.email, 'bound.user@example.com'); + assert.deepEqual(state.registrationEmailState, { + current: 'bound.user@example.com', + previous: 'old.bound@example.com', + source: 'bind_email', + updatedAt: 123, + }); + assert.equal(state.step8VerificationTargetEmail, 'bound.user@example.com'); + assert.equal(state.lastEmailTimestamp, 456); + assert.equal(state.lastSignupCode, '111111'); + assert.equal(state.lastLoginCode, '222222'); + assert.equal(state.bindEmailSubmitted, true); + assert.deepEqual(state.currentPhoneActivation, PHONE_ACTIVATION); + assert.equal(state.currentPhoneVerificationCode, '222222'); + assert.equal(state.accountIdentifierType, 'phone'); + assert.equal(state.accountIdentifier, PHONE_NUMBER); + assert.equal(state.phoneNumber, PHONE_NUMBER); + assert.equal(state.signupPhoneNumber, PHONE_NUMBER); + assert.deepEqual(state.signupPhoneActivation, PHONE_ACTIVATION); + assert.deepEqual(state.signupPhoneCompletedActivation, PHONE_ACTIVATION); +}); + +test('auto-run cleanup prevents next phone signup run from reusing previous identity', async () => { + const { controller, getState } = createHarness({ completedNodeIds: FULL_NODE_IDS }); + + await controller.autoRunLoop(2, { mode: 'restart', autoRunSkipFailures: false }); + + const state = getState(); + assert.equal(state.email, null); + assert.equal(state.currentPhoneActivation, null); + assert.equal(state.phoneNumber, ''); + assert.equal(state.signupPhoneNumber, ''); + assert.equal(state.accountIdentifierType, null); + assert.equal(state.accountIdentifier, ''); + assert.equal(state.signupPhoneActivation, null); + assert.equal(state.signupPhoneCompletedActivation, null); + assert.equal(state.bindEmailSubmitted, false); +}); + +test('auto-run cleanup uses frozen resolved signup method instead of stale setting', async () => { + const emailRun = createHarness({ + completedNodeIds: FULL_NODE_IDS, + initialState: { + signupMethod: 'phone', + resolvedSignupMethod: 'email', + }, + }); + + await emailRun.controller.autoRunLoop(1, { mode: 'restart', autoRunSkipFailures: false }); + + assert.equal(emailRun.getState().email, 'bound.user@example.com'); + assert.equal(emailRun.getState().accountIdentifierType, 'phone'); + + const phoneRun = createHarness({ + completedNodeIds: FULL_NODE_IDS, + initialState: { + signupMethod: 'email', + resolvedSignupMethod: 'phone', + }, + }); + + await phoneRun.controller.autoRunLoop(1, { mode: 'restart', autoRunSkipFailures: false }); + + assert.equal(phoneRun.getState().email, null); + assert.equal(phoneRun.getState().accountIdentifierType, null); + assert.equal(phoneRun.getState().signupPhoneNumber, ''); +}); diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js index c969047..4bf69bf 100644 --- a/tests/background-message-router-step2-skip.test.js +++ b/tests/background-message-router-step2-skip.test.js @@ -87,6 +87,7 @@ function createRouter(overrides = {}) { } return next; }; + let currentState = normalizeState(overrides.state || { nodeStatuses: { 'fill-password': 'pending' } }); const router = api.createMessageRouter({ addLog: async (message, level, options = {}) => { @@ -138,13 +139,13 @@ function createRouter(overrides = {}) { getCurrentLuckmailPurchase: () => null, getPendingAutoRunTimerPlan: () => null, getSourceLabel: () => '', - getState: async () => normalizeState(overrides.state || { nodeStatuses: { 'fill-password': 'pending' } }), + getState: async () => normalizeState(currentState), getNodeIdsForState: overrides.getNodeIdsForState || (() => ['open-chatgpt', 'submit-signup-email', 'fill-password', 'fetch-signup-code', 'fill-profile', 'wait-registration-success', 'oauth-login', 'fetch-login-code', 'confirm-oauth', 'platform-verify']), getStepIdByNodeIdForState: overrides.getStepIdByNodeIdForState || ((nodeId, state = {}) => (stepByNode && Object.prototype.hasOwnProperty.call(stepByNode, nodeId)) ? stepByNode[nodeId] || 0 : (state.plusModeEnabled ? plusStepByNode : normalStepByNode)[nodeId] || 0), getStepDefinitionForState: overrides.getStepDefinitionForState, - getStepIdsForState: overrides.getStepIdsForState, + getStepIdsForState: overrides.getStepIdsForState || (() => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), getLastStepIdForState: overrides.getLastStepIdForState, getTabId: overrides.getTabId || (async () => null), getStopRequested: () => false, @@ -204,11 +205,24 @@ function createRouter(overrides = {}) { setPersistentSettings: async () => {}, setState: async (updates) => { events.stateUpdates.push(updates); + currentState = normalizeState({ + ...currentState, + ...updates, + nodeStatuses: updates.nodeStatuses ? { ...updates.nodeStatuses } : currentState.nodeStatuses, + stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses, + }); }, setNodeStatus: async (nodeId, status) => { events.nodeStatuses.push({ nodeId, status }); const step = getStepForNode(nodeId); events.stepStatuses.push({ step, status }); + currentState = normalizeState({ + ...currentState, + nodeStatuses: { + ...(currentState.nodeStatuses || {}), + [nodeId]: status, + }, + }); }, skipAutoRunCountdown: async () => false, skipNode: async () => {}, @@ -223,7 +237,7 @@ function createRouter(overrides = {}) { }), }); - return { router, events }; + return { router, events, getState: () => normalizeState(currentState) }; } test('message router skips step 3 when step 2 lands on verification page', async () => { @@ -574,6 +588,159 @@ test('message router marks step 3 failed when post-submit finalize fails', async assert.deepStrictEqual(response, { ok: true, error: '步骤 3 提交后仍停留在密码页。' }); }); +test('message router skips signup tail after finalize confirms phone login password page advanced', async () => { + const finalizeResult = { ready: true, state: 'phone_verification_page' }; + const { router, events } = createRouter({ + state: { + signupMethod: 'phone', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + nodeStatuses: { + 'fill-password': 'running', + 'fetch-signup-code': 'pending', + 'fill-profile': 'pending', + 'wait-registration-success': 'pending', + }, + }, + finalizeStep3Completion: async (payload) => { + events.finalizePayloads.push(payload); + return finalizeResult; + }, + }); + + const response = await router.handleMessage({ + type: 'NODE_COMPLETE', + nodeId: 'fill-password', + source: 'openai-auth', + payload: { + nodeId: 'fill-password', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + signupVerificationRequestedAt: 123456, + deferredSubmit: true, + passwordPageUrl: 'https://auth.openai.com/log-in/password', + passwordPagePath: '/log-in/password', + passwordPageMode: 'login', + }, + }, {}); + + assert.deepStrictEqual(response, { ok: true }); + assert.deepStrictEqual(events.finalizePayloads.map((payload) => payload.passwordPageMode), ['login']); + assert.deepStrictEqual(events.stepStatuses, [ + { step: 3, status: 'completed' }, + { step: 4, status: 'skipped' }, + { step: 5, status: 'skipped' }, + { step: 6, status: 'skipped' }, + ]); + assert.equal(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === 123456), false); + assert.equal(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === null), true); + assert.equal(events.logs.some(({ message }) => /手机号密码提交后已确认账号进入登录后续状态/.test(message)), true); + assert.equal(events.notifyCompletions[0].payload.passwordLoginFlow, true); + assert.equal(events.notifyCompletions[0].payload.skipRegistrationFlow, true); + assert.equal(events.notifyCompletions[0].payload.signupVerificationRequestedAt, null); + assert.equal(events.notifyCompletions[0].payload.state, 'phone_verification_page'); +}); + +test('message router keeps signup tail when phone password submit used create-account page', async () => { + const finalizeResult = { ready: true, state: 'verification' }; + const { router, events } = createRouter({ + state: { + signupMethod: 'phone', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + nodeStatuses: { + 'fill-password': 'running', + 'fetch-signup-code': 'pending', + 'fill-profile': 'pending', + 'wait-registration-success': 'pending', + }, + }, + finalizeStep3Completion: async (payload) => { + events.finalizePayloads.push(payload); + return finalizeResult; + }, + }); + + const response = await router.handleMessage({ + type: 'NODE_COMPLETE', + nodeId: 'fill-password', + source: 'openai-auth', + payload: { + nodeId: 'fill-password', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + signupVerificationRequestedAt: 123456, + deferredSubmit: true, + passwordPageUrl: 'https://auth.openai.com/create-account/password', + passwordPagePath: '/create-account/password', + passwordPageMode: 'signup', + }, + }, {}); + + assert.deepStrictEqual(response, { ok: true }); + assert.deepStrictEqual(events.finalizePayloads.map((payload) => payload.passwordPageMode), ['signup']); + assert.deepStrictEqual(events.stepStatuses, [ + { step: 3, status: 'completed' }, + ]); + assert.equal(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === 123456), true); + assert.equal(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === null), false); + assert.equal(events.notifyCompletions[0].payload.state, 'verification'); + assert.equal(events.notifyCompletions[0].payload.skipRegistrationFlow, undefined); +}); + +test('message router does not skip signup tail when login password finalize fails', async () => { + const { router, events } = createRouter({ + state: { + signupMethod: 'phone', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + nodeStatuses: { + 'fill-password': 'running', + 'fetch-signup-code': 'pending', + 'fill-profile': 'pending', + 'wait-registration-success': 'pending', + }, + }, + finalizeStep3Completion: async (payload) => { + events.finalizePayloads.push(payload); + throw new Error('步骤 3 收尾仍停留在登录密码页。'); + }, + }); + + const response = await router.handleMessage({ + type: 'NODE_COMPLETE', + nodeId: 'fill-password', + source: 'openai-auth', + payload: { + nodeId: 'fill-password', + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + signupVerificationRequestedAt: null, + deferredSubmit: true, + passwordPageUrl: 'https://auth.openai.com/log-in/password', + passwordPagePath: '/log-in/password', + passwordPageMode: 'login', + passwordLoginFlow: true, + }, + }, {}); + + assert.deepStrictEqual(response, { ok: true, error: '步骤 3 收尾仍停留在登录密码页。' }); + assert.deepStrictEqual(events.finalizePayloads.map((payload) => payload.passwordPageMode), ['login']); + assert.deepStrictEqual(events.stepStatuses, [ + { step: 3, status: 'failed' }, + ]); + assert.equal(events.stepStatuses.some(({ step, status }) => step === 4 && status === 'skipped'), false); + assert.equal(events.stepStatuses.some(({ step, status }) => step === 5 && status === 'skipped'), false); + assert.equal(events.stepStatuses.some(({ step, status }) => step === 6 && status === 'skipped'), false); + assert.equal(events.notifyCompletions.length, 0); +}); + test('message router does not duplicate step 3 mismatch failure log after finalize already failed', async () => { const mismatchError = 'SIGNUP_PHONE_PASSWORD_MISMATCH::步骤 3:检测到注册手机号或密码不正确,需要重新开始当前轮。页面提示:Incorrect phone number or password'; const state = { diff --git a/tests/step3-direct-complete.test.js b/tests/step3-direct-complete.test.js index aa2290e..3416502 100644 --- a/tests/step3-direct-complete.test.js +++ b/tests/step3-direct-complete.test.js @@ -211,6 +211,9 @@ return { assert.deepStrictEqual(result, beforeSubmit.completions[0].payload); assert.equal(result.email, 'user@example.com'); assert.equal(result.deferredSubmit, true); + assert.equal(result.passwordPageUrl, 'https://auth.openai.com/create-account/password'); + assert.equal(result.passwordPagePath, '/create-account/password'); + assert.equal(result.passwordPageMode, 'signup'); assert.equal(typeof result.signupVerificationRequestedAt, 'number'); assert.equal(beforeSubmit.events.includes('report:true'), true); assert.equal(beforeSubmit.events.includes('operation:submit-signup-password:start'), false); @@ -233,3 +236,71 @@ return { assert.deepStrictEqual(afterSubmit.clicks, ['Continue']); assert.equal(afterSubmit.events.includes('delay:submit-signup-password:2000'), true); }); + +test('step 3 marks login password page from log-in URL', async () => { + const api = new Function(` +const completions = []; +const scheduled = []; +const snapshot = { + state: 'password_page', + passwordInput: { value: '', hidden: false }, + submitButton: { textContent: 'Continue', hidden: false }, + displayedEmail: '', + url: 'https://auth.openai.com/log-in/password', +}; +const window = { + setTimeout(fn, ms) { + scheduled.push({ fn, ms }); + return scheduled.length; + }, + CodexOperationDelay: { + async performOperationWithDelay(metadata, operation) { + return operation(); + }, + }, +}; +const location = { + href: 'https://auth.openai.com/log-in/password', + pathname: '/log-in/password', +}; +function inspectSignupEntryState() { return snapshot; } +function ensureSignupPasswordPageReady() { return { ready: true }; } +function getSignupPasswordSubmitButton() { return snapshot.submitButton; } +async function waitForElementByText() { return null; } +function fillInput(input, value) { input.value = value; } +async function humanPause() {} +async function sleep() {} +function throwIfStopped() {} +function isStopError() { return false; } +function log() {} +function logSignupPasswordDiagnostics() {} +function reportComplete(step, payload) { completions.push({ step, payload }); } +function simulateClick() {} +function getOperationDelayRunner() { return window.CodexOperationDelay.performOperationWithDelay; } + +${extractFunction('step3_fillEmailPassword')} + +return { + run() { + return step3_fillEmailPassword({ + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + phoneNumber: '+66959916439', + password: 'Secret123!', + }); + }, + completions, +}; +`)(); + + const result = await api.run(); + + assert.equal(result.accountIdentifierType, 'phone'); + assert.equal(result.accountIdentifier, '+66959916439'); + assert.equal(result.passwordPageUrl, 'https://auth.openai.com/log-in/password'); + assert.equal(result.passwordPagePath, '/log-in/password'); + assert.equal(result.passwordPageMode, 'login'); + assert.equal(result.passwordLoginFlow, true); + assert.equal(result.signupVerificationRequestedAt, null); + assert.equal(api.completions[0].payload.passwordPageMode, 'login'); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 2b7f13a..64b9c69 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -1180,10 +1180,11 @@ Hide My Email 获取与管理链路: - 下一轮继续 7. 当前轮最终失败时,写入账号记录,并在自动重试链路中累计重试次数 - 手动停止与自动停止会写入“停止”状态(若后续同邮箱成功/失败,会被覆盖为最新状态) -8. 如果配置了线程间隔,则挂计时计划;计时计划会带上当前 `autoRunSessionId` -9. 旧 timer / 旧 alarm / 旧恢复入口只有在 session 仍有效时才允许恢复执行;Stop 会立即使当前 session 失效,防止“停止后旧倒计时又把流程重新拉起” -10. 如果启用 IP 代理,后台会在每轮成功后按 `ipProxyPoolTargetCount` 检查是否需要切换出口;候选代理不足时只记录日志并跳过切换 -11. 所有轮次结束后输出汇总,并清空当前 session 标识 +8. 当前轮最终成功时,如果本轮是手机号注册且当前 flow 的全部 workflow node 都已经完成,后台会清空 `signupPhone* / accountIdentifier* / phoneNumber / currentPhoneActivation` 与 `email / registrationEmailState / step8VerificationTargetEmail` 等运行态,避免下一轮继续复用上一轮手机号或绑定邮箱 +9. 如果配置了线程间隔,则挂计时计划;计时计划会带上当前 `autoRunSessionId` +10. 旧 timer / 旧 alarm / 旧恢复入口只有在 session 仍有效时才允许恢复执行;Stop 会立即使当前 session 失效,防止“停止后旧倒计时又把流程重新拉起” +11. 如果启用 IP 代理,后台会在每轮成功后按 `ipProxyPoolTargetCount` 检查是否需要切换出口;候选代理不足时只记录日志并跳过切换 +12. 所有轮次结束后输出汇总,并清空当前 session 标识 ## 9. 新增功能时最容易漏掉的地方 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 1f6b708..cea77fe 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -49,7 +49,7 @@ ## `background/` - `background/account-run-history.js`:账号记录模块,负责以统一账号标识维护最新记录;邮箱注册以邮箱为主身份,手机号注册以手机号为主身份,同一轮后续绑定的手机号或邮箱会并入同一条记录并覆盖旧占位状态;模块统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,兼容 phone-only 与 email+phone 组合记录、空密码,并按默认本地 helper 地址自动尝试把完整快照同步到本地 helper;若 helper 未启动,则静默跳过。 -- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 Plus 模式与 PayPal 配置、`gmailBaseEmail`、`mail2925BaseEmail`、当前 2925 账号选择与 `stepExecutionRangeByFlow`,避免自动流程重置后丢失关键持久配置。 +- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 Plus 模式与 PayPal 配置、`gmailBaseEmail`、`mail2925BaseEmail`、当前 2925 账号选择与 `stepExecutionRangeByFlow`,避免自动流程重置后丢失关键持久配置;手机号注册自动轮次只有在完整 workflow 全部完成后,才会清空手机号和绑定邮箱运行态,避免下一轮复用旧身份。 - `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `flowpilot.qlhazycoder.top` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。 - `background/cpa-api.js`:CPA 管理接口直连模块,负责请求 CPA OAuth 地址、提交 localhost callback,以及把当前 ChatGPT 已登录会话转换为 CPA `auth-files` 导入格式并直接写入管理接口。 - `background/cloudmail-provider.js`:Cloud Mail / SkyMail 后台 provider 模块,负责 Token 获取、邮箱创建、邮件列表读取、验证码轮询和生成/转发收件目标邮箱选择;新生成的 Cloud Mail 地址会统一走共享注册邮箱状态持久化,既回写带来源标记的注册邮箱运行态,也能在 Step 8 `add-email` 的手机号身份链路中保留 `signupPhone* / accountIdentifier*`。 @@ -245,6 +245,7 @@ - `tests/auth-japanese-localization.test.js`:测试登录、验证码、OAuth 同意页与资料页对日文按钮和文案的本地化识别。 - `tests/auto-run-fresh-attempt-reset.test.js`:测试自动运行在新一轮开始前会重置旧运行时上下文,并补充 `gmailBaseEmail` / `mail2925BaseEmail` 在 fresh reset 后仍被保留的回归验证。 - `tests/auto-run-hotmail-preflight.test.js`:测试自动运行控制器在每轮 fresh attempt 前会先预检 Hotmail 邮箱可用性。 +- `tests/auto-run-phone-signup-success-email-cleanup.test.js`:测试手机号注册自动流程只有在完整 workflow 全部完成后才清理手机号和绑定邮箱运行态,并确认下一轮不会复用上一轮手机号或邮箱。 - `tests/auto-run-step4-restart.test.js`:测试步骤 4 失败后的同邮箱重开、`user_already_exists` 终止分支、注册链跳过与手机号状态清理。 - `tests/auto-run-step4-mail2925-thread-terminate.test.js`:测试步骤 4 命中 2925“结束当前尝试”错误时,不会再沿用当前邮箱回到步骤 1 重开,而是直接把错误抛给自动重试控制器。 - `tests/auto-run-timer-session-guard.test.js`:测试自动运行的旧计时计划在 Stop 使 session 失效后,不能再把已停止的自动流程重新拉起。