修复手机号注册登录密码页与成功清理

This commit is contained in:
QLHazyCoder
2026-05-28 06:52:35 +08:00
parent 2992cc88ac
commit fae7b9bffd
9 changed files with 797 additions and 12 deletions
@@ -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, '');
});
@@ -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 = {
+71
View File
@@ -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');
});