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

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
+121 -39
View File
@@ -4420,7 +4420,6 @@ function getLoginAuthStateLabel(state) {
if (typeof loggingStatus !== 'undefined' && loggingStatus?.getLoginAuthStateLabel) {
return loggingStatus.getLoginAuthStateLabel(state);
}
state = state === 'oauth_consent_page' ? 'unknown' : state;
switch (state) {
case 'verification_page': return '登录验证码页';
case 'password_page': return '密码页';
@@ -4487,7 +4486,8 @@ function hasSavedProgress(statuses = {}, stateOverride = null) {
return activeStepIds.some((step) => (merged[step] || 'pending') !== 'pending');
}
function getDownstreamStateResets(step) {
function getDownstreamStateResets(step, state = {}) {
const stepKey = getStepExecutionKeyForState(step, state);
const plusRuntimeResets = {
plusCheckoutTabId: null,
plusCheckoutUrl: null,
@@ -4574,6 +4574,20 @@ function getDownstreamStateResets(step) {
localhostUrl: null,
};
}
if (stepKey === 'oauth-login' || stepKey === 'fetch-login-code') {
return {
lastLoginCode: null,
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
localhostUrl: null,
};
}
if (stepKey === 'confirm-oauth') {
return {
localhostUrl: null,
};
}
return {};
}
@@ -4609,7 +4623,7 @@ async function invalidateDownstreamAfterStepRestart(step, options = {}) {
await addLog(`${logLabel},已重置后续步骤状态:${changedSteps.join(', ')}`, 'warn');
}
const resets = getDownstreamStateResets(step);
const resets = getDownstreamStateResets(step, state);
if (Object.keys(resets).length) {
await setState(resets);
broadcastDataUpdate(resets);
@@ -5436,6 +5450,24 @@ let resumeWaiter = null;
const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000;
const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 7, 8, 9]);
const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 10, 12]);
const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([
'open-chatgpt',
'submit-signup-email',
'fetch-signup-code',
'clear-login-cookies',
'plus-checkout-create',
'plus-checkout-billing',
'paypal-approve',
'plus-checkout-return',
'oauth-login',
'fetch-login-code',
'confirm-oauth',
]);
const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([
'fill-password',
'fill-profile',
'platform-verify',
]);
function waitForStepComplete(step, timeoutMs = 120000) {
return new Promise((resolve, reject) => {
@@ -5457,7 +5489,26 @@ function waitForStepComplete(step, timeoutMs = 120000) {
});
}
function doesStepUseCompletionSignal(step) {
function getStepExecutionKeyForState(step, state = {}) {
if (typeof getStepDefinitionForState !== 'function') {
return '';
}
return String(getStepDefinitionForState(step, state)?.key || '').trim();
}
function doesStepUseBackgroundCompletion(step, state = {}) {
const stepKey = getStepExecutionKeyForState(step, state);
if (stepKey) {
return AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS.has(stepKey);
}
return AUTO_RUN_BACKGROUND_COMPLETED_STEPS.has(step);
}
function doesStepUseCompletionSignal(step, state = {}) {
const stepKey = getStepExecutionKeyForState(step, state);
if (stepKey) {
return STEP_COMPLETION_SIGNAL_STEP_KEYS.has(stepKey);
}
return STEP_COMPLETION_SIGNAL_STEPS.has(step);
}
@@ -5586,16 +5637,28 @@ async function waitForRunningStepsToFinish(payload = {}) {
return currentState;
}
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10, 11, 12]);
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10, 11, 12, 13]);
const AUTH_CHAIN_STEP_KEYS = new Set([
'oauth-login',
'fetch-login-code',
'confirm-oauth',
'platform-verify',
]);
let activeTopLevelAuthChainExecution = null;
function isAuthChainStep(step) {
function isAuthChainStep(step, state = {}) {
const stepKey = typeof getStepDefinitionForState === 'function'
? String(getStepDefinitionForState(step, state)?.key || '').trim()
: '';
if (stepKey && typeof AUTH_CHAIN_STEP_KEYS !== 'undefined') {
return AUTH_CHAIN_STEP_KEYS.has(stepKey);
}
return AUTH_CHAIN_STEP_IDS.has(Number(step));
}
async function acquireTopLevelAuthChainExecution(step) {
async function acquireTopLevelAuthChainExecution(step, state = {}) {
const normalizedStep = Number(step);
if (!isAuthChainStep(normalizedStep)) {
if (!isAuthChainStep(normalizedStep, state)) {
return {
joined: false,
release() {},
@@ -5735,7 +5798,8 @@ async function requestStop(options = {}) {
async function executeStep(step, options = {}) {
const { deferRetryableTransportError = false } = options;
console.log(LOG_PREFIX, `Executing step ${step}`);
const authChainClaim = await acquireTopLevelAuthChainExecution(step);
let state = await getState();
const authChainClaim = await acquireTopLevelAuthChainExecution(step, state);
if (authChainClaim.joined) {
return;
}
@@ -5747,7 +5811,7 @@ async function executeStep(step, options = {}) {
await addLog(`步骤 ${step} 开始执行`);
await humanStepDelay();
const state = await getState();
state = await getState();
// Set flow start time on first step
if (step === 1 && !state.flowStartTime) {
@@ -5765,11 +5829,11 @@ async function executeStep(step, options = {}) {
});
} catch (err) {
executionError = err;
const state = await getState();
const errorState = await getState();
if (isStopError(err)) {
await setStepStatus(step, 'stopped');
await addLog(`步骤 ${step} 已被用户停止`, 'warn');
await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, state, getErrorMessage(err));
await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, errorState, getErrorMessage(err));
throw err;
}
if (isTerminalSecurityBlockedError(err)) {
@@ -5780,10 +5844,10 @@ async function executeStep(step, options = {}) {
await handleBrowserSwitchRequired(err);
throw new Error(STOP_ERROR_MESSAGE);
}
if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step) && isRetryableContentScriptTransportError(err))) {
if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step, errorState) && isRetryableContentScriptTransportError(err))) {
await setStepStatus(step, 'failed');
await addLog(`步骤 ${step} 失败:${err.message}`, 'error');
await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, state, getErrorMessage(err));
await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, errorState, getErrorMessage(err));
} else {
console.warn(
LOG_PREFIX,
@@ -5813,12 +5877,13 @@ async function executeStepAndWait(step, delayAfter = 2000) {
await sleepWithStop(delaySeconds * 1000);
}
if (AUTO_RUN_BACKGROUND_COMPLETED_STEPS.has(step)) {
const executionState = await getState();
if (doesStepUseBackgroundCompletion(step, executionState)) {
await addLog(`自动运行:步骤 ${step} 由后台流程负责收尾,执行函数返回后将直接进入下一步。`, 'info');
await executeStep(step);
const latestState = await getState();
await addLog(`自动运行:步骤 ${step} 已执行返回,当前状态为 ${latestState.stepStatuses?.[step] || 'pending'},准备继续后续步骤。`, 'info');
} else if (doesStepUseCompletionSignal(step)) {
} else if (doesStepUseCompletionSignal(step, executionState)) {
await addLog(`自动运行:步骤 ${step} 已发起,正在等待完成信号(超时 ${AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS / 1000} 秒)。`, 'info');
await executeStepViaCompletionSignal(step, AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS);
await addLog(`自动运行:步骤 ${step} 已收到完成信号,准备继续后续步骤。`, 'info');
@@ -6581,7 +6646,10 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
if (restartDecision.blockedByAddPhone) {
const addPhoneUrl = restartDecision.authState?.url || 'https://auth.openai.com/add-phone';
await addLog(`步骤 ${step}:检测到认证流程进入 add-phone(${addPhoneUrl}),停止自动回到步骤 7 重开。`, 'warn');
const authChainStartStep = typeof getAuthChainStartStepId === 'function'
? getAuthChainStartStepId(await getState())
: FINAL_OAUTH_CHAIN_START_STEP;
await addLog(`步骤 ${step}:检测到认证流程进入 add-phone(${addPhoneUrl}),停止自动回到步骤 ${authChainStartStep} 重开。`, 'warn');
}
throw err;
}
@@ -7440,12 +7508,13 @@ async function runPreStep6CookieCleanup() {
// Step 7: Login and ensure the auth page reaches the login verification page
// ============================================================
async function refreshOAuthUrlBeforeStep6(state) {
async function refreshOAuthUrlBeforeStep6(state, options = {}) {
const visibleStep = Number(options.visibleStep) || Number(state?.visibleStep) || 7;
if (state?.contributionModeExpected && !state?.contributionMode) {
throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入贡献模式后再点击自动。');
throw new Error(`步骤 ${visibleStep}:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入贡献模式后再点击自动。`);
}
if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) {
await addLog('步骤 7contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...', 'info');
await addLog(`步骤 ${visibleStep}contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...`, 'info');
const contributionState = await contributionOAuthManager.startContributionFlow({
nickname: state.contributionNickname || '',
openAuthTab: false,
@@ -7458,9 +7527,9 @@ async function refreshOAuthUrlBeforeStep6(state) {
await handleStepData(1, { oauthUrl });
return oauthUrl;
}
await addLog(`步骤 7contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info');
await addLog(`步骤 ${visibleStep}contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info');
console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel');
const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: '步骤 7' });
const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: `步骤 ${visibleStep}` });
await handleStepData(1, refreshResult);
if (!refreshResult?.oauthUrl) {
@@ -7470,9 +7539,12 @@ async function refreshOAuthUrlBeforeStep6(state) {
return refreshResult.oauthUrl;
}
function buildOAuthFlowTimeoutError(step, actionLabel = '后续授权流程') {
function buildOAuthFlowTimeoutError(step, actionLabel = '后续授权流程', state = {}) {
const restartStep = typeof getAuthChainStartStepId === 'function'
? getAuthChainStartStepId(state)
: FINAL_OAUTH_CHAIN_START_STEP;
return new Error(
`步骤 ${step}:从拿到 OAuth 登录地址开始,${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟内未完成${actionLabel},结束当前链路,准备从步骤 7 重新开始。`
`步骤 ${step}:从拿到 OAuth 登录地址开始,${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟内未完成${actionLabel},结束当前链路,准备从步骤 ${restartStep} 重新开始。`
);
}
@@ -7523,7 +7595,7 @@ async function getOAuthFlowRemainingMs(options = {}) {
const remainingMs = deadlineAt - Date.now();
if (remainingMs <= 0) {
throw buildOAuthFlowTimeoutError(step, actionLabel);
throw buildOAuthFlowTimeoutError(step, actionLabel, state);
}
return remainingMs;
@@ -7539,9 +7611,11 @@ async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) {
const budgetMs = remainingMs - reserveMs;
if (budgetMs <= 0) {
const stateForError = options.state || await getState();
throw buildOAuthFlowTimeoutError(
Number(options.step) || 7,
String(options.actionLabel || '后续授权流程').trim() || '后续授权流程'
String(options.actionLabel || '后续授权流程').trim() || '后续授权流程',
stateForError
);
}
@@ -7615,7 +7689,7 @@ async function getPostStep6AutoRestartDecision(step, error) {
let authState = null;
try {
authState = await getLoginAuthStateFromContent({
logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 7 重开...`,
logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 ${authChainStartStep} 重开...`,
});
} catch (inspectError) {
console.warn(LOG_PREFIX, '[AutoRun] failed to inspect login auth state after post-step6 error', {
@@ -7671,6 +7745,8 @@ async function getLoginAuthStateFromContent(options = {}) {
}
async function ensureStep8VerificationPageReady(options = {}) {
const visibleStep = Number(options.visibleStep) || 8;
const authLoginStep = Number(options.authLoginStep) || (visibleStep >= 11 ? 10 : 7);
const pageState = await getLoginAuthStateFromContent(options);
if (pageState.state === 'verification_page') {
return pageState;
@@ -7682,17 +7758,17 @@ async function ensureStep8VerificationPageReady(options = {}) {
if (pageState.state === 'login_timeout_error_page') {
const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
throw new Error(`STEP8_RESTART_STEP7::步骤 8:当前认证页进入登录超时报错页,请回到步骤 7 重新开始。${urlPart}`.trim());
throw new Error(`STEP8_RESTART_STEP7::步骤 ${visibleStep}:当前认证页进入登录超时报错页,请回到步骤 ${authLoginStep} 重新开始。${urlPart}`.trim());
}
if (pageState.state === 'add_phone_page') {
const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
throw new Error(`步骤 8:当前认证页进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim());
throw new Error(`步骤 ${visibleStep}:当前认证页进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim());
}
const stateLabel = getLoginAuthStateLabel(pageState.state);
const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 7。当前状态:${stateLabel}.${urlPart}`.trim());
throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 ${authLoginStep}。当前状态:${stateLabel}.${urlPart}`.trim());
}
async function rerunStep7ForStep8Recovery(options = {}) {
@@ -7703,27 +7779,33 @@ async function rerunStep7ForStep8Recovery(options = {}) {
throwIfStopped();
const initialState = await getState();
const authLoginStep = typeof getAuthChainStartStepId === 'function'
? getAuthChainStartStepId(initialState)
: FINAL_OAUTH_CHAIN_START_STEP;
await addLog(logMessage, 'warn');
await setStepStatus(7, 'running');
await addLog('步骤 7 开始执行');
await setStepStatus(authLoginStep, 'running');
await addLog(`步骤 ${authLoginStep} 开始执行`);
try {
await step7Executor.executeStep7(initialState);
await step7Executor.executeStep7({
...initialState,
visibleStep: authLoginStep,
});
} catch (err) {
const latestState = await getState();
if (isStopError(err)) {
await setStepStatus(7, 'stopped');
await addLog('步骤 7 已被用户停止', 'warn');
await appendManualAccountRunRecordIfNeeded('step7_stopped', latestState, getErrorMessage(err));
await setStepStatus(authLoginStep, 'stopped');
await addLog(`步骤 ${authLoginStep} 已被用户停止`, 'warn');
await appendManualAccountRunRecordIfNeeded(`step${authLoginStep}_stopped`, latestState, getErrorMessage(err));
throw err;
}
if (isTerminalSecurityBlockedError(err)) {
await handleCloudflareSecurityBlocked(err);
throw new Error(STOP_ERROR_MESSAGE);
}
await setStepStatus(7, 'failed');
await addLog(`步骤 7 失败:${getErrorMessage(err)}`, 'error');
await appendManualAccountRunRecordIfNeeded('step7_failed', latestState, getErrorMessage(err));
await setStepStatus(authLoginStep, 'failed');
await addLog(`步骤 ${authLoginStep} 失败:${getErrorMessage(err)}`, 'error');
await appendManualAccountRunRecordIfNeeded(`step${authLoginStep}_failed`, latestState, getErrorMessage(err));
throw err;
}
-1
View File
@@ -70,7 +70,6 @@
}
function getLoginAuthStateLabel(state) {
state = state === 'oauth_consent_page' ? 'unknown' : state;
switch (state) {
case 'verification_page':
return '登录验证码页';
+39 -2
View File
@@ -137,6 +137,31 @@
return '';
}
function isStepProtectedFromAutoSkip(status) {
return status === 'running'
|| status === 'completed'
|| status === 'manual_completed'
|| status === 'skipped';
}
function findStepByKeyAfter(currentStep, targetKey, state = {}) {
const activeStepIds = typeof getStepIdsForState === 'function'
? getStepIdsForState(state)
: [];
const candidates = activeStepIds.length ? activeStepIds : [Number(currentStep) + 1, 8];
return candidates.find((stepId) => {
const numericStep = Number(stepId);
if (!Number.isFinite(numericStep) || numericStep <= Number(currentStep)) {
return false;
}
const stepKey = getStepKeyForState(numericStep, state);
if (stepKey) {
return stepKey === targetKey;
}
return targetKey === 'fetch-login-code' && Number(currentStep) === 7 && numericStep === 8;
}) || null;
}
async function handlePlatformVerifyStepData(payload) {
if (payload.localhostUrl) {
await closeLocalhostCallbackTabs(payload.localhostUrl);
@@ -180,7 +205,18 @@
const stepKey = getStepKeyForState(step, stateForStep);
if (stepKey === 'oauth-login') {
if (payload.loginVerificationRequestedAt) {
if (payload.skipLoginVerificationStep) {
await setState({ loginVerificationRequestedAt: null });
const latestState = await getState();
const loginCodeStep = findStepByKeyAfter(step, 'fetch-login-code', latestState);
if (loginCodeStep) {
const currentStatus = latestState.stepStatuses?.[loginCodeStep];
if (!isStepProtectedFromAutoSkip(currentStatus)) {
await setStepStatus(loginCodeStep, 'skipped');
await addLog(`步骤 ${step}:认证页已直接进入 OAuth 授权页,已自动跳过步骤 ${loginCodeStep} 的登录验证码。`, 'warn');
}
}
} else if (payload.loginVerificationRequestedAt) {
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
}
return;
@@ -484,7 +520,8 @@
await setPersistentSettings({ emailPrefix: message.payload.emailPrefix });
await setState({ emailPrefix: message.payload.emailPrefix });
}
if (doesStepUseCompletionSignal(step)) {
const executionState = await getState();
if (doesStepUseCompletionSignal(step, executionState)) {
await executeStepViaCompletionSignal(step);
} else {
await executeStep(step);
+5 -1
View File
@@ -38,10 +38,14 @@
return visibleStep > 0 ? visibleStep : fallback;
}
function getAuthLoginStepForVisibleStep(visibleStep) {
return visibleStep >= 12 ? 10 : 7;
}
async function executeStep9(state) {
const visibleStep = getVisibleStep(state, 9);
if (!state.oauthUrl) {
const authLoginStep = visibleStep === 11 ? 10 : 7;
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${authLoginStep}`);
}
+41 -22
View File
@@ -31,25 +31,34 @@
throwIfStopped,
} = deps;
async function getStep8ReadyTimeoutMs(actionLabel, expectedOauthUrl = '') {
function getVisibleStep(state, fallback = 8) {
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
return visibleStep > 0 ? visibleStep : fallback;
}
function getAuthLoginStepForVisibleStep(visibleStep) {
return visibleStep >= 11 ? 10 : 7;
}
async function getStep8ReadyTimeoutMs(actionLabel, expectedOauthUrl = '', visibleStep = 8) {
if (typeof getOAuthFlowStepTimeoutMs !== 'function') {
return 15000;
}
return getOAuthFlowStepTimeoutMs(15000, {
step: 8,
step: visibleStep,
actionLabel,
oauthUrl: expectedOauthUrl,
});
}
function getStep8RemainingTimeResolver(expectedOauthUrl = '') {
function getStep8RemainingTimeResolver(expectedOauthUrl = '', visibleStep = 8) {
if (typeof getOAuthFlowRemainingMs !== 'function') {
return undefined;
}
return async (details = {}) => getOAuthFlowRemainingMs({
step: 8,
step: visibleStep,
actionLabel: details.actionLabel || '登录验证码流程',
oauthUrl: expectedOauthUrl,
});
@@ -96,6 +105,7 @@
}
async function runStep8Attempt(state) {
const visibleStep = getVisibleStep(state, 8);
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
@@ -110,14 +120,16 @@
await chrome.tabs.update(authTabId, { active: true });
} else {
if (!state.oauthUrl) {
throw new Error('缺少登录用 OAuth 链接,请先完成步骤 7。');
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}`);
}
await reuseOrCreateTab('signup-page', state.oauthUrl);
}
throwIfStopped();
const pageState = await ensureStep8VerificationPageReady({
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || ''),
visibleStep,
authLoginStep: getAuthLoginStepForVisibleStep(visibleStep),
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep),
});
const shouldCompareVerificationEmail = mail.provider !== '2925';
const displayedVerificationEmail = shouldCompareVerificationEmail
@@ -131,22 +143,25 @@
step8VerificationTargetEmail: displayedVerificationEmail || '',
});
await addLog('步骤 8:登录验证码页面已就绪,开始获取验证码。', 'info');
await addLog(`步骤 ${visibleStep}:登录验证码页面已就绪,开始获取验证码。`, 'info');
if (shouldCompareVerificationEmail && displayedVerificationEmail) {
await addLog(`步骤 8:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info');
await addLog(`步骤 ${visibleStep}:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info');
}
if (shouldUseCustomRegistrationEmail(state)) {
await confirmCustomVerificationStepBypass(8);
await confirmCustomVerificationStepBypass(8, {
completionStep: visibleStep,
promptStep: visibleStep,
});
return;
}
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
await addLog('步骤 8:正在确认 iCloud 邮箱登录态...', 'info');
await addLog(`步骤 ${visibleStep}:正在确认 iCloud 邮箱登录态...`, 'info');
await ensureIcloudMailSession({
state,
step: 8,
actionLabel: '步骤 8:确认 iCloud 邮箱登录态',
actionLabel: `步骤 ${visibleStep}:确认 iCloud 邮箱登录态`,
});
}
@@ -156,22 +171,22 @@
|| mail.provider === LUCKMAIL_PROVIDER
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
) {
await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`);
await addLog(`步骤 ${visibleStep}:正在通过 ${mail.label} 轮询验证码...`);
} else {
await addLog(`步骤 8:正在打开${mail.label}...`);
await addLog(`步骤 ${visibleStep}:正在打开${mail.label}...`);
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: 'Step 8: ensure 2925 mailbox session',
actionLabel: `Step ${visibleStep}: ensure 2925 mailbox session`,
});
} else {
await focusOrOpenMailTab(mail);
}
if (mail.provider === '2925') {
await addLog(`步骤 8:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info');
await addLog(`步骤 ${visibleStep}:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info');
}
}
@@ -179,10 +194,11 @@
...state,
step8VerificationTargetEmail: displayedVerificationEmail || '',
}, mail, {
completionStep: visibleStep,
filterAfterTimestamp: verificationFilterAfterTimestamp,
sessionKey: verificationSessionKey,
disableTimeBudgetCap: mail.provider === '2925',
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || ''),
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || '', visibleStep),
requestFreshCodeFirst: false,
targetEmail: fixedTargetEmail,
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
@@ -206,6 +222,8 @@
await runStep8Attempt(currentState);
return;
} catch (err) {
const visibleStep = getVisibleStep(currentState, 8);
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
if (!isVerificationMailPollingError(err) && !isStep8RestartStep7Error(err)) {
throw err;
}
@@ -218,26 +236,27 @@
mailPollingAttempt += 1;
await addLog(
isStep8RestartStep7Error(err)
? `步骤 8:检测到认证页进入重试/超时报错状态,准备从步骤 7 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}...`
: `步骤 8:检测到邮箱轮询类失败,准备从步骤 7 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}...`,
? `步骤 ${visibleStep}:检测到认证页进入重试/超时报错状态,准备从步骤 ${authLoginStep} 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}...`
: `步骤 ${visibleStep}:检测到邮箱轮询类失败,准备从步骤 ${authLoginStep} 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}...`,
'warn'
);
await rerunStep7ForStep8Recovery({
logMessage: isStep8RestartStep7Error(err)
? '步骤 8:认证页进入重试/超时报错状态,正在回到步骤 7 重新发起登录流程...'
: '步骤 8:正在回到步骤 7,重新发起登录验证码流程...',
? `步骤 ${visibleStep}:认证页进入重试/超时报错状态,正在回到步骤 ${authLoginStep} 重新发起登录流程...`
: `步骤 ${visibleStep}:正在回到步骤 ${authLoginStep},重新发起登录验证码流程...`,
});
currentState = await getState();
}
}
const visibleStep = getVisibleStep(currentState, 8);
if (lastMailPollingError) {
throw new Error(
`步骤 8:登录验证码流程在 ${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS} 轮邮箱轮询恢复后仍未成功。最后一次原因:${lastMailPollingError.message}`
`步骤 ${visibleStep}:登录验证码流程在 ${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS} 轮邮箱轮询恢复后仍未成功。最后一次原因:${lastMailPollingError.message}`
);
}
throw new Error('步骤 8:登录验证码流程未成功完成。');
throw new Error(`步骤 ${visibleStep}:登录验证码流程未成功完成。`);
}
return { executeStep8 };
+10 -3
View File
@@ -60,7 +60,7 @@
try {
const currentState = attempt === 1 ? state : await getState();
const password = currentState.password || currentState.customPassword || '';
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState, { visibleStep });
if (typeof startOAuthFlowTimeoutWindow === 'function') {
await startOAuthFlowTimeoutWindow({ step: visibleStep, oauthUrl });
}
@@ -105,9 +105,16 @@
}
if (isStep6SuccessResult(result)) {
await completeStepFromBackground(visibleStep, {
const completionPayload = {
loginVerificationRequestedAt: result.loginVerificationRequestedAt || null,
});
};
if (result.skipLoginVerificationStep) {
completionPayload.skipLoginVerificationStep = true;
}
if (result.directOAuthConsentPage) {
completionPayload.directOAuthConsentPage = true;
}
await completeStepFromBackground(visibleStep, completionPayload);
return;
}
+11 -7
View File
@@ -32,21 +32,25 @@
}
function getConfirmStepForVisibleStep(visibleStep) {
return visibleStep === 12 ? 11 : 9;
return visibleStep >= 13 ? 12 : 9;
}
function parseLocalhostCallback(rawUrl) {
function getAuthLoginStepForVisibleStep(visibleStep) {
return visibleStep >= 13 ? 10 : 7;
}
function parseLocalhostCallback(rawUrl, visibleStep = 10, confirmStep = 9) {
let parsed;
try {
parsed = new URL(rawUrl);
} catch {
throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 9。');
throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 ${confirmStep}`);
}
const code = normalizeString(parsed.searchParams.get('code'));
const state = normalizeString(parsed.searchParams.get('state'));
if (!code || !state) {
throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 9。');
throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 ${confirmStep}`);
}
return {
@@ -191,16 +195,16 @@
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}`);
}
if (!state.codex2apiSessionId) {
throw new Error(`缺少 Codex2API 会话信息,请重新执行步骤 ${visibleStep === 12 ? 10 : 7}`);
throw new Error(`缺少 Codex2API 会话信息,请重新执行步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}`);
}
if (!normalizeString(state.codex2apiAdminKey)) {
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
}
const callback = parseLocalhostCallback(state.localhostUrl);
const callback = parseLocalhostCallback(state.localhostUrl, visibleStep, confirmStep);
const expectedState = normalizeString(state.codex2apiOAuthState);
if (expectedState && expectedState !== callback.state) {
throw new Error('Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 7。');
throw new Error(`Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}`);
}
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
+17 -9
View File
@@ -175,25 +175,32 @@
return Math.max(0, Math.floor(Number(VERIFICATION_POLL_MAX_ROUNDS) || 1) - 1);
}
async function confirmCustomVerificationStepBypass(step) {
function getCompletionStep(step, options = {}) {
const completionStep = Number(options.completionStep);
return Number.isFinite(completionStep) && completionStep > 0 ? completionStep : step;
}
async function confirmCustomVerificationStepBypass(step, options = {}) {
const completionStep = getCompletionStep(step, options);
const promptStep = getCompletionStep(step, { completionStep: options.promptStep ?? completionStep });
const verificationLabel = getVerificationCodeLabel(step);
await addLog(`步骤 ${step}:当前为自定义邮箱模式,请手动在页面中输入${verificationLabel}验证码并进入下一页面。`, 'warn');
await addLog(`步骤 ${completionStep}:当前为自定义邮箱模式,请手动在页面中输入${verificationLabel}验证码并进入下一页面。`, 'warn');
let response = null;
try {
response = await confirmCustomVerificationStepBypassRequest(step);
response = await confirmCustomVerificationStepBypassRequest(promptStep);
} catch {
throw new Error(`步骤 ${step}:无法打开确认弹窗,请先保持侧边栏打开后重试。`);
throw new Error(`步骤 ${completionStep}:无法打开确认弹窗,请先保持侧边栏打开后重试。`);
}
if (response?.error) {
throw new Error(response.error);
}
if (step === 8 && response?.addPhoneDetected) {
throw new Error('步骤 8:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。 URL: https://auth.openai.com/add-phone');
throw new Error(`步骤 ${completionStep}:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。 URL: https://auth.openai.com/add-phone`);
}
if (!response?.confirmed) {
throw new Error(`步骤 ${step}:已取消手动${verificationLabel}验证码确认。`);
throw new Error(`步骤 ${completionStep}:已取消手动${verificationLabel}验证码确认。`);
}
await setState({
@@ -201,8 +208,8 @@
signupVerificationRequestedAt: null,
loginVerificationRequestedAt: null,
});
await deps.setStepStatus(step, 'skipped');
await addLog(`步骤 ${step}:已确认手动完成${verificationLabel}验证码输入,当前步骤已跳过。`, 'warn');
await deps.setStepStatus(completionStep, 'skipped');
await addLog(`步骤 ${completionStep}:已确认手动完成${verificationLabel}验证码输入,当前步骤已跳过。`, 'warn');
}
function getVerificationPollPayload(step, state, overrides = {}) {
@@ -781,6 +788,7 @@
}
async function resolveVerificationStep(step, state, mail, options = {}) {
const completionStep = getCompletionStep(step, options);
const stateKey = getVerificationCodeStateKey(step);
const rejectedCodes = new Set();
const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER
@@ -918,7 +926,7 @@
[stateKey]: result.code,
});
await completeStepFromBackground(step, {
await completeStepFromBackground(completionStep, {
emailTimestamp: result.emailTimestamp,
code: result.code,
phoneVerificationRequired: Boolean(submitResult.addPhonePage),
+204 -274
View File
@@ -1803,6 +1803,13 @@ function inspectLoginAuthState() {
};
}
if (consentReady) {
return {
...baseState,
state: 'oauth_consent_page',
};
}
return baseState;
}
@@ -1830,7 +1837,7 @@ function serializeLoginAuthState(snapshot) {
}
function getLoginAuthStateLabel(snapshot) {
const state = snapshot?.state === 'oauth_consent_page' ? 'unknown' : snapshot?.state;
const state = snapshot?.state;
switch (state) {
case 'verification_page':
return '登录验证码页';
@@ -1887,13 +1894,32 @@ async function waitForLoginVerificationPageReady(timeout = 10000) {
}
function createStep6SuccessResult(snapshot, options = {}) {
return {
const result = {
step6Outcome: 'success',
state: snapshot?.state || 'verification_page',
url: snapshot?.url || location.href,
via: options.via || '',
loginVerificationRequestedAt: options.loginVerificationRequestedAt || null,
};
if (options.skipLoginVerificationStep) {
result.skipLoginVerificationStep = true;
}
if (options.directOAuthConsentPage) {
result.directOAuthConsentPage = true;
}
return result;
}
function createStep6OAuthConsentSuccessResult(snapshot, options = {}) {
return createStep6SuccessResult(snapshot, {
...options,
via: options.via || 'oauth_consent_page',
loginVerificationRequestedAt: null,
skipLoginVerificationStep: true,
directOAuthConsentPage: true,
});
}
function createStep6RecoverableResult(reason, snapshot, options = {}) {
@@ -1948,6 +1974,15 @@ async function createStep6LoginTimeoutRecoveryTransition(reason, snapshot, messa
};
}
if (resolvedSnapshot.state === 'oauth_consent_page') {
return {
action: 'done',
result: createStep6OAuthConsentSuccessResult(resolvedSnapshot, {
via,
}),
};
}
if (resolvedSnapshot.state === 'password_page') {
log('步骤 7:登录超时报错页恢复后已进入密码页,继续当前登录流程。', 'warn');
return { action: 'password', snapshot: resolvedSnapshot };
@@ -2007,6 +2042,13 @@ async function finalizeStep6VerificationReady(options = {}) {
});
}
if (snapshot.state === 'oauth_consent_page') {
log(`${logLabel}:认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。`, 'ok');
return createStep6OAuthConsentSuccessResult(snapshot, {
via: `${via}_oauth_consent`,
});
}
if (snapshot.state === 'login_timeout_error_page') {
log(`${logLabel}:页面进入登录超时报错页,准备自动恢复后重试步骤 7。`, 'warn');
return createStep6LoginTimeoutRecoverableResult(
@@ -2037,6 +2079,12 @@ async function finalizeStep6VerificationReady(options = {}) {
loginVerificationRequestedAt,
});
}
if (snapshot.state === 'oauth_consent_page') {
log(`${logLabel}:认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。`, 'ok');
return createStep6OAuthConsentSuccessResult(snapshot, {
via: `${via}_oauth_consent`,
});
}
if (snapshot.state === 'login_timeout_error_page') {
log(`${logLabel}:页面进入登录超时报错页,准备自动恢复后重试步骤 7。`, 'warn');
return createStep6LoginTimeoutRecoverableResult(
@@ -2059,21 +2107,14 @@ async function finalizeStep6VerificationReady(options = {}) {
}
function normalizeStep6Snapshot(snapshot) {
if (snapshot?.state !== 'oauth_consent_page') {
return snapshot;
}
return {
...snapshot,
state: 'unknown',
};
return snapshot;
}
function throwForStep6FatalState(snapshot) {
snapshot = normalizeStep6Snapshot(snapshot);
switch (snapshot?.state) {
case 'oauth_consent_page':
throw new Error(`当前页面已进入 OAuth 授权页,未经过登录验证码页,无法完成步骤 7。URL: ${snapshot.url}`);
return;
case 'add_phone_page':
throw new Error(`当前页面已进入手机号页面,未经过登录验证码页,无法完成步骤 7。URL: ${snapshot.url}`);
case 'unknown':
@@ -2595,88 +2636,55 @@ async function fillVerificationCode(step, payload) {
// Step 7: Login with registered account (on OAuth auth page)
// ============================================================
async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 12000) {
const start = Date.now();
let snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
function getStep6OptionMessage(value, snapshot) {
return typeof value === 'function' ? value(snapshot) : String(value || '');
}
while (Date.now() - start < timeout) {
throwIfStopped();
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
async function resolveStep6PostSubmitSnapshot(snapshot, options = {}) {
const normalizedSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
const {
via = 'post_submit',
loginVerificationRequestedAt = null,
oauthConsentVia = `${via}_oauth_consent`,
timeoutRecoveryReason = 'login_timeout_error_page',
timeoutRecoveryMessage = '登录提交后进入登录超时报错页。',
timeoutRecoveryVia = `${via}_timeout_recovered`,
allowPasswordAction = false,
allowEmailAction = false,
allowFinalPasswordAction = false,
allowFinalEmailAction = false,
allowFinalSwitchAction = false,
final = false,
addPhoneMessage,
} = options;
if (snapshot.state === 'verification_page') {
return {
action: 'done',
result: createStep6SuccessResult(snapshot, {
via: 'email_submit',
loginVerificationRequestedAt: emailSubmittedAt,
}),
};
}
if (snapshot.state === 'password_page') {
return { action: 'password', snapshot };
}
if (snapshot.state === 'login_timeout_error_page') {
const transition = await createStep6LoginTimeoutRecoveryTransition(
'login_timeout_error_page',
snapshot,
'提交邮箱后进入登录超时报错页。',
{
loginVerificationRequestedAt: emailSubmittedAt,
via: 'email_submit_timeout_recovered',
}
);
if (transition.action === 'done') {
return {
action: 'done',
result: transition.result,
};
}
if (transition.action === 'password') {
return { action: 'password', snapshot: transition.snapshot };
}
if (transition.action === 'email') {
return { action: 'email', snapshot: transition.snapshot };
}
return {
action: 'recoverable',
result: transition.result,
};
}
if (snapshot.state === 'oauth_consent_page') {
throw new Error(`提交邮箱后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
}
if (snapshot.state === 'add_phone_page') {
throw new Error(`提交邮箱后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`);
}
await sleep(250);
}
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
if (snapshot.state === 'verification_page') {
if (normalizedSnapshot.state === 'verification_page') {
return {
action: 'done',
result: createStep6SuccessResult(snapshot, {
via: 'email_submit',
loginVerificationRequestedAt: emailSubmittedAt,
result: createStep6SuccessResult(normalizedSnapshot, {
via,
loginVerificationRequestedAt,
}),
};
}
if (snapshot.state === 'password_page') {
return { action: 'password', snapshot };
if (normalizedSnapshot.state === 'oauth_consent_page') {
return {
action: 'done',
result: createStep6OAuthConsentSuccessResult(normalizedSnapshot, {
via: oauthConsentVia,
}),
};
}
if (snapshot.state === 'login_timeout_error_page') {
if (normalizedSnapshot.state === 'login_timeout_error_page') {
const transition = await createStep6LoginTimeoutRecoveryTransition(
'login_timeout_error_page',
snapshot,
'提交邮箱后进入登录超时报错页。',
timeoutRecoveryReason,
normalizedSnapshot,
timeoutRecoveryMessage,
{
loginVerificationRequestedAt: emailSubmittedAt,
via: 'email_submit_timeout_recovered',
loginVerificationRequestedAt,
via: timeoutRecoveryVia,
}
);
if (transition.action === 'done') {
@@ -2696,213 +2704,116 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120
result: transition.result,
};
}
if (snapshot.state === 'oauth_consent_page') {
throw new Error(`提交邮箱后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
if (normalizedSnapshot.state === 'password_page') {
if (allowPasswordAction || (final && allowFinalPasswordAction)) {
return { action: 'password', snapshot: normalizedSnapshot };
}
if (final && allowFinalSwitchAction && normalizedSnapshot.switchTrigger) {
return { action: 'switch', snapshot: normalizedSnapshot };
}
}
if (snapshot.state === 'add_phone_page') {
throw new Error(`提交邮箱后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`);
if (normalizedSnapshot.state === 'email_page' && (allowEmailAction || (final && allowFinalEmailAction))) {
return { action: 'email', snapshot: normalizedSnapshot };
}
if (normalizedSnapshot.state === 'add_phone_page') {
const message = getStep6OptionMessage(addPhoneMessage, normalizedSnapshot)
|| `登录提交后页面进入手机号页面。URL: ${normalizedSnapshot.url || location.href}`;
throw new Error(message);
}
return null;
}
async function waitForStep6PostSubmitTransition(options = {}) {
const {
timeout = 10000,
stalledReason = 'post_submit_stalled',
stalledMessage = '登录提交后未进入可识别的下一页。',
} = options;
const start = Date.now();
let snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
while (Date.now() - start < timeout) {
throwIfStopped();
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
const transition = await resolveStep6PostSubmitSnapshot(snapshot, {
...options,
final: false,
});
if (transition) {
return transition;
}
await sleep(250);
}
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
const transition = await resolveStep6PostSubmitSnapshot(snapshot, {
...options,
final: true,
});
if (transition) {
return transition;
}
return {
action: 'recoverable',
result: createStep6RecoverableResult('email_submit_stalled', snapshot, {
message: '提交邮箱后长时间未进入密码页或登录验证码页。',
result: createStep6RecoverableResult(stalledReason, snapshot, {
message: stalledMessage,
loginVerificationRequestedAt: options.loginVerificationRequestedAt || null,
}),
};
}
async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 12000) {
return waitForStep6PostSubmitTransition({
timeout,
via: 'email_submit',
oauthConsentVia: 'email_submit_oauth_consent',
loginVerificationRequestedAt: emailSubmittedAt,
timeoutRecoveryMessage: '提交邮箱后进入登录超时报错页。',
timeoutRecoveryVia: 'email_submit_timeout_recovered',
allowPasswordAction: true,
stalledReason: 'email_submit_stalled',
stalledMessage: '提交邮箱后长时间未进入密码页或登录验证码页。',
addPhoneMessage: (snapshot) => `提交邮箱后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`,
});
}
async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout = 10000) {
const start = Date.now();
let snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
while (Date.now() - start < timeout) {
throwIfStopped();
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
if (snapshot.state === 'verification_page') {
return {
action: 'done',
result: createStep6SuccessResult(snapshot, {
via: 'password_submit',
loginVerificationRequestedAt: passwordSubmittedAt,
}),
};
}
if (snapshot.state === 'login_timeout_error_page') {
const transition = await createStep6LoginTimeoutRecoveryTransition(
'login_timeout_error_page',
snapshot,
'提交密码后进入登录超时报错页。',
{
loginVerificationRequestedAt: passwordSubmittedAt,
via: 'password_submit_timeout_recovered',
}
);
if (transition.action === 'done') {
return {
action: 'done',
result: transition.result,
};
}
if (transition.action === 'password') {
return { action: 'password', snapshot: transition.snapshot };
}
if (transition.action === 'email') {
return { action: 'email', snapshot: transition.snapshot };
}
return {
action: 'recoverable',
result: transition.result,
};
}
if (snapshot.state === 'oauth_consent_page') {
throw new Error(`提交密码后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
}
if (snapshot.state === 'add_phone_page') {
throw new Error(`提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`);
}
await sleep(250);
}
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
if (snapshot.state === 'verification_page') {
return {
action: 'done',
result: createStep6SuccessResult(snapshot, {
via: 'password_submit',
loginVerificationRequestedAt: passwordSubmittedAt,
}),
};
}
if (snapshot.state === 'login_timeout_error_page') {
const transition = await createStep6LoginTimeoutRecoveryTransition(
'login_timeout_error_page',
snapshot,
'提交密码后进入登录超时报错页。',
{
loginVerificationRequestedAt: passwordSubmittedAt,
via: 'password_submit_timeout_recovered',
}
);
if (transition.action === 'done') {
return {
action: 'done',
result: transition.result,
};
}
if (transition.action === 'password') {
return { action: 'password', snapshot: transition.snapshot };
}
if (transition.action === 'email') {
return { action: 'email', snapshot: transition.snapshot };
}
return {
action: 'recoverable',
result: transition.result,
};
}
if (snapshot.state === 'oauth_consent_page') {
throw new Error(`提交密码后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
}
if (snapshot.state === 'add_phone_page') {
throw new Error(`提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`);
}
if (snapshot.state === 'password_page' && snapshot.switchTrigger) {
return { action: 'switch', snapshot };
}
return {
action: 'recoverable',
result: createStep6RecoverableResult('password_submit_stalled', snapshot, {
message: '提交密码后仍未进入登录验证码页。',
}),
};
return waitForStep6PostSubmitTransition({
timeout,
via: 'password_submit',
oauthConsentVia: 'password_submit_oauth_consent',
loginVerificationRequestedAt: passwordSubmittedAt,
timeoutRecoveryMessage: '提交密码后进入登录超时报错页。',
timeoutRecoveryVia: 'password_submit_timeout_recovered',
allowFinalSwitchAction: true,
stalledReason: 'password_submit_stalled',
stalledMessage: '提交密码后仍未进入登录验证码页。',
addPhoneMessage: (snapshot) => `提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`,
});
}
async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeout = 10000) {
const start = Date.now();
let snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
const transition = await waitForStep6PostSubmitTransition({
timeout,
via: 'switch_to_one_time_code_login',
oauthConsentVia: 'switch_to_one_time_code_oauth_consent',
loginVerificationRequestedAt,
timeoutRecoveryMessage: '切换到一次性验证码登录后进入登录超时报错页。',
timeoutRecoveryVia: 'switch_to_one_time_code_timeout_recovered',
stalledReason: 'one_time_code_switch_stalled',
stalledMessage: '点击一次性验证码登录后仍未进入登录验证码页。',
addPhoneMessage: (snapshot) => `切换到一次性验证码登录后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`,
});
while (Date.now() - start < timeout) {
throwIfStopped();
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
if (snapshot.state === 'verification_page') {
return createStep6SuccessResult(snapshot, {
via: 'switch_to_one_time_code_login',
loginVerificationRequestedAt,
});
}
if (snapshot.state === 'login_timeout_error_page') {
const transition = await createStep6LoginTimeoutRecoveryTransition(
'login_timeout_error_page',
snapshot,
'切换到一次性验证码登录后进入登录超时报错页。',
{
loginVerificationRequestedAt,
via: 'switch_to_one_time_code_timeout_recovered',
}
);
if (transition.action === 'done') {
return transition.result;
}
if (transition.action === 'password' || transition.action === 'email') {
return transition;
}
return transition.result;
}
if (snapshot.state === 'oauth_consent_page') {
throw new Error(`切换到一次性验证码登录后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
}
if (snapshot.state === 'add_phone_page') {
throw new Error(`切换到一次性验证码登录后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`);
}
await sleep(250);
}
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
if (snapshot.state === 'verification_page') {
return createStep6SuccessResult(snapshot, {
via: 'switch_to_one_time_code_login',
loginVerificationRequestedAt,
});
}
if (snapshot.state === 'login_timeout_error_page') {
const transition = await createStep6LoginTimeoutRecoveryTransition(
'login_timeout_error_page',
snapshot,
'切换到一次性验证码登录后进入登录超时报错页。',
{
loginVerificationRequestedAt,
via: 'switch_to_one_time_code_timeout_recovered',
}
);
if (transition.action === 'done') {
return transition.result;
}
if (transition.action === 'password' || transition.action === 'email') {
return transition;
}
if (transition.action === 'done' || transition.action === 'recoverable') {
return transition.result;
}
if (snapshot.state === 'oauth_consent_page') {
throw new Error(`切换到一次性验证码登录后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
}
if (snapshot.state === 'add_phone_page') {
throw new Error(`切换到一次性验证码登录后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`);
}
return createStep6RecoverableResult('one_time_code_switch_stalled', snapshot, {
message: '点击一次性验证码登录后仍未进入登录验证码页。',
});
return transition;
}
async function step6SwitchToOneTimeCodeLogin(payload, snapshot) {
@@ -2921,6 +2832,9 @@ async function step6SwitchToOneTimeCodeLogin(payload, snapshot) {
await sleep(1200);
const result = await waitForStep6SwitchTransition(loginVerificationRequestedAt);
if (result?.step6Outcome === 'success') {
if (result.skipLoginVerificationStep) {
return result;
}
return finalizeStep6VerificationReady({
logLabel: '步骤 7 收尾',
loginVerificationRequestedAt: result.loginVerificationRequestedAt || loginVerificationRequestedAt,
@@ -2964,6 +2878,9 @@ async function step6LoginFromPasswordPage(payload, snapshot) {
const transition = await waitForStep6PasswordSubmitTransition(passwordSubmittedAt);
if (transition.action === 'done') {
if (transition.result?.skipLoginVerificationStep) {
return transition.result;
}
return finalizeStep6VerificationReady({
logLabel: '步骤 7 收尾',
loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || passwordSubmittedAt,
@@ -3020,6 +2937,9 @@ async function step6LoginFromEmailPage(payload, snapshot) {
const transition = await waitForStep6EmailSubmitTransition(emailSubmittedAt);
if (transition.action === 'done') {
if (transition.result?.skipLoginVerificationStep) {
return transition.result;
}
return finalizeStep6VerificationReady({
logLabel: '步骤 7 收尾',
loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || emailSubmittedAt,
@@ -3057,6 +2977,13 @@ async function step6_login(payload) {
});
}
if (snapshot.state === 'oauth_consent_page') {
log('步骤 7:认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。', 'ok');
return createStep6OAuthConsentSuccessResult(snapshot, {
via: 'already_on_oauth_consent_page',
});
}
if (snapshot.state === 'login_timeout_error_page') {
log('步骤 7:检测到登录超时报错页,先尝试恢复当前页面。', 'warn');
const transition = await createStep6LoginTimeoutRecoveryTransition(
@@ -3069,6 +2996,9 @@ async function step6_login(payload) {
}
);
if (transition.action === 'done') {
if (transition.result?.skipLoginVerificationStep) {
return transition.result;
}
return finalizeStep6VerificationReady({
logLabel: '步骤 7 收尾',
loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || null,
+1 -1
View File
@@ -1012,7 +1012,7 @@ async function step1_getOAuthLink(payload, options = {}) {
async function step9_vpsVerify(payload) {
const visibleStep = Number(payload?.visibleStep) || 10;
const confirmStep = visibleStep === 12 ? 11 : 9;
const confirmStep = visibleStep >= 13 ? 12 : 9;
await ensureOAuthManagementPage(payload?.vpsPassword, confirmStep);
// 优先从 payload 读取 localhostUrl;没有时再回退到全局状态
+3 -2
View File
@@ -25,8 +25,9 @@
{ id: 8, order: 80, key: 'paypal-approve', title: 'PayPal 登录与授权' },
{ id: 9, order: 90, key: 'plus-checkout-return', title: '订阅回跳确认' },
{ id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录' },
{ id: 11, order: 110, key: 'confirm-oauth', title: '自动确认 OAuth' },
{ id: 12, order: 120, key: 'platform-verify', title: '平台回调验证' },
{ id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码' },
{ id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth' },
{ id: 13, order: 130, key: 'platform-verify', title: '平台回调验证' },
];
function isPlusModeEnabled(options = {}) {
+3 -2
View File
@@ -2965,6 +2965,7 @@ function getCustomMailProviderUiCopy() {
function getCustomVerificationPromptCopy(step) {
const verificationLabel = step === 4 ? '注册验证码' : '登录验证码';
const isLoginVerificationStep = step === 8 || step === 11;
return {
title: `手动处理${verificationLabel}`,
message: `当前邮箱服务为“自定义邮箱”。请先在页面中手动输入${verificationLabel},并确认已经进入下一页面后,再点击确认。`,
@@ -2972,7 +2973,7 @@ function getCustomVerificationPromptCopy(step) {
text: `点击确认后会跳过步骤 ${step}`,
tone: 'danger',
},
...(step === 8 ? {
...(isLoginVerificationStep ? {
phoneActionLabel: '出现手机号验证',
phoneActionAlert: {
text: '如果当前页面已经进入手机号验证,可直接标记为失败并继续下一个邮箱。',
@@ -2984,7 +2985,7 @@ function getCustomVerificationPromptCopy(step) {
async function openCustomVerificationConfirmDialog(step) {
const promptCopy = getCustomVerificationPromptCopy(step);
if (step === 8) {
if (step === 8 || step === 11) {
return openActionModal({
title: promptCopy.title,
message: promptCopy.message,
@@ -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;