修改登陆及后续逻辑,统一使用一个逻辑

This commit is contained in:
QLHazyCoder
2026-04-26 05:22:36 +08:00
parent ef52ff806b
commit 88ceb79af6
21 changed files with 868 additions and 370 deletions
@@ -44,4 +44,5 @@ test('logging/status add-phone detection ignores step 2 phone-entry switch failu
true
);
assert.equal(loggingStatus.getLoginAuthStateLabel('phone_verification_page'), '手机验证码页');
assert.equal(loggingStatus.getLoginAuthStateLabel('oauth_consent_page'), 'OAuth 授权页');
});
@@ -59,6 +59,8 @@ function createRouter(overrides = {}) {
getPendingAutoRunTimerPlan: () => null,
getSourceLabel: () => '',
getState: async () => overrides.state || { stepStatuses: { 3: 'pending' } },
getStepDefinitionForState: overrides.getStepDefinitionForState,
getStepIdsForState: overrides.getStepIdsForState,
getTabId: overrides.getTabId || (async () => null),
getStopRequested: () => false,
handleAutoRunLoopUnhandledError: async () => {},
@@ -184,6 +186,49 @@ test('message router skips step 5 when step 4 reports already logged-in transiti
assert.equal(events.logs[0]?.message, '步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。');
});
test('message router skips login-code step when oauth login lands on consent page', async () => {
const stepKeys = {
7: 'oauth-login',
8: 'fetch-login-code',
9: 'confirm-oauth',
};
const { router, events } = createRouter({
state: { stepStatuses: { 7: 'completed', 8: 'pending', 9: 'pending' } },
getStepDefinitionForState: (step) => ({ id: step, key: stepKeys[step] || '' }),
getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
});
await router.handleStepData(7, {
skipLoginVerificationStep: true,
directOAuthConsentPage: true,
});
assert.deepStrictEqual(events.stepStatuses, [{ step: 8, status: 'skipped' }]);
assert.equal(events.logs.some(({ message }) => /OAuth 授权页.*步骤 8/.test(message)), true);
});
test('message router skips Plus login-code step when oauth login lands on consent page', async () => {
const stepKeys = {
10: 'oauth-login',
11: 'fetch-login-code',
12: 'confirm-oauth',
13: 'platform-verify',
};
const { router, events } = createRouter({
state: { plusModeEnabled: true, stepStatuses: { 10: 'completed', 11: 'pending', 12: 'pending', 13: 'pending' } },
getStepDefinitionForState: (step) => ({ id: step, key: stepKeys[step] || '' }),
getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
});
await router.handleStepData(10, {
skipLoginVerificationStep: true,
directOAuthConsentPage: true,
});
assert.deepStrictEqual(events.stepStatuses, [{ step: 11, status: 'skipped' }]);
assert.equal(events.logs.some(({ message }) => /OAuth 授权页.*步骤 11/.test(message)), true);
});
test('message router finalizes step 3 before marking it completed', async () => {
const { router, events } = createRouter();
@@ -178,6 +178,55 @@ test('step 7 starts a new oauth timeout window for each refreshed oauth url', as
]);
});
test('step 7 forwards direct OAuth consent skip metadata when completing', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
const events = {
completions: [],
};
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async (step, payload) => {
events.completions.push({ step, payload });
},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async () => ({
step6Outcome: 'success',
state: 'oauth_consent_page',
skipLoginVerificationStep: true,
directOAuthConsentPage: true,
}),
STEP6_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
});
await executor.executeStep7({
email: 'user@example.com',
password: 'secret',
visibleStep: 10,
});
assert.deepStrictEqual(events.completions, [
{
step: 10,
payload: {
loginVerificationRequestedAt: null,
skipLoginVerificationStep: true,
directOAuthConsentPage: true,
},
},
]);
});
test('step 7 stops immediately when management secret is missing', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
+71 -1
View File
@@ -85,8 +85,78 @@ test('step 8 submits login verification directly without replaying step 7', asyn
{ step8VerificationTargetEmail: 'display.user@example.com' },
]);
assert.deepStrictEqual(calls.ensureReadyOptions, [
{ timeoutMs: 5000 },
{ visibleStep: 8, authLoginStep: 7, timeoutMs: 5000 },
]);
assert.equal(calls.resolveOptions.completionStep, 8);
});
test('Plus login-code step reuses step 8 verification logic but completes visible step 11', async () => {
let resolvedStep = null;
let resolvedOptions = null;
let readyOptions = null;
const remainingStepCalls = [];
const executor = api.createStep8Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async (options) => {
readyOptions = options;
return { state: 'verification_page', displayedEmail: 'plus.user@example.com' };
},
rerunStep7ForStep8Recovery: async () => {},
getOAuthFlowRemainingMs: async (details) => {
remainingStepCalls.push(details.step);
return 9000;
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs, details) => {
remainingStepCalls.push(details.step);
return Math.min(defaultTimeoutMs, 9000);
},
getMailConfig: () => ({
provider: 'qq',
label: 'QQ 邮箱',
source: 'mail-qq',
url: 'https://mail.qq.com',
navigateOnReuse: false,
}),
getState: async () => ({ email: 'user@example.com', password: 'secret', plusModeEnabled: true }),
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
isVerificationMailPollingError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
resolveVerificationStep: async (step, _state, _mail, options) => {
resolvedStep = step;
resolvedOptions = options;
await options.getRemainingTimeMs({ actionLabel: '登录验证码流程' });
},
reuseOrCreateTab: async () => {},
setState: async () => {},
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
});
await executor.executeStep8({
visibleStep: 11,
plusModeEnabled: true,
email: 'user@example.com',
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
});
assert.equal(resolvedStep, 8);
assert.equal(resolvedOptions.completionStep, 11);
assert.equal(resolvedOptions.targetEmail, 'plus.user@example.com');
assert.deepStrictEqual(readyOptions, { visibleStep: 11, authLoginStep: 10, timeoutMs: 9000 });
assert.deepStrictEqual(remainingStepCalls, [11, 11]);
});
test('step 8 uses a fixed 10-minute lookback window and disables resend interval for 2925 mailbox polling', async () => {
+40
View File
@@ -216,3 +216,43 @@ return {
assert.equal(modalPayload.actions[1].id, 'add_phone');
assert.equal(modalPayload.actions[1].label, '出现手机号验证');
});
test('sidepanel custom verification dialog exposes add-phone action for Plus login code step', async () => {
const bundle = [
extractFunction('getCustomVerificationPromptCopy'),
extractFunction('openCustomVerificationConfirmDialog'),
].join('\n');
const api = new Function(`
let openActionModalPayload = null;
async function openActionModal(options) {
openActionModalPayload = options;
return options.buildResult('add_phone');
}
async function openConfirmModal() {
throw new Error('Plus login code step should use action modal');
}
${bundle}
return {
getCustomVerificationPromptCopy,
openCustomVerificationConfirmDialog,
getOpenActionModalPayload: () => openActionModalPayload,
};
`)();
const prompt = api.getCustomVerificationPromptCopy(11);
assert.equal(prompt.phoneActionLabel, '出现手机号验证');
const result = await api.openCustomVerificationConfirmDialog(11);
assert.deepEqual(result, {
confirmed: false,
addPhoneDetected: true,
});
const modalPayload = api.getOpenActionModalPayload();
assert.equal(modalPayload.actions[1].id, 'add_phone');
});
+4 -3
View File
@@ -44,14 +44,15 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
'paypal-approve',
'plus-checkout-return',
'oauth-login',
'fetch-login-code',
'confirm-oauth',
'platform-verify',
]
);
assert.equal(plusSteps.some((step) => step.key === 'clear-login-cookies'), false);
assert.equal(plusSteps.some((step) => step.key === 'fetch-login-code'), false);
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
assert.equal(api.getLastStepId({ plusModeEnabled: true }), 12);
assert.equal(plusSteps.some((step) => step.key === 'fetch-login-code'), true);
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
assert.equal(api.getLastStepId({ plusModeEnabled: true }), 13);
});
test('sidepanel html loads shared step definitions before sidepanel bootstrap', () => {
+6 -3
View File
@@ -215,17 +215,20 @@ return {
consentReady: true,
});
const inspected = api.inspectLoginAuthState();
assert.strictEqual(inspected.state, 'oauth_consent_page');
const snapshot = api.normalizeStep6Snapshot({
state: 'oauth_consent_page',
url: 'https://auth.openai.com/authorize',
});
assert.strictEqual(snapshot.state, 'unknown', '第六步应忽略 oauth_consent_page 状态');
assert.strictEqual(snapshot.state, 'oauth_consent_page', '第六步应保留 oauth_consent_page 状态');
}
assert.ok(
!extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"),
'inspectLoginAuthState 不应再产出 oauth_consent_page 状态'
extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"),
'inspectLoginAuthState 产出 oauth_consent_page 状态'
);
console.log('step6 login state tests passed');
+134
View File
@@ -0,0 +1,134 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('content/signup-page.js', 'utf8');
function extractFunction(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 index = start; index < source.length; index += 1) {
const char = source[index];
if (char === '(') {
parenDepth += 1;
} else if (char === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (char === '{' && signatureEnded) {
braceStart = index;
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 char = source[end];
if (char === '{') depth += 1;
if (char === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('password submit treats direct OAuth consent as a login-code skip', async () => {
const api = new Function(`
const location = { href: 'https://auth.openai.com/authorize' };
function inspectLoginAuthState() {
return {
state: 'oauth_consent_page',
url: location.href,
};
}
function throwIfStopped() {}
async function sleep() {
throw new Error('should not wait once oauth consent is detected');
}
${extractFunction('createStep6SuccessResult')}
${extractFunction('createStep6OAuthConsentSuccessResult')}
${extractFunction('createStep6RecoverableResult')}
${extractFunction('normalizeStep6Snapshot')}
${extractFunction('getStep6OptionMessage')}
${extractFunction('resolveStep6PostSubmitSnapshot')}
${extractFunction('waitForStep6PostSubmitTransition')}
${extractFunction('waitForStep6PasswordSubmitTransition')}
return {
run() {
return waitForStep6PasswordSubmitTransition(123, 1000);
},
};
`)();
const transition = await api.run();
assert.equal(transition.action, 'done');
assert.equal(transition.result.state, 'oauth_consent_page');
assert.equal(transition.result.skipLoginVerificationStep, true);
assert.equal(transition.result.directOAuthConsentPage, true);
assert.equal(transition.result.loginVerificationRequestedAt, null);
});
test('step 7 entry succeeds when the auth page is already on OAuth consent', async () => {
const logs = [];
const api = new Function(`
const location = { href: 'https://auth.openai.com/authorize' };
const logs = arguments[0];
function inspectLoginAuthState() {
return {
state: 'oauth_consent_page',
url: location.href,
};
}
function throwIfStopped() {}
async function sleep() {}
function log(message, level = 'info') {
logs.push({ message, level });
}
${extractFunction('createStep6SuccessResult')}
${extractFunction('createStep6OAuthConsentSuccessResult')}
${extractFunction('normalizeStep6Snapshot')}
${extractFunction('waitForKnownLoginAuthState')}
${extractFunction('step6_login')}
return {
run() {
return step6_login({ email: 'user@example.com' });
},
};
`)(logs);
const result = await api.run();
assert.equal(result.step6Outcome, 'success');
assert.equal(result.state, 'oauth_consent_page');
assert.equal(result.skipLoginVerificationStep, true);
assert.equal(result.directOAuthConsentPage, true);
assert.equal(logs.some(({ level }) => level === 'ok'), true);
});
+63
View File
@@ -833,6 +833,69 @@ test('verification flow uses configured login resend count for step 8', async ()
assert.equal(pollCalls, 3);
});
test('verification flow can complete Plus visible login-code step with shared step 8 semantics', async () => {
const completed = [];
const fillMessages = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (step, payload) => {
completed.push({ step, payload });
},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async (_source, message) => {
if (message.type === 'FILL_CODE') {
fillMessages.push(message);
}
return {};
},
sendToMailContentScriptResilient: async () => ({ code: '654321', emailTimestamp: 456 }),
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
await helpers.resolveVerificationStep(
8,
{ email: 'user@example.com', lastLoginCode: null },
{ provider: 'qq', label: 'QQ 邮箱' },
{
completionStep: 11,
requestFreshCodeFirst: false,
maxResendRequests: 0,
resendIntervalMs: 0,
}
);
assert.deepStrictEqual(fillMessages.map((message) => message.step), [8]);
assert.deepStrictEqual(completed, [
{
step: 11,
payload: {
emailTimestamp: 456,
code: '654321',
phoneVerificationRequired: false,
},
},
]);
});
test('verification flow waits during resend cooldown instead of tight-looping', async () => {
const sleepCalls = [];
let pollCalls = 0;