feat: Enhance login flow with add-email and phone verification support
- Added support for the add-email page in the logging status module. - Updated step 8 to handle phone-registered accounts during email verification. - Implemented logic to submit add-email before polling for email verification codes. - Enhanced tests for step 7 to detect username input as phone number and ignore hidden inputs. - Improved step 8 to allow add-email handoff only when requested. - Updated documentation to reflect changes in the login verification flow.
This commit is contained in:
+15
-2
@@ -7198,6 +7198,7 @@ function getLoginAuthStateLabel(state) {
|
|||||||
case 'login_timeout_error_page': return '登录超时报错页';
|
case 'login_timeout_error_page': return '登录超时报错页';
|
||||||
case 'oauth_consent_page': return 'OAuth 授权页';
|
case 'oauth_consent_page': return 'OAuth 授权页';
|
||||||
case 'add_phone_page': return '手机号页';
|
case 'add_phone_page': return '手机号页';
|
||||||
|
case 'add_email_page': return '添加邮箱页';
|
||||||
default: return '未知页面';
|
default: return '未知页面';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10345,10 +10346,12 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
|
|||||||
isVerificationMailPollingError,
|
isVerificationMailPollingError,
|
||||||
LUCKMAIL_PROVIDER,
|
LUCKMAIL_PROVIDER,
|
||||||
resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep,
|
resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep,
|
||||||
|
resolveSignupEmailForFlow,
|
||||||
phoneVerificationHelpers,
|
phoneVerificationHelpers,
|
||||||
rerunStep7ForStep8Recovery: (...args) => rerunStep7ForStep8Recovery(...args),
|
rerunStep7ForStep8Recovery: (...args) => rerunStep7ForStep8Recovery(...args),
|
||||||
resolveSignupMethod,
|
resolveSignupMethod,
|
||||||
reuseOrCreateTab,
|
reuseOrCreateTab,
|
||||||
|
sendToContentScriptResilient,
|
||||||
setState,
|
setState,
|
||||||
shouldUseCustomRegistrationEmail,
|
shouldUseCustomRegistrationEmail,
|
||||||
sleepWithStop,
|
sleepWithStop,
|
||||||
@@ -11305,7 +11308,12 @@ async function ensureStep8VerificationPageReady(options = {}) {
|
|||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
let pageState = await inspectState();
|
let pageState = await inspectState();
|
||||||
if (pageState.state === 'verification_page' || pageState.state === 'oauth_consent_page') {
|
if (
|
||||||
|
pageState.state === 'verification_page'
|
||||||
|
|| pageState.state === 'oauth_consent_page'
|
||||||
|
|| (options.allowPhoneVerificationPage && pageState.state === 'phone_verification_page')
|
||||||
|
|| (options.allowAddEmailPage && pageState.state === 'add_email_page')
|
||||||
|
) {
|
||||||
return pageState;
|
return pageState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11379,7 +11387,12 @@ async function ensureStep8VerificationPageReady(options = {}) {
|
|||||||
logMessage: '认证页恢复后,正在确认验证码页是否可继续...',
|
logMessage: '认证页恢复后,正在确认验证码页是否可继续...',
|
||||||
logStepKey: 'fetch-login-code',
|
logStepKey: 'fetch-login-code',
|
||||||
});
|
});
|
||||||
if (pageState.state === 'verification_page' || pageState.state === 'oauth_consent_page') {
|
if (
|
||||||
|
pageState.state === 'verification_page'
|
||||||
|
|| pageState.state === 'oauth_consent_page'
|
||||||
|
|| (options.allowPhoneVerificationPage && pageState.state === 'phone_verification_page')
|
||||||
|
|| (options.allowAddEmailPage && pageState.state === 'add_email_page')
|
||||||
|
) {
|
||||||
return pageState;
|
return pageState;
|
||||||
}
|
}
|
||||||
if (pageState.maxCheckAttemptsBlocked) {
|
if (pageState.maxCheckAttemptsBlocked) {
|
||||||
|
|||||||
@@ -101,6 +101,8 @@
|
|||||||
return 'OAuth 授权页';
|
return 'OAuth 授权页';
|
||||||
case 'add_phone_page':
|
case 'add_phone_page':
|
||||||
return '手机号页';
|
return '手机号页';
|
||||||
|
case 'add_email_page':
|
||||||
|
return '添加邮箱页';
|
||||||
case 'phone_verification_page':
|
case 'phone_verification_page':
|
||||||
return '手机验证码页';
|
return '手机验证码页';
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
root.MultiPageBackgroundStep8 = factory();
|
root.MultiPageBackgroundStep8 = factory();
|
||||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() {
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() {
|
||||||
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
|
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
|
||||||
|
const STEP8_ADD_EMAIL_URL = 'https://auth.openai.com/add-email';
|
||||||
|
const STEP8_CURRENT_STEP_RECOVERY_MAX_ATTEMPTS = 3;
|
||||||
|
|
||||||
function createStep8Executor(deps = {}) {
|
function createStep8Executor(deps = {}) {
|
||||||
const {
|
const {
|
||||||
@@ -22,11 +24,12 @@
|
|||||||
isTabAlive,
|
isTabAlive,
|
||||||
isVerificationMailPollingError,
|
isVerificationMailPollingError,
|
||||||
LUCKMAIL_PROVIDER,
|
LUCKMAIL_PROVIDER,
|
||||||
|
resolveSignupEmailForFlow,
|
||||||
resolveVerificationStep,
|
resolveVerificationStep,
|
||||||
rerunStep7ForStep8Recovery,
|
rerunStep7ForStep8Recovery,
|
||||||
reuseOrCreateTab,
|
reuseOrCreateTab,
|
||||||
|
sendToContentScriptResilient,
|
||||||
phoneVerificationHelpers = null,
|
phoneVerificationHelpers = null,
|
||||||
resolveSignupMethod = () => 'email',
|
|
||||||
setState,
|
setState,
|
||||||
shouldUseCustomRegistrationEmail,
|
shouldUseCustomRegistrationEmail,
|
||||||
sleepWithStop,
|
sleepWithStop,
|
||||||
@@ -99,11 +102,98 @@
|
|||||||
return String(value || '').trim().toLowerCase();
|
return String(value || '').trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPhoneLoginState(state = {}) {
|
async function getLoginAuthStateFromContent(visibleStep, options = {}) {
|
||||||
return String(state?.accountIdentifierType || '').trim().toLowerCase() === 'phone'
|
if (typeof sendToContentScriptResilient !== 'function') {
|
||||||
|| resolveSignupMethod(state) === 'phone'
|
return {};
|
||||||
|| Boolean(state?.signupPhoneCompletedActivation)
|
}
|
||||||
|| Boolean(state?.signupPhoneActivation);
|
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 15000);
|
||||||
|
const result = await sendToContentScriptResilient(
|
||||||
|
'signup-page',
|
||||||
|
{
|
||||||
|
type: 'GET_LOGIN_AUTH_STATE',
|
||||||
|
source: 'background',
|
||||||
|
payload: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeoutMs,
|
||||||
|
responseTimeoutMs: timeoutMs,
|
||||||
|
retryDelayMs: 600,
|
||||||
|
logMessage: options.logMessage || `步骤 ${visibleStep}:认证页正在切换,等待页面重新就绪...`,
|
||||||
|
logStep: visibleStep,
|
||||||
|
logStepKey: 'fetch-login-code',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
return result || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitAddEmailIfNeeded(state, visibleStep, initialPageState = null) {
|
||||||
|
if (typeof resolveSignupEmailForFlow !== 'function' || typeof sendToContentScriptResilient !== 'function') {
|
||||||
|
return { state, pageState: initialPageState };
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageState = initialPageState?.state
|
||||||
|
? initialPageState
|
||||||
|
: await getLoginAuthStateFromContent(visibleStep, {
|
||||||
|
timeoutMs: 15000,
|
||||||
|
logMessage: `步骤 ${visibleStep}:正在确认是否已进入添加邮箱页...`,
|
||||||
|
});
|
||||||
|
if (pageState?.state !== 'add_email_page') {
|
||||||
|
return { state, pageState };
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestState = typeof getState === 'function' ? await getState() : state;
|
||||||
|
const resolvedEmail = await resolveSignupEmailForFlow(latestState);
|
||||||
|
await addLog(`步骤 ${visibleStep}:检测到添加邮箱页,正在添加邮箱 ${resolvedEmail} 并进入邮箱验证码页...`);
|
||||||
|
|
||||||
|
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||||
|
? await getOAuthFlowStepTimeoutMs(60000, {
|
||||||
|
step: visibleStep,
|
||||||
|
actionLabel: '添加邮箱并进入验证码页',
|
||||||
|
oauthUrl: latestState?.oauthUrl || state?.oauthUrl || '',
|
||||||
|
})
|
||||||
|
: 60000;
|
||||||
|
const result = await sendToContentScriptResilient(
|
||||||
|
'signup-page',
|
||||||
|
{
|
||||||
|
type: 'SUBMIT_ADD_EMAIL',
|
||||||
|
source: 'background',
|
||||||
|
payload: { email: resolvedEmail },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeoutMs,
|
||||||
|
responseTimeoutMs: timeoutMs,
|
||||||
|
retryDelayMs: 700,
|
||||||
|
logMessage: `步骤 ${visibleStep}:添加邮箱页面正在切换,等待邮箱验证码页就绪...`,
|
||||||
|
logStep: visibleStep,
|
||||||
|
logStepKey: 'fetch-login-code',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayedEmail = normalizeStep8VerificationTargetEmail(result?.displayedEmail || resolvedEmail);
|
||||||
|
await setState({
|
||||||
|
email: resolvedEmail,
|
||||||
|
step8VerificationTargetEmail: displayedEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: {
|
||||||
|
...latestState,
|
||||||
|
email: resolvedEmail,
|
||||||
|
step8VerificationTargetEmail: displayedEmail,
|
||||||
|
},
|
||||||
|
pageState: {
|
||||||
|
state: result?.directOAuthConsentPage ? 'oauth_consent_page' : 'verification_page',
|
||||||
|
displayedEmail,
|
||||||
|
url: result?.url || pageState?.url || '',
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, options = {}) {
|
async function completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, options = {}) {
|
||||||
@@ -130,12 +220,69 @@
|
|||||||
return /add-phone|手机号页面|手机号验证页|phone[\s-_]verification|phone\s+number/i.test(message);
|
return /add-phone|手机号页面|手机号验证页|phone[\s-_]verification|phone\s+number/i.test(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isStep8EmailInUseError(error) {
|
||||||
|
const message = String(error?.message || error || '');
|
||||||
|
return /STEP8_EMAIL_IN_USE::|email_in_use|email\s+(?:address\s+)?already\s+exists|already\s+associated\s+with\s+this\s+email/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStep8MaxCheckAttemptsError(error) {
|
||||||
|
const message = String(error?.message || error || '');
|
||||||
|
return /AUTH_MAX_CHECK_ATTEMPTS::|max_check_attempts/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openStep8AddEmailPage(state, visibleStep, reasonLabel = '') {
|
||||||
|
const tabId = typeof getTabId === 'function' ? await getTabId('signup-page') : 0;
|
||||||
|
const url = STEP8_ADD_EMAIL_URL;
|
||||||
|
if (tabId && chrome?.tabs?.update) {
|
||||||
|
await chrome.tabs.update(tabId, { url, active: true });
|
||||||
|
} else if (typeof reuseOrCreateTab === 'function') {
|
||||||
|
await reuseOrCreateTab('signup-page', url);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Step ${visibleStep}: cannot reopen add-email page for Step 8 recovery.`);
|
||||||
|
}
|
||||||
|
if (typeof sleepWithStop === 'function') {
|
||||||
|
await sleepWithStop(1000);
|
||||||
|
}
|
||||||
|
await addLog(
|
||||||
|
`步骤 ${visibleStep}:重新打开添加邮箱页面${reasonLabel ? `(${reasonLabel})` : ''}。`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...(state || {}),
|
||||||
|
oauthUrl: state?.oauthUrl || url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetStep8AfterEmailInUse(state, visibleStep) {
|
||||||
|
const currentEmail = String(state?.email || '').trim();
|
||||||
|
await setState({
|
||||||
|
email: null,
|
||||||
|
step8VerificationTargetEmail: '',
|
||||||
|
loginVerificationRequestedAt: null,
|
||||||
|
});
|
||||||
|
if (currentEmail) {
|
||||||
|
await addLog(`步骤 ${visibleStep}:检测到邮箱 ${currentEmail} 已被占用,已清理运行态并准备重新获取新邮箱。`, 'warn');
|
||||||
|
} else {
|
||||||
|
await addLog(`步骤 ${visibleStep}:检测到邮箱已被占用,已清理运行态并准备重新获取新邮箱。`, 'warn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetStep8AfterMaxCheckAttempts(visibleStep) {
|
||||||
|
await setState({
|
||||||
|
step8VerificationTargetEmail: '',
|
||||||
|
loginVerificationRequestedAt: null,
|
||||||
|
});
|
||||||
|
await addLog(`步骤 ${visibleStep}:检测到 max_check_attempts,将重新开始当前添加邮箱步骤,不继续点击重试。`, 'warn');
|
||||||
|
}
|
||||||
|
|
||||||
async function recoverStep8PollingFailure(currentState, visibleStep) {
|
async function recoverStep8PollingFailure(currentState, visibleStep) {
|
||||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||||
try {
|
try {
|
||||||
const pageState = await ensureStep8VerificationPageReady({
|
const pageState = await ensureStep8VerificationPageReady({
|
||||||
visibleStep,
|
visibleStep,
|
||||||
authLoginStep,
|
authLoginStep,
|
||||||
|
allowPhoneVerificationPage: true,
|
||||||
|
allowAddEmailPage: true,
|
||||||
timeoutMs: await getStep8ReadyTimeoutMs(
|
timeoutMs: await getStep8ReadyTimeoutMs(
|
||||||
'登录验证码轮询异常后复核认证页状态',
|
'登录验证码轮询异常后复核认证页状态',
|
||||||
currentState?.oauthUrl || '',
|
currentState?.oauthUrl || '',
|
||||||
@@ -146,9 +293,9 @@
|
|||||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { fromRecovery: true });
|
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { fromRecovery: true });
|
||||||
return { outcome: 'completed' };
|
return { outcome: 'completed' };
|
||||||
}
|
}
|
||||||
if (pageState?.state === 'verification_page') {
|
if (pageState?.state === 'verification_page' || pageState?.state === 'phone_verification_page' || pageState?.state === 'add_email_page') {
|
||||||
await addLog(
|
await addLog(
|
||||||
`步骤 ${visibleStep}:检测到邮箱轮询/页面通信异常,但认证页仍在验证码页,先在当前链路重试,不回到步骤 ${authLoginStep}。`,
|
`步骤 ${visibleStep}:检测到邮箱轮询/页面通信异常,但认证页仍在当前登录后续页面,先在当前链路重试,不回到步骤 ${authLoginStep}。`,
|
||||||
'warn'
|
'warn'
|
||||||
);
|
);
|
||||||
return { outcome: 'retry_without_step7' };
|
return { outcome: 'retry_without_step7' };
|
||||||
@@ -250,40 +397,58 @@
|
|||||||
await reuseOrCreateTab('signup-page', state.oauthUrl);
|
await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPhoneLoginState(state)) {
|
|
||||||
return executeLoginPhoneCodeStep(state, authTabId, visibleStep);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mail = getMailConfig(state);
|
|
||||||
if (mail.error) throw new Error(mail.error);
|
|
||||||
const stateLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
|
const stateLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
|
||||||
let latestResendAt = Math.max(0, Number(runtime?.stickyLastResendAt) || 0, stateLastResendAt);
|
let latestResendAt = Math.max(0, Number(runtime?.stickyLastResendAt) || 0, stateLastResendAt);
|
||||||
const notifyResendRequestedAt = typeof runtime?.onResendRequestedAt === 'function'
|
const notifyResendRequestedAt = typeof runtime?.onResendRequestedAt === 'function'
|
||||||
? runtime.onResendRequestedAt
|
? runtime.onResendRequestedAt
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const stepStartedAt = Date.now();
|
|
||||||
const verificationFilterAfterTimestamp = mail.provider === '2925'
|
|
||||||
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
|
||||||
: stepStartedAt;
|
|
||||||
const verificationSessionKey = `8:${stepStartedAt}`;
|
|
||||||
|
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
const pageState = await ensureStep8VerificationPageReady({
|
let pageState = await ensureStep8VerificationPageReady({
|
||||||
visibleStep,
|
visibleStep,
|
||||||
authLoginStep: getAuthLoginStepForVisibleStep(visibleStep),
|
authLoginStep: getAuthLoginStepForVisibleStep(visibleStep),
|
||||||
|
allowPhoneVerificationPage: true,
|
||||||
|
allowAddEmailPage: true,
|
||||||
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep),
|
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep),
|
||||||
});
|
});
|
||||||
if (pageState?.state === 'oauth_consent_page') {
|
if (pageState?.state === 'oauth_consent_page') {
|
||||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep);
|
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (pageState?.state === 'phone_verification_page') {
|
||||||
|
return executeLoginPhoneCodeStep(state, authTabId, visibleStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
let preparedState = state;
|
||||||
|
const addEmailPreparation = await submitAddEmailIfNeeded(preparedState, visibleStep, pageState);
|
||||||
|
preparedState = addEmailPreparation?.state || preparedState;
|
||||||
|
pageState = addEmailPreparation?.pageState || pageState;
|
||||||
|
if (pageState?.state === 'oauth_consent_page') {
|
||||||
|
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pageState?.state === 'phone_verification_page') {
|
||||||
|
return executeLoginPhoneCodeStep(preparedState, authTabId, visibleStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
const preparedStateLastResendAt = Number(preparedState?.loginVerificationRequestedAt) || 0;
|
||||||
|
if (preparedStateLastResendAt > 0) {
|
||||||
|
latestResendAt = Math.max(latestResendAt, preparedStateLastResendAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mail = getMailConfig(preparedState);
|
||||||
|
if (mail.error) throw new Error(mail.error);
|
||||||
|
const stepStartedAt = Date.now();
|
||||||
|
const verificationFilterAfterTimestamp = mail.provider === '2925'
|
||||||
|
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||||
|
: stepStartedAt;
|
||||||
|
const verificationSessionKey = `8:${stepStartedAt}`;
|
||||||
const shouldCompareVerificationEmail = mail.provider !== '2925';
|
const shouldCompareVerificationEmail = mail.provider !== '2925';
|
||||||
const displayedVerificationEmail = shouldCompareVerificationEmail
|
const displayedVerificationEmail = shouldCompareVerificationEmail
|
||||||
? normalizeStep8VerificationTargetEmail(pageState?.displayedEmail)
|
? normalizeStep8VerificationTargetEmail(pageState?.displayedEmail)
|
||||||
: '';
|
: '';
|
||||||
const fixedTargetEmail = shouldCompareVerificationEmail
|
const fixedTargetEmail = shouldCompareVerificationEmail
|
||||||
? (displayedVerificationEmail || normalizeStep8VerificationTargetEmail(state?.email))
|
? (displayedVerificationEmail || normalizeStep8VerificationTargetEmail(preparedState?.email))
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
await setState({
|
await setState({
|
||||||
@@ -295,7 +460,7 @@
|
|||||||
await addLog(`步骤 ${visibleStep}:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info');
|
await addLog(`步骤 ${visibleStep}:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldUseCustomRegistrationEmail(state)) {
|
if (shouldUseCustomRegistrationEmail(preparedState)) {
|
||||||
await confirmCustomVerificationStepBypass(8, {
|
await confirmCustomVerificationStepBypass(8, {
|
||||||
completionStep: visibleStep,
|
completionStep: visibleStep,
|
||||||
promptStep: visibleStep,
|
promptStep: visibleStep,
|
||||||
@@ -306,7 +471,7 @@
|
|||||||
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
|
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
|
||||||
await addLog(`步骤 ${visibleStep}:正在确认 iCloud 邮箱登录态...`, 'info');
|
await addLog(`步骤 ${visibleStep}:正在确认 iCloud 邮箱登录态...`, 'info');
|
||||||
await ensureIcloudMailSession({
|
await ensureIcloudMailSession({
|
||||||
state,
|
state: preparedState,
|
||||||
step: 8,
|
step: 8,
|
||||||
actionLabel: `步骤 ${visibleStep}:确认 iCloud 邮箱登录态`,
|
actionLabel: `步骤 ${visibleStep}:确认 iCloud 邮箱登录态`,
|
||||||
});
|
});
|
||||||
@@ -323,10 +488,10 @@
|
|||||||
await addLog(`步骤 ${visibleStep}:正在打开${mail.label}...`);
|
await addLog(`步骤 ${visibleStep}:正在打开${mail.label}...`);
|
||||||
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
|
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
|
||||||
await ensureMail2925MailboxSession({
|
await ensureMail2925MailboxSession({
|
||||||
accountId: state.currentMail2925AccountId || null,
|
accountId: preparedState.currentMail2925AccountId || null,
|
||||||
forceRelogin: false,
|
forceRelogin: false,
|
||||||
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
|
allowLoginWhenOnLoginPage: Boolean(preparedState?.mail2925UseAccountPool),
|
||||||
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
|
expectedMailboxEmail: getExpectedMail2925MailboxEmail(preparedState),
|
||||||
actionLabel: `Step ${visibleStep}: ensure 2925 mailbox session`,
|
actionLabel: `Step ${visibleStep}: ensure 2925 mailbox session`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -338,14 +503,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await resolveVerificationStep(8, {
|
await resolveVerificationStep(8, {
|
||||||
...state,
|
...preparedState,
|
||||||
step8VerificationTargetEmail: displayedVerificationEmail || '',
|
step8VerificationTargetEmail: displayedVerificationEmail || '',
|
||||||
}, mail, {
|
}, mail, {
|
||||||
completionStep: visibleStep,
|
completionStep: visibleStep,
|
||||||
filterAfterTimestamp: verificationFilterAfterTimestamp,
|
filterAfterTimestamp: verificationFilterAfterTimestamp,
|
||||||
sessionKey: verificationSessionKey,
|
sessionKey: verificationSessionKey,
|
||||||
disableTimeBudgetCap: mail.provider === '2925',
|
disableTimeBudgetCap: mail.provider === '2925',
|
||||||
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || '', visibleStep),
|
getRemainingTimeMs: getStep8RemainingTimeResolver(preparedState?.oauthUrl || '', visibleStep),
|
||||||
requestFreshCodeFirst: false,
|
requestFreshCodeFirst: false,
|
||||||
lastResendAt: latestResendAt,
|
lastResendAt: latestResendAt,
|
||||||
onResendRequestedAt: async (requestedAt) => {
|
onResendRequestedAt: async (requestedAt) => {
|
||||||
@@ -381,6 +546,7 @@
|
|||||||
let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
|
let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
|
||||||
let retryWithoutStep7Streak = 0;
|
let retryWithoutStep7Streak = 0;
|
||||||
const maxRetryWithoutStep7Streak = 3;
|
const maxRetryWithoutStep7Streak = 3;
|
||||||
|
let currentStepRecoveryAttempt = 0;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
@@ -403,6 +569,28 @@
|
|||||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||||
let currentError = err;
|
let currentError = err;
|
||||||
let retryWithoutStep7 = false;
|
let retryWithoutStep7 = false;
|
||||||
|
|
||||||
|
if (isStep8EmailInUseError(currentError) || isStep8MaxCheckAttemptsError(currentError)) {
|
||||||
|
currentStepRecoveryAttempt += 1;
|
||||||
|
if (currentStepRecoveryAttempt > STEP8_CURRENT_STEP_RECOVERY_MAX_ATTEMPTS) {
|
||||||
|
throw currentError;
|
||||||
|
}
|
||||||
|
if (isStep8EmailInUseError(currentError)) {
|
||||||
|
await resetStep8AfterEmailInUse(currentState, visibleStep);
|
||||||
|
await openStep8AddEmailPage(currentState, visibleStep, 'email_in_use');
|
||||||
|
} else {
|
||||||
|
await resetStep8AfterMaxCheckAttempts(visibleStep);
|
||||||
|
await openStep8AddEmailPage(currentState, visibleStep, 'max_check_attempts');
|
||||||
|
}
|
||||||
|
const latestState = typeof getState === 'function' ? await getState() : currentState;
|
||||||
|
currentState = {
|
||||||
|
...(currentState || {}),
|
||||||
|
...(latestState || {}),
|
||||||
|
oauthUrl: currentState?.oauthUrl || latestState?.oauthUrl || STEP8_ADD_EMAIL_URL,
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const isMailPollingError = isVerificationMailPollingError(err);
|
const isMailPollingError = isVerificationMailPollingError(err);
|
||||||
if (isMailPollingError && !isStep8RestartStep7Error(err)) {
|
if (isMailPollingError && !isStep8RestartStep7Error(err)) {
|
||||||
const recovery = await recoverStep8PollingFailure(currentState, visibleStep);
|
const recovery = await recoverStep8PollingFailure(currentState, visibleStep);
|
||||||
|
|||||||
+727
-106
File diff suppressed because it is too large
Load Diff
@@ -44,5 +44,6 @@ test('logging/status add-phone detection ignores step 2 phone-entry switch failu
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
assert.equal(loggingStatus.getLoginAuthStateLabel('phone_verification_page'), '手机验证码页');
|
assert.equal(loggingStatus.getLoginAuthStateLabel('phone_verification_page'), '手机验证码页');
|
||||||
|
assert.equal(loggingStatus.getLoginAuthStateLabel('add_email_page'), '添加邮箱页');
|
||||||
assert.equal(loggingStatus.getLoginAuthStateLabel('oauth_consent_page'), 'OAuth 授权页');
|
assert.equal(loggingStatus.getLoginAuthStateLabel('oauth_consent_page'), 'OAuth 授权页');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,16 +85,23 @@ test('step 8 submits login verification directly without replaying step 7', asyn
|
|||||||
{ step8VerificationTargetEmail: 'display.user@example.com' },
|
{ step8VerificationTargetEmail: 'display.user@example.com' },
|
||||||
]);
|
]);
|
||||||
assert.deepStrictEqual(calls.ensureReadyOptions, [
|
assert.deepStrictEqual(calls.ensureReadyOptions, [
|
||||||
{ visibleStep: 8, authLoginStep: 7, timeoutMs: 5000 },
|
{
|
||||||
|
visibleStep: 8,
|
||||||
|
authLoginStep: 7,
|
||||||
|
allowPhoneVerificationPage: true,
|
||||||
|
allowAddEmailPage: true,
|
||||||
|
timeoutMs: 5000,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
assert.equal(calls.resolveOptions.completionStep, 8);
|
assert.equal(calls.resolveOptions.completionStep, 8);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('step 8 routes phone login verification through sms helper and skips mail provider setup', async () => {
|
test('step 8 keeps phone-registered accounts on email-code flow when page is email verification', async () => {
|
||||||
const calls = {
|
const calls = {
|
||||||
getMailConfigCalls: 0,
|
getMailConfigCalls: 0,
|
||||||
helperCalls: [],
|
helperCalls: [],
|
||||||
completions: [],
|
completions: [],
|
||||||
|
resolveCalls: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const executor = api.createStep8Executor({
|
const executor = api.createStep8Executor({
|
||||||
@@ -109,9 +116,7 @@ test('step 8 routes phone login verification through sms helper and skips mail p
|
|||||||
calls.completions.push({ step, payload });
|
calls.completions.push({ step, payload });
|
||||||
},
|
},
|
||||||
confirmCustomVerificationStepBypass: async () => {},
|
confirmCustomVerificationStepBypass: async () => {},
|
||||||
ensureStep8VerificationPageReady: async () => {
|
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page', displayedEmail: 'phone-user@example.com' }),
|
||||||
throw new Error('phone login branch should not probe email verification page readiness');
|
|
||||||
},
|
|
||||||
getOAuthFlowRemainingMs: async () => 5000,
|
getOAuthFlowRemainingMs: async () => 5000,
|
||||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
getMailConfig: () => {
|
getMailConfig: () => {
|
||||||
@@ -141,7 +146,7 @@ test('step 8 routes phone login verification through sms helper and skips mail p
|
|||||||
},
|
},
|
||||||
resolveSignupMethod: () => 'phone',
|
resolveSignupMethod: () => 'phone',
|
||||||
resolveVerificationStep: async () => {
|
resolveVerificationStep: async () => {
|
||||||
throw new Error('phone login branch should not call email verification flow');
|
calls.resolveCalls += 1;
|
||||||
},
|
},
|
||||||
rerunStep7ForStep8Recovery: async () => {
|
rerunStep7ForStep8Recovery: async () => {
|
||||||
throw new Error('phone login branch should not rerun step 7 in this test');
|
throw new Error('phone login branch should not rerun step 7 in this test');
|
||||||
@@ -164,6 +169,73 @@ test('step 8 routes phone login verification through sms helper and skips mail p
|
|||||||
oauthUrl: 'https://oauth.example/latest',
|
oauthUrl: 'https://oauth.example/latest',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assert.equal(calls.getMailConfigCalls, 1);
|
||||||
|
assert.equal(calls.resolveCalls, 1);
|
||||||
|
assert.deepStrictEqual(calls.helperCalls, []);
|
||||||
|
assert.deepStrictEqual(calls.completions, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('step 8 routes only a real phone verification page through sms helper', async () => {
|
||||||
|
const calls = {
|
||||||
|
getMailConfigCalls: 0,
|
||||||
|
helperCalls: [],
|
||||||
|
completions: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const executor = api.createStep8Executor({
|
||||||
|
addLog: async () => {},
|
||||||
|
chrome: {
|
||||||
|
tabs: {
|
||||||
|
update: async () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||||
|
completeStepFromBackground: async (step, payload) => {
|
||||||
|
calls.completions.push({ step, payload });
|
||||||
|
},
|
||||||
|
confirmCustomVerificationStepBypass: async () => {},
|
||||||
|
ensureStep8VerificationPageReady: async () => ({ state: 'phone_verification_page' }),
|
||||||
|
getOAuthFlowRemainingMs: async () => 5000,
|
||||||
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
|
getMailConfig: () => {
|
||||||
|
calls.getMailConfigCalls += 1;
|
||||||
|
return {
|
||||||
|
provider: 'qq',
|
||||||
|
label: 'QQ 邮箱',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getState: async () => ({}),
|
||||||
|
getTabId: async () => 1,
|
||||||
|
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||||
|
isTabAlive: async () => true,
|
||||||
|
isVerificationMailPollingError: () => false,
|
||||||
|
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||||
|
phoneVerificationHelpers: {
|
||||||
|
completeLoginPhoneVerificationFlow: async (tabId, options) => {
|
||||||
|
calls.helperCalls.push({ tabId, visibleStep: options.visibleStep, state: options.state });
|
||||||
|
return { code: '654321' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolveVerificationStep: async () => {
|
||||||
|
throw new Error('real phone verification branch should not call email verification flow');
|
||||||
|
},
|
||||||
|
rerunStep7ForStep8Recovery: async () => {
|
||||||
|
throw new Error('real phone verification branch should not rerun step 7 in this test');
|
||||||
|
},
|
||||||
|
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: 8,
|
||||||
|
accountIdentifierType: 'phone',
|
||||||
|
oauthUrl: 'https://oauth.example/latest',
|
||||||
|
});
|
||||||
|
|
||||||
assert.equal(calls.getMailConfigCalls, 0);
|
assert.equal(calls.getMailConfigCalls, 0);
|
||||||
assert.deepStrictEqual(calls.helperCalls, [
|
assert.deepStrictEqual(calls.helperCalls, [
|
||||||
{
|
{
|
||||||
@@ -172,10 +244,6 @@ test('step 8 routes phone login verification through sms helper and skips mail p
|
|||||||
state: {
|
state: {
|
||||||
visibleStep: 8,
|
visibleStep: 8,
|
||||||
accountIdentifierType: 'phone',
|
accountIdentifierType: 'phone',
|
||||||
signupPhoneCompletedActivation: {
|
|
||||||
activationId: 'signup-done',
|
|
||||||
phoneNumber: '66959916439',
|
|
||||||
},
|
|
||||||
oauthUrl: 'https://oauth.example/latest',
|
oauthUrl: 'https://oauth.example/latest',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -192,6 +260,96 @@ test('step 8 routes phone login verification through sms helper and skips mail p
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('step 8 submits add-email before polling the email verification code', async () => {
|
||||||
|
const calls = {
|
||||||
|
contentMessages: [],
|
||||||
|
resolvedStates: [],
|
||||||
|
setStates: [],
|
||||||
|
mailStates: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const executor = api.createStep8Executor({
|
||||||
|
addLog: async () => {},
|
||||||
|
chrome: {
|
||||||
|
tabs: {
|
||||||
|
update: async () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||||
|
confirmCustomVerificationStepBypass: async () => {},
|
||||||
|
ensureStep8VerificationPageReady: async () => ({ state: 'add_email_page', url: 'https://auth.openai.com/add-email' }),
|
||||||
|
getOAuthFlowRemainingMs: async () => 5000,
|
||||||
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
|
getMailConfig: (state) => {
|
||||||
|
calls.mailStates.push(state);
|
||||||
|
return {
|
||||||
|
provider: 'qq',
|
||||||
|
label: 'QQ 邮箱',
|
||||||
|
source: 'mail-qq',
|
||||||
|
url: 'https://mail.qq.com',
|
||||||
|
navigateOnReuse: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getState: async () => ({
|
||||||
|
email: '',
|
||||||
|
password: 'secret',
|
||||||
|
oauthUrl: 'https://oauth.example/latest',
|
||||||
|
}),
|
||||||
|
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
|
||||||
|
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||||
|
isTabAlive: async () => true,
|
||||||
|
isVerificationMailPollingError: () => false,
|
||||||
|
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||||
|
resolveSignupEmailForFlow: async (state) => {
|
||||||
|
calls.resolvedStates.push(state);
|
||||||
|
return 'new.user@example.com';
|
||||||
|
},
|
||||||
|
resolveVerificationStep: async (_step, state, _mail, options) => {
|
||||||
|
calls.resolvedVerification = { state, options };
|
||||||
|
},
|
||||||
|
rerunStep7ForStep8Recovery: async () => {},
|
||||||
|
reuseOrCreateTab: async () => {},
|
||||||
|
sendToContentScriptResilient: async (_source, message) => {
|
||||||
|
calls.contentMessages.push(message);
|
||||||
|
assert.equal(message.type, 'SUBMIT_ADD_EMAIL');
|
||||||
|
assert.equal(message.payload.email, 'new.user@example.com');
|
||||||
|
return {
|
||||||
|
submitted: true,
|
||||||
|
displayedEmail: 'new.user@example.com',
|
||||||
|
url: 'https://auth.openai.com/email-verification',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
setState: async (payload) => {
|
||||||
|
calls.setStates.push(payload);
|
||||||
|
},
|
||||||
|
shouldUseCustomRegistrationEmail: () => false,
|
||||||
|
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
|
||||||
|
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await executor.executeStep8({
|
||||||
|
visibleStep: 8,
|
||||||
|
accountIdentifierType: 'phone',
|
||||||
|
oauthUrl: 'https://oauth.example/latest',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(calls.contentMessages.length, 1);
|
||||||
|
assert.equal(calls.resolvedStates.length, 1);
|
||||||
|
assert.equal(calls.mailStates[0].email, 'new.user@example.com');
|
||||||
|
assert.equal(calls.resolvedVerification.state.email, 'new.user@example.com');
|
||||||
|
assert.equal(calls.resolvedVerification.options.targetEmail, 'new.user@example.com');
|
||||||
|
assert.deepStrictEqual(calls.setStates, [
|
||||||
|
{
|
||||||
|
email: 'new.user@example.com',
|
||||||
|
step8VerificationTargetEmail: 'new.user@example.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step8VerificationTargetEmail: 'new.user@example.com',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('Plus login-code step reuses step 8 verification logic but completes visible step 11', async () => {
|
test('Plus login-code step reuses step 8 verification logic but completes visible step 11', async () => {
|
||||||
let resolvedStep = null;
|
let resolvedStep = null;
|
||||||
let resolvedOptions = null;
|
let resolvedOptions = null;
|
||||||
@@ -257,7 +415,13 @@ test('Plus login-code step reuses step 8 verification logic but completes visibl
|
|||||||
assert.equal(resolvedStep, 8);
|
assert.equal(resolvedStep, 8);
|
||||||
assert.equal(resolvedOptions.completionStep, 11);
|
assert.equal(resolvedOptions.completionStep, 11);
|
||||||
assert.equal(resolvedOptions.targetEmail, 'plus.user@example.com');
|
assert.equal(resolvedOptions.targetEmail, 'plus.user@example.com');
|
||||||
assert.deepStrictEqual(readyOptions, { visibleStep: 11, authLoginStep: 10, timeoutMs: 9000 });
|
assert.deepStrictEqual(readyOptions, {
|
||||||
|
visibleStep: 11,
|
||||||
|
authLoginStep: 10,
|
||||||
|
allowPhoneVerificationPage: true,
|
||||||
|
allowAddEmailPage: true,
|
||||||
|
timeoutMs: 9000,
|
||||||
|
});
|
||||||
assert.deepStrictEqual(remainingStepCalls, [11, 11]);
|
assert.deepStrictEqual(remainingStepCalls, [11, 11]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ function isAddPhonePageReady() {
|
|||||||
return ${JSON.stringify(Boolean(overrides.addPhonePage))};
|
return ${JSON.stringify(Boolean(overrides.addPhonePage))};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAddEmailPageReady() {
|
||||||
|
return ${JSON.stringify(Boolean(overrides.addEmailPage))};
|
||||||
|
}
|
||||||
|
|
||||||
function isVisibleElement() {
|
function isVisibleElement() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -261,6 +265,20 @@ return {
|
|||||||
assert.strictEqual(snapshot.state, 'phone_entry_page');
|
assert.strictEqual(snapshot.state, 'phone_entry_page');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const api = createApi({
|
||||||
|
pathname: '/add-email',
|
||||||
|
href: 'https://auth.openai.com/add-email',
|
||||||
|
emailInput: { id: 'email' },
|
||||||
|
submitButton: { id: 'submit' },
|
||||||
|
addEmailPage: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = api.inspectLoginAuthState();
|
||||||
|
assert.strictEqual(snapshot.state, 'add_email_page');
|
||||||
|
assert.strictEqual(snapshot.addEmailPage, true);
|
||||||
|
}
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"),
|
extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"),
|
||||||
'inspectLoginAuthState 应产出 oauth_consent_page 状态'
|
'inspectLoginAuthState 应产出 oauth_consent_page 状态'
|
||||||
@@ -271,4 +289,9 @@ assert.ok(
|
|||||||
'inspectLoginAuthState 应产出 phone_entry_page 状态'
|
'inspectLoginAuthState 应产出 phone_entry_page 状态'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
extractFunction('inspectLoginAuthState').includes("state: 'add_email_page'"),
|
||||||
|
'inspectLoginAuthState 应产出 add_email_page 状态'
|
||||||
|
);
|
||||||
|
|
||||||
console.log('step6 login state tests passed');
|
console.log('step6 login state tests passed');
|
||||||
|
|||||||
@@ -68,13 +68,15 @@ function createPhoneLoginEntryApi(options = {}) {
|
|||||||
inputRootText = '',
|
inputRootText = '',
|
||||||
pageText = '',
|
pageText = '',
|
||||||
addPhoneForm = false,
|
addPhoneForm = false,
|
||||||
|
phoneUsernameKind = false,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
return new Function(`
|
return new Function(`
|
||||||
${extractConst('ADD_PHONE_PAGE_PATTERN')}
|
${extractConst('ADD_PHONE_PAGE_PATTERN')}
|
||||||
|
${extractConst('LOGIN_PHONE_ENTRY_PAGE_PATTERN')}
|
||||||
|
|
||||||
const location = {
|
const location = {
|
||||||
href: ${JSON.stringify(href)},
|
href: ${JSON.stringify(phoneUsernameKind ? `${href}${href.includes('?') ? '&' : '?'}usernameKind=phone_number` : href)},
|
||||||
pathname: ${JSON.stringify(pathname)},
|
pathname: ${JSON.stringify(pathname)},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,7 +109,7 @@ const document = {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
querySelectorAll(selector) {
|
querySelectorAll(selector) {
|
||||||
if (selector === 'input') return [phoneInput];
|
if (String(selector || '').includes('input')) return [phoneInput];
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
getElementById() {
|
getElementById() {
|
||||||
@@ -125,7 +127,18 @@ function isVisibleElement(element) {
|
|||||||
return Boolean(element);
|
return Boolean(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPhoneVerificationPageReady() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
${extractFunction('getPageTextSnapshot')}
|
${extractFunction('getPageTextSnapshot')}
|
||||||
|
${extractFunction('isLoginPhoneUsernameKind')}
|
||||||
|
${extractFunction('isLoginPhoneEntryPageText')}
|
||||||
|
${extractFunction('isInsideHiddenPhoneControl')}
|
||||||
|
${extractFunction('summarizePhoneInputCandidate')}
|
||||||
|
${extractFunction('isUsablePhoneInputElement')}
|
||||||
|
${extractFunction('collectPhoneInputCandidates')}
|
||||||
|
${extractFunction('findUsablePhoneInput')}
|
||||||
${extractFunction('getLoginPhoneInput')}
|
${extractFunction('getLoginPhoneInput')}
|
||||||
${extractFunction('isAddPhonePageReady')}
|
${extractFunction('isAddPhonePageReady')}
|
||||||
|
|
||||||
@@ -155,6 +168,26 @@ test('step 7 does not mistake email entry with a phone switch action for phone i
|
|||||||
assert.equal(api.isAddPhonePageReady(), false);
|
assert.equal(api.isAddPhonePageReady(), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('step 7 detects username text input when usernameKind is phone_number', () => {
|
||||||
|
const api = createPhoneLoginEntryApi({
|
||||||
|
phoneUsernameKind: true,
|
||||||
|
pageText: '\u6b22\u8fce\u56de\u6765',
|
||||||
|
inputAttributes: { type: 'text', name: 'username', autocomplete: 'username' },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.ok(api.getLoginPhoneInput(), 'username text input should be treated as phone on phone login url');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('step 7 ignores hidden phone inputs while resolving login phone entry', () => {
|
||||||
|
const api = createPhoneLoginEntryApi({
|
||||||
|
phoneUsernameKind: true,
|
||||||
|
pageText: '\u7535\u8bdd\u53f7\u7801',
|
||||||
|
inputAttributes: { type: 'hidden', name: 'phone' },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(api.getLoginPhoneInput(), null);
|
||||||
|
});
|
||||||
|
|
||||||
test('add-phone detection stays true for real add-phone urls and forms', () => {
|
test('add-phone detection stays true for real add-phone urls and forms', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
createPhoneLoginEntryApi({
|
createPhoneLoginEntryApi({
|
||||||
@@ -312,29 +345,73 @@ return {
|
|||||||
assert.deepEqual(api.getClicks(), ['\u6fb3\u5927\u5229\u4e9a (+61)', '\u82f1\u56fd +(44)']);
|
assert.deepEqual(api.getClicks(), ['\u6fb3\u5927\u5229\u4e9a (+61)', '\u82f1\u56fd +(44)']);
|
||||||
});
|
});
|
||||||
|
|
||||||
function createPhoneFillApi(fillBehavior) {
|
function createPhoneFillApi(fillBehavior, options = {}) {
|
||||||
|
const {
|
||||||
|
initialValue = '+44',
|
||||||
|
} = options;
|
||||||
|
|
||||||
return new Function('fillBehavior', `
|
return new Function('fillBehavior', `
|
||||||
const fills = [];
|
const fills = [];
|
||||||
const phoneInput = {
|
const phoneInput = {
|
||||||
value: '+44',
|
value: ${JSON.stringify(initialValue)},
|
||||||
|
form: null,
|
||||||
getAttribute(name) {
|
getAttribute(name) {
|
||||||
return name === 'value' ? this.value : '';
|
return name === 'value' ? this.value : '';
|
||||||
},
|
},
|
||||||
|
focus() {},
|
||||||
|
closest() {
|
||||||
|
return this.form;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hiddenPhoneInput = {
|
||||||
|
type: 'hidden',
|
||||||
|
value: '',
|
||||||
|
events: [],
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'type') return 'hidden';
|
||||||
|
if (name === 'name') return 'phone';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
dispatchEvent(event) {
|
||||||
|
this.events.push(event.type);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const root = {
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
if (String(selector).includes('input[name="phone"]')) {
|
||||||
|
return [hiddenPhoneInput];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
phoneInput.form = root;
|
||||||
|
|
||||||
function fillInput(input, value) {
|
function fillInput(input, value) {
|
||||||
fills.push(value);
|
fills.push(value);
|
||||||
fillBehavior(input, value);
|
fillBehavior(input, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sleep() {}
|
async function sleep() {}
|
||||||
|
function throwIfStopped() {}
|
||||||
|
function isVisibleElement() { return false; }
|
||||||
|
function log() {}
|
||||||
|
|
||||||
${extractFunction('normalizePhoneDigits')}
|
${extractFunction('normalizePhoneDigits')}
|
||||||
${extractFunction('toNationalPhoneNumber')}
|
${extractFunction('toNationalPhoneNumber')}
|
||||||
${extractFunction('toE164PhoneNumber')}
|
${extractFunction('toE164PhoneNumber')}
|
||||||
${extractFunction('getPhoneInputRenderedValue')}
|
${extractFunction('getPhoneInputRenderedValue')}
|
||||||
|
${extractFunction('isPhoneInputValueVerified')}
|
||||||
|
${extractFunction('waitForPhoneInputValue')}
|
||||||
|
${extractFunction('formatPhoneHiddenFormValue')}
|
||||||
|
${extractFunction('getPhoneHiddenValueInput')}
|
||||||
|
${extractFunction('setPhoneHiddenValue')}
|
||||||
|
${extractFunction('syncPhoneHiddenFormValue')}
|
||||||
${extractFunction('isPhoneInputValueComplete')}
|
${extractFunction('isPhoneInputValueComplete')}
|
||||||
${extractFunction('getLoginPhoneFillCandidates')}
|
${extractFunction('getLoginPhoneFillCandidates')}
|
||||||
|
${extractFunction('getLoginPhoneInputDiagnostics')}
|
||||||
|
${extractFunction('getLoginPhoneHiddenValueDiagnostics')}
|
||||||
${extractFunction('fillLoginPhoneInputAndConfirm')}
|
${extractFunction('fillLoginPhoneInputAndConfirm')}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -343,6 +420,7 @@ return {
|
|||||||
phoneNumber: '447780579093',
|
phoneNumber: '447780579093',
|
||||||
dialCode: '44',
|
dialCode: '44',
|
||||||
visibleStep: 7,
|
visibleStep: 7,
|
||||||
|
maxAttempts: 2,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getFills() {
|
getFills() {
|
||||||
@@ -351,13 +429,19 @@ return {
|
|||||||
getValue() {
|
getValue() {
|
||||||
return phoneInput.value;
|
return phoneInput.value;
|
||||||
},
|
},
|
||||||
|
getHiddenValue() {
|
||||||
|
return hiddenPhoneInput.value;
|
||||||
|
},
|
||||||
|
getHiddenEvents() {
|
||||||
|
return hiddenPhoneInput.events.slice();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
`)(fillBehavior);
|
`)(fillBehavior);
|
||||||
}
|
}
|
||||||
|
|
||||||
test('step 7 retries phone fill with e164 when the auth input keeps only the dial code', async () => {
|
test('step 7 keeps visible dial prefix when filling phone login and syncs the hidden value', async () => {
|
||||||
const api = createPhoneFillApi((input, value) => {
|
const api = createPhoneFillApi((input, value) => {
|
||||||
input.value = value === '7780579093' ? '+44' : value;
|
input.value = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await api.run();
|
const result = await api.run();
|
||||||
@@ -365,7 +449,24 @@ test('step 7 retries phone fill with e164 when the auth input keeps only the dia
|
|||||||
assert.equal(result.inputValue, '7780579093');
|
assert.equal(result.inputValue, '7780579093');
|
||||||
assert.equal(result.attemptedValue, '+447780579093');
|
assert.equal(result.attemptedValue, '+447780579093');
|
||||||
assert.equal(api.getValue(), '+447780579093');
|
assert.equal(api.getValue(), '+447780579093');
|
||||||
assert.deepEqual(api.getFills(), ['7780579093', '+447780579093']);
|
assert.equal(api.getHiddenValue(), '+447780579093');
|
||||||
|
assert.deepEqual(api.getHiddenEvents(), ['input', 'change']);
|
||||||
|
assert.deepEqual(api.getFills(), ['+447780579093']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('step 7 keeps national phone fill when visible login input has no dial prefix', async () => {
|
||||||
|
const api = createPhoneFillApi((input, value) => {
|
||||||
|
input.value = value;
|
||||||
|
}, { initialValue: '' });
|
||||||
|
|
||||||
|
const result = await api.run();
|
||||||
|
|
||||||
|
assert.equal(result.inputValue, '7780579093');
|
||||||
|
assert.equal(result.attemptedValue, '7780579093');
|
||||||
|
assert.equal(api.getValue(), '7780579093');
|
||||||
|
assert.equal(api.getHiddenValue(), '+447780579093');
|
||||||
|
assert.deepEqual(api.getHiddenEvents(), ['input', 'change']);
|
||||||
|
assert.deepEqual(api.getFills(), ['7780579093']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('step 7 stops before submit when phone fill never includes the local number', async () => {
|
test('step 7 stops before submit when phone fill never includes the local number', async () => {
|
||||||
@@ -375,5 +476,5 @@ test('step 7 stops before submit when phone fill never includes the local number
|
|||||||
|
|
||||||
await assert.rejects(api.run, /7780579093/);
|
await assert.rejects(api.run, /7780579093/);
|
||||||
assert.equal(api.getValue(), '+44');
|
assert.equal(api.getValue(), '+44');
|
||||||
assert.deepEqual(api.getFills(), ['7780579093', '+447780579093', '447780579093']);
|
assert.deepEqual(api.getFills(), ['+447780579093', '7780579093', '+447780579093', '7780579093']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -115,6 +115,37 @@ return {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ensureStep8VerificationPageReady allows add-email handoff only when requested', async () => {
|
||||||
|
const api = new Function(`
|
||||||
|
function getLoginAuthStateLabel(state) {
|
||||||
|
return state === 'add_email_page' ? '添加邮箱页' : 'unknown page';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLoginAuthStateFromContent() {
|
||||||
|
return {
|
||||||
|
state: 'add_email_page',
|
||||||
|
url: 'https://auth.openai.com/add-email',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
${extractFunction(backgroundSource, 'ensureStep8VerificationPageReady')}
|
||||||
|
|
||||||
|
return {
|
||||||
|
run(options) {
|
||||||
|
return ensureStep8VerificationPageReady(options || {});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`)();
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => api.run({}),
|
||||||
|
/当前未进入登录验证码页面/
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await api.run({ allowAddEmailPage: true });
|
||||||
|
assert.equal(result.state, 'add_email_page');
|
||||||
|
});
|
||||||
|
|
||||||
test('step 8 reruns step 7 when auth page enters login timeout retry state', async () => {
|
test('step 8 reruns step 7 when auth page enters login timeout retry state', async () => {
|
||||||
const calls = {
|
const calls = {
|
||||||
rerunStep7: 0,
|
rerunStep7: 0,
|
||||||
|
|||||||
+16
-13
@@ -366,9 +366,9 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
|
|||||||
|
|
||||||
这两步共享验证码主流程:
|
这两步共享验证码主流程:
|
||||||
|
|
||||||
1. 先按账号身份分流验证码通道:
|
1. 先按注册阶段或认证页真实状态分流验证码通道:
|
||||||
Step 4:邮箱注册走邮箱验证码,手机号注册走短信验证码
|
Step 4:邮箱注册走邮箱验证码,手机号注册走短信验证码
|
||||||
Step 8:邮箱登录走邮箱验证码,手机号登录走短信验证码
|
Step 8:真实邮箱验证码页走邮箱验证码,真实手机验证码页才走短信验证码;如果先进入 `add-email`,先绑定邮箱再走邮箱验证码
|
||||||
2. 确定 provider
|
2. 确定 provider
|
||||||
3. 必要时重发验证码
|
3. 必要时重发验证码
|
||||||
4. 轮询邮箱、短信 API 或对应 provider 通道
|
4. 轮询邮箱、短信 API 或对应 provider 通道
|
||||||
@@ -438,7 +438,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
|
|||||||
- 邮箱账号:沿用现有邮箱登录链路
|
- 邮箱账号:沿用现有邮箱登录链路
|
||||||
- 手机号账号:优先探测登录页是否存在手机号登录入口,必要时展开更多选项或从邮箱页切到手机号页;进入手机号输入页后会复用 Step 2 的可视国家下拉框同步逻辑,按本轮手机号最长区号切到目标国家,填写后还会读回输入框确认已包含本地号码,避免英国 `+44` 号码仍按默认澳大利亚 `+61` 提交或只提交区号
|
- 手机号账号:优先探测登录页是否存在手机号登录入口,必要时展开更多选项或从邮箱页切到手机号页;进入手机号输入页后会复用 Step 2 的可视国家下拉框同步逻辑,按本轮手机号最长区号切到目标国家,填写后还会读回输入框确认已包含本地号码,避免英国 `+44` 号码仍按默认澳大利亚 `+61` 提交或只提交区号
|
||||||
4. 登录;如果进入密码页且当前有密码,则填写并提交密码;如果当前没有密码但检测到一次性验证码入口,则直接切换到一次性验证码登录
|
4. 登录;如果进入密码页且当前有密码,则填写并提交密码;如果当前没有密码但检测到一次性验证码入口,则直接切换到一次性验证码登录
|
||||||
5. 确保真正进入验证码页;手机号登录允许以 `phone-verification` 作为 Step 7 的成功落点
|
5. 确保真正进入验证码页或登录后续页;手机号登录允许以 `phone-verification` 作为 Step 7 的成功落点,手机号注册后的 OAuth 登录也允许以真实 `add-email` 作为 Step 7 完成并交给 Step 8 接管
|
||||||
6. 如果页面没有可用的手机号登录入口,则 Step 7 直接抛出明确业务错误,不再误进入 Step 8 邮箱轮询
|
6. 如果页面没有可用的手机号登录入口,则 Step 7 直接抛出明确业务错误,不再误进入 Step 8 邮箱轮询
|
||||||
7. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次;但一旦当前失败已经明确进入 `https://auth.openai.com/add-phone`,则立即退出步骤 7 内部重试,不再继续第 2 / 3 次尝试
|
7. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次;但一旦当前失败已经明确进入 `https://auth.openai.com/add-phone`,则立即退出步骤 7 内部重试,不再继续第 2 / 3 次尝试
|
||||||
8. 自动运行一旦进入步骤 7 之后的链路,若后续步骤报错且认证页未进入 `https://auth.openai.com/add-phone`,则统一回到步骤 7 重新开始授权流程
|
8. 自动运行一旦进入步骤 7 之后的链路,若后续步骤报错且认证页未进入 `https://auth.openai.com/add-phone`,则统一回到步骤 7 重新开始授权流程
|
||||||
@@ -457,19 +457,21 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
|
|||||||
|
|
||||||
流程:
|
流程:
|
||||||
|
|
||||||
1. 先按账号身份判断当前登录验证码通道
|
1. 先确认当前认证页真实状态,而不是只看 `accountIdentifierType`。
|
||||||
2. 邮箱账号:
|
2. 当前是 `add-email`:
|
||||||
- 确认当前认证页已经进入邮箱验证码页
|
- 调用 `resolveSignupEmailForFlow` 获取或生成本轮绑定邮箱
|
||||||
|
- 内容脚本发送 `SUBMIT_ADD_EMAIL` 填写邮箱并提交
|
||||||
|
- 页面进入邮箱验证码页后,用绑定邮箱作为 `step8VerificationTargetEmail`
|
||||||
|
3. 当前是邮箱验证码页:
|
||||||
- 对非 2925 provider,固定当前验证码页显示邮箱为本次 Step 8 的目标邮箱
|
- 对非 2925 provider,固定当前验证码页显示邮箱为本次 Step 8 的目标邮箱
|
||||||
- 打开邮箱页或 API 轮询入口
|
- 打开邮箱页或 API 轮询入口
|
||||||
- 轮询登录验证码并回填
|
- 轮询登录验证码并回填
|
||||||
3. 手机号账号:
|
4. 当前是真实 `phone-verification` 页:
|
||||||
- 直接复用注册成功后的同一手机号订单,不重新申请新号码
|
- 才复用手机号验证码共享流程,重新激活同一号码并轮询短信验证码
|
||||||
- 重新激活该号码,轮询短信验证码
|
|
||||||
- 在当前 `phone-verification` 页回填短信验证码
|
- 在当前 `phone-verification` 页回填短信验证码
|
||||||
4. 如果登录验证码提交后页面进入 `add-phone / 手机号页`,则步骤 8 会保留“登录验证码已提交成功”的结果,并把后续手机号验证需求继续交给步骤 9 处理
|
5. 如果登录验证码提交后页面进入 `add-phone / 手机号页`,则邮箱注册模式下交给后续 OAuth 后置手机号验证链路;手机号注册模式不把 `signupPhone*` 身份误当成官方 add-phone 订单
|
||||||
5. 如遇邮箱轮询类失败或显式的 `STEP8_RESTART_STEP7` 恢复错误,则按有限次数回到 Step 7 重试
|
6. 如遇邮箱轮询类失败或显式的 `STEP8_RESTART_STEP7` 恢复错误,则按有限次数回到 Step 7 重试;若仍停在验证码页、手机验证码页或 add-email 页,则优先在当前链路重试
|
||||||
6. 获取到登录验证码后不再触发“刷新 OAuth 并重走 Step 7”的前置回放,直接在当前验证码页提交并继续进入 Step 9
|
7. 获取到登录验证码后不再触发“刷新 OAuth 并重走 Step 7”的前置回放,直接在当前验证码页提交并继续进入 Step 9
|
||||||
|
|
||||||
补充:
|
补充:
|
||||||
|
|
||||||
@@ -477,7 +479,8 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
|
|||||||
- 对 `2925 receive` 而言,Step 8 会把当前目标注册邮箱一并传给 2925 内容脚本;只有当邮件里显式写出了其他邮箱时才会跳过,从而在不破坏历史兼容性的前提下,尽量降低误收验证码的概率。
|
- 对 `2925 receive` 而言,Step 8 会把当前目标注册邮箱一并传给 2925 内容脚本;只有当邮件里显式写出了其他邮箱时才会跳过,从而在不破坏历史兼容性的前提下,尽量降低误收验证码的概率。
|
||||||
- 对 `custom` provider 而言,Step 8 仍使用手动验证码确认弹窗;弹窗当前额外提供“出现手机号验证”按钮,点击后会直接抛出与真实 `add-phone` 页面一致的 fatal 错误,供 Auto 按既有 add-phone 分支继续下一邮箱。
|
- 对 `custom` provider 而言,Step 8 仍使用手动验证码确认弹窗;弹窗当前额外提供“出现手机号验证”按钮,点击后会直接抛出与真实 `add-phone` 页面一致的 fatal 错误,供 Auto 按既有 add-phone 分支继续下一邮箱。
|
||||||
- 当登录验证码提交后进入 `phone-verification` 页时,内容脚本会显式把该页面识别为“手机验证码页”,避免与邮箱验证码页混淆。
|
- 当登录验证码提交后进入 `phone-verification` 页时,内容脚本会显式把该页面识别为“手机验证码页”,避免与邮箱验证码页混淆。
|
||||||
- 手机号登录分支不会再读取邮箱 provider 配置,也不会误调用邮箱轮询;如果缺少可复用的注册手机号激活记录,会直接要求回到步骤 7 重新开始登录。
|
- 手机号注册身份本身不会让 Step 8 自动走短信;只有真实页面状态是 `phone_verification_page` 时才调用短信 helper。手机号注册登录后出现 `add-email` 时,Step 8 走“绑定邮箱 -> 邮箱验证码”链路。
|
||||||
|
- `email_in_use` 会在 Step 8 当前步骤内清理当前邮箱并重新打开 `add-email` 获取新邮箱;`max_check_attempts` 不继续点击认证页 `重试`,只恢复当前 Step 8。
|
||||||
|
|
||||||
### Step 9
|
### Step 9
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -54,7 +54,7 @@
|
|||||||
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;`LOG / STEP_COMPLETE / STEP_ERROR` 会把消息里的真实 `step` 继续传给结构化日志,跳过登录验证码这类派生日志也按当前步骤 key 写入;当前额外接入 PayPal 账号池的新增与切换、2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息;当侧栏关闭 `oauthFlowTimeoutEnabled` 时,会立即清空已存在的 OAuth 总预算 deadline。
|
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;`LOG / STEP_COMPLETE / STEP_ERROR` 会把消息里的真实 `step` 继续传给结构化日志,跳过登录验证码这类派生日志也按当前步骤 key 写入;当前额外接入 PayPal 账号池的新增与切换、2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息;当侧栏关闭 `oauthFlowTimeoutEnabled` 时,会立即清空已存在的 OAuth 总预算 deadline。
|
||||||
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。
|
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。
|
||||||
- `background/paypal-account-store.js`:PayPal 账号池持久化模块,负责保存账号列表、切换当前账号,并把当前选中账号同步回兼容字段 `paypalEmail / paypalPassword`。
|
- `background/paypal-account-store.js`:PayPal 账号池持久化模块,负责保存账号列表、切换当前账号,并把当前选中账号同步回兼容字段 `paypalEmail / paypalPassword`。
|
||||||
- `background/phone-verification-flow.js`:手机号验证码共享流程模块,负责复用 HeroSMS / 5sim / NexSMS 的取号、复用、轮询、重发、换号与收尾能力;既承接 OAuth 后置 `add-phone / phone-verification` 页面,也承接手机号注册 Step 4 和手机号 OAuth 登录 Step 8,并保持 `signupPhone*` 运行态与 `currentPhoneActivation` 隔离;该流程由调用方传入当前可见步骤号,手机号日志不会再固定显示普通模式 Step 9。
|
- `background/phone-verification-flow.js`:手机号验证码共享流程模块,负责复用 HeroSMS / 5sim / NexSMS 的取号、复用、轮询、重发、换号与收尾能力;既承接 OAuth 后置 `add-phone / phone-verification` 页面,也承接手机号注册 Step 4 和 Step 8 中真实出现的 `phone-verification` 页面,并保持 `signupPhone*` 运行态与 `currentPhoneActivation` 隔离;该流程由调用方传入当前可见步骤号,手机号日志不会再固定显示普通模式 Step 9。
|
||||||
- `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。
|
- `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。
|
||||||
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。
|
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。
|
||||||
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;内容脚本恢复等待日志支持 `logStep / logStepKey`,用于保持 Plus 复用步骤的日志标签正确;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
|
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;内容脚本恢复等待日志支持 `logStep / logStepKey`,用于保持 Plus 复用步骤的日志标签正确;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
- `background/steps/clear-login-cookies.js`:步骤 6 实现,负责登录前 Cookie 清理。
|
- `background/steps/clear-login-cookies.js`:步骤 6 实现,负责登录前 Cookie 清理。
|
||||||
- `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成;等待 callback 期间会动态检查 OAuth 总预算开关,关闭后仅保留本地回调等待超时。
|
- `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成;等待 callback 期间会动态检查 OAuth 总预算开关,关闭后仅保留本地回调等待超时。
|
||||||
- `background/steps/create-plus-checkout.js`:Plus 模式第 6 步实现,负责在已登录 ChatGPT 页面创建 Plus checkout session,打开 `chatgpt.com/checkout/openai_ie/{checkout_session_id}` 短链,并记录 checkout 运行态。
|
- `background/steps/create-plus-checkout.js`:Plus 模式第 6 步实现,负责在已登录 ChatGPT 页面创建 Plus checkout session,打开 `chatgpt.com/checkout/openai_ie/{checkout_session_id}` 短链,并记录 checkout 运行态。
|
||||||
- `background/steps/fetch-login-code.js`:步骤 8 实现,负责按账号身份分发登录验证码流程:邮箱账号继续走邮箱轮询、验证码回填与回退控制;手机号账号则直接复用注册成功后的同一手机号订单,轮询短信验证码并在当前认证页提交;邮箱分支对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱,当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录。
|
- `background/steps/fetch-login-code.js`:步骤 8 实现,负责按真实认证页状态分发登录后续流程:邮箱验证码页走邮箱轮询、验证码回填与回退控制;真实 `phone-verification` 页才复用手机号验证码共享流程;手机号注册登录后若进入 `add-email`,会先生成/解析邮箱并提交绑定,再进入邮箱验证码轮询;邮箱分支对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱,当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录。
|
||||||
- `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;邮箱注册继续走邮箱验证码,手机号注册改走短信验证码;当 provider 为 2925 时,会在邮箱轮询前先确保当前 2925 账号已自动登录。
|
- `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;邮箱注册继续走邮箱验证码,手机号注册改走短信验证码;当 provider 为 2925 时,会在邮箱轮询前先确保当前 2925 账号已自动登录。
|
||||||
- `background/steps/fill-plus-checkout.js`:Plus 模式第 7 步实现,负责驱动 checkout 页面选择 PayPal、生成账单姓名、读取本地地址 seed、触发地址推荐、提交订阅并等待跳转到 PayPal。
|
- `background/steps/fill-plus-checkout.js`:Plus 模式第 7 步实现,负责驱动 checkout 页面选择 PayPal、生成账单姓名、读取本地地址 seed、触发地址推荐、提交订阅并等待跳转到 PayPal。
|
||||||
- `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交;当前已改为身份中立,手机号注册遇到无密码页时会按页面状态自动跳过。
|
- `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交;当前已改为身份中立,手机号注册遇到无密码页时会按页面状态自动跳过。
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
- `content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。
|
- `content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。
|
||||||
- `content/plus-checkout.js`:ChatGPT Plus checkout 页面脚本,负责读取 `/api/auth/session` 创建 Plus checkout、选择 PayPal、填写账单姓名、触发 Google 地址推荐、校验结构化地址字段并点击订阅。
|
- `content/plus-checkout.js`:ChatGPT Plus checkout 页面脚本,负责读取 `/api/auth/session` 创建 Plus checkout、选择 PayPal、填写账单姓名、触发 Google 地址推荐、校验结构化地址字段并点击订阅。
|
||||||
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
|
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
|
||||||
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、手机号国家下拉框同步校验、手机号填写与提交;Step 7 登录链路会额外识别手机号输入页、手机号登录入口和 `phone-verification` 页面,避免把手机验证码页误判成邮箱验证码页或普通未知页。
|
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、手机号国家下拉框同步校验、手机号填写与提交;Step 7 / 8 登录链路会额外识别手机号输入页、手机号登录入口、`phone-verification` 与真实 `add-email` 页面,避免把手机验证码页或添加邮箱页误判成普通邮箱登录入口。
|
||||||
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;平台验证请求会读取 `visibleStep`,普通模式承接步骤 10,Plus 模式承接步骤 13。
|
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;平台验证请求会读取 `visibleStep`,普通模式承接步骤 10,Plus 模式承接步骤 13。
|
||||||
- `content/utils.js`:内容脚本公共工具层,负责结构化日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击;内容脚本日志通过 `{ step, stepKey }` 上报,正文不再承担步骤号解析职责。
|
- `content/utils.js`:内容脚本公共工具层,负责结构化日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击;内容脚本日志通过 `{ step, stepKey }` 上报,正文不再承担步骤号解析职责。
|
||||||
- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做平台验证判定;当前支持普通步骤 10 与 Plus 可见步骤 13。
|
- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做平台验证判定;当前支持普通步骤 10 与 Plus 可见步骤 13。
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
- `tests/background-step-modules.test.js`:测试步骤模块文件都已由后台入口加载。
|
- `tests/background-step-modules.test.js`:测试步骤模块文件都已由后台入口加载。
|
||||||
- `tests/background-step5-submit-short-circuit.test.js`:测试步骤 5 会把生成好的资料直接转发给内容脚本,并依赖完成信号收尾。
|
- `tests/background-step5-submit-short-circuit.test.js`:测试步骤 5 会把生成好的资料直接转发给内容脚本,并依赖完成信号收尾。
|
||||||
- `tests/background-step6-retry-limit.test.js`:测试步骤 6 的 Cookie 清理,以及步骤 7 的有限重试上限与 `add-phone` 命中后的立即跳出行为。
|
- `tests/background-step6-retry-limit.test.js`:测试步骤 6 的 Cookie 清理,以及步骤 7 的有限重试上限与 `add-phone` 命中后的立即跳出行为。
|
||||||
- `tests/background-step7-recovery.test.js`:测试步骤 8 获取登录验证码后直接提交(不再回放步骤 7),并为 2925 关闭重发间隔。
|
- `tests/background-step7-recovery.test.js`:测试步骤 8 获取登录验证码后直接提交(不再回放步骤 7),覆盖邮箱验证码页、真实手机验证码页、`add-email` 绑定邮箱后收码、2925 固定回看窗口与关闭重发间隔。
|
||||||
- `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。
|
- `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。
|
||||||
- `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂,并覆盖等待标签完成、等待标签稳定完成,以及等待过程中的 Stop 中断行为。
|
- `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂,并覆盖等待标签完成、等待标签稳定完成,以及等待过程中的 Stop 中断行为。
|
||||||
- `tests/background-verification-flow-module.test.js`:测试验证码流程模块已接入且导出工厂。
|
- `tests/background-verification-flow-module.test.js`:测试验证码流程模块已接入且导出工厂。
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
- `tests/step3-direct-complete.test.js`:测试步骤 3 在提交前先完成上报,并保留延迟提交回调的可执行性验证。
|
- `tests/step3-direct-complete.test.js`:测试步骤 3 在提交前先完成上报,并保留延迟提交回调的可执行性验证。
|
||||||
- `tests/step5-age-consent.test.js`:测试步骤 5 在年龄页会先勾选顶部“我同意以下所有各项”复选框,再点击提交。
|
- `tests/step5-age-consent.test.js`:测试步骤 5 在年龄页会先勾选顶部“我同意以下所有各项”复选框,再点击提交。
|
||||||
- `tests/step5-direct-complete.test.js`:测试步骤 5 在资料页点击提交后立即完成当前步骤。
|
- `tests/step5-direct-complete.test.js`:测试步骤 5 在资料页点击提交后立即完成当前步骤。
|
||||||
- `tests/step6-login-state.test.js`:测试 OAuth 登录状态识别逻辑,当前对应步骤 7 链路,并覆盖 `phone-verification` 页面不会被误判成邮箱验证码页。
|
- `tests/step6-login-state.test.js`:测试 OAuth 登录状态识别逻辑,当前对应步骤 7 链路,并覆盖 `phone-verification` 页面不会被误判成邮箱验证码页、真实 `add-email` 页面不会被误判成普通邮箱输入页。
|
||||||
- `tests/step6-timeout-recovery.test.js`:测试 OAuth 登录超时报错页的可恢复结果构造,当前对应步骤 7 链路。
|
- `tests/step6-timeout-recovery.test.js`:测试 OAuth 登录超时报错页的可恢复结果构造,当前对应步骤 7 链路。
|
||||||
- `tests/step7-phone-login-entry.test.js`:测试 OAuth 手机号登录入口识别、真实 add-phone 与手机号登录页区分、慢切换等待窗口、手机号登录页可视国家下拉框会按本轮号码区号自动切换,以及输入框只剩区号时不会继续提交。
|
- `tests/step7-phone-login-entry.test.js`:测试 OAuth 手机号登录入口识别、真实 add-phone 与手机号登录页区分、慢切换等待窗口、手机号登录页可视国家下拉框会按本轮号码区号自动切换,以及输入框只剩区号时不会继续提交。
|
||||||
- `tests/step8-callback-handling.test.js`:测试 localhost 回调地址捕获逻辑,当前对应步骤 9。
|
- `tests/step8-callback-handling.test.js`:测试 localhost 回调地址捕获逻辑,当前对应步骤 9。
|
||||||
|
|||||||
Reference in New Issue
Block a user