手机号注册模式

This commit is contained in:
QLHazyCoder
2026-05-04 12:56:35 +08:00
parent ff8b86a03b
commit c04b64c966
38 changed files with 4509 additions and 540 deletions
+102 -12
View File
@@ -90,8 +90,61 @@
return '流程失败';
}
function buildRecordId(email = '') {
return String(email || '').trim().toLowerCase();
function normalizeAccountIdentifierType(value = '') {
return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
}
function normalizeAccountIdentifierValue(value = '', identifierType = 'email') {
const normalizedValue = String(value || '').trim();
if (!normalizedValue) {
return '';
}
return normalizeAccountIdentifierType(identifierType) === 'phone'
? normalizedValue
: normalizedValue.toLowerCase();
}
function resolveRecordIdentity(record = {}) {
const email = String(record.email || '').trim().toLowerCase();
const phoneNumber = String(record.phoneNumber ?? record.phone ?? record.number ?? '').trim();
const rawIdentifierType = String(record.accountIdentifierType || '').trim().toLowerCase();
const inferredIdentifierType = rawIdentifierType === 'phone'
|| (!email && phoneNumber)
? 'phone'
: 'email';
const rawAccountIdentifier = String(
record.accountIdentifier
|| (inferredIdentifierType === 'phone' ? phoneNumber : email)
|| ''
).trim();
const accountIdentifierType = rawAccountIdentifier
? normalizeAccountIdentifierType(inferredIdentifierType)
: (email ? 'email' : (phoneNumber ? 'phone' : ''));
const accountIdentifier = normalizeAccountIdentifierValue(
rawAccountIdentifier || (accountIdentifierType === 'phone' ? phoneNumber : email),
accountIdentifierType || inferredIdentifierType
);
return {
email,
phoneNumber,
accountIdentifierType,
accountIdentifier,
};
}
function buildRecordId(identifier = '', identifierType = 'email') {
const normalizedIdentifierType = normalizeAccountIdentifierType(identifierType);
const normalizedIdentifier = normalizeAccountIdentifierValue(identifier, normalizedIdentifierType);
if (!normalizedIdentifier) {
return '';
}
if (normalizedIdentifierType === 'phone' && /^phone:/i.test(normalizedIdentifier)) {
return normalizedIdentifier.toLowerCase();
}
return normalizedIdentifierType === 'phone'
? `phone:${normalizedIdentifier.toLowerCase()}`
: normalizedIdentifier;
}
function normalizeSource(value = '') {
@@ -140,11 +193,15 @@
return null;
}
const email = String(record.email || '').trim();
const password = String(record.password || '').trim();
const identity = resolveRecordIdentity(record);
const email = identity.email;
const phoneNumber = identity.phoneNumber;
const accountIdentifierType = identity.accountIdentifierType;
const accountIdentifier = identity.accountIdentifier;
const password = String(record.password ?? '').trim();
const finalStatus = normalizeFinalStatus(record.finalStatus || record.status || '');
if (!email || !password || !finalStatus) {
if (!accountIdentifier || !finalStatus) {
return null;
}
@@ -167,8 +224,11 @@
const rawFailureLabel = String(record.failureLabel || '').trim();
return {
recordId: String(record.recordId || '').trim() || buildRecordId(email),
recordId: String(record.recordId || '').trim() || buildRecordId(accountIdentifier, accountIdentifierType),
accountIdentifierType,
accountIdentifier,
email,
phoneNumber,
password,
finalStatus,
finishedAt,
@@ -215,11 +275,20 @@
}
function buildAccountRunHistoryRecord(state = {}, status = '', reason = '') {
const email = String(state.email || '').trim();
const password = String(state.password || state.customPassword || '').trim() || '无';
const identity = resolveRecordIdentity({
accountIdentifierType: state.accountIdentifierType,
accountIdentifier: state.accountIdentifier,
email: state.email,
phoneNumber: state.phoneNumber || state.signupPhoneNumber,
});
const email = identity.email;
const phoneNumber = identity.phoneNumber;
const accountIdentifierType = identity.accountIdentifierType;
const accountIdentifier = identity.accountIdentifier;
const password = String(state.password || state.customPassword || '').trim();
const finalStatus = normalizeFinalStatus(status);
if (!email || !finalStatus) {
if (!accountIdentifier || !finalStatus) {
return null;
}
@@ -233,8 +302,11 @@
const finishedAt = new Date().toISOString();
return {
recordId: buildRecordId(email),
recordId: buildRecordId(accountIdentifier, accountIdentifierType),
accountIdentifierType,
accountIdentifier,
email,
phoneNumber,
password,
finalStatus,
finishedAt,
@@ -257,10 +329,23 @@
const recordId = String(record.recordId || '').trim();
const emailKey = String(record.email || '').trim().toLowerCase();
const phoneKey = String(record.phoneNumber || '').trim().toLowerCase();
const identifierKey = buildRecordId(
record.accountIdentifier || record.email || record.phoneNumber,
record.accountIdentifierType || (phoneKey && !emailKey ? 'phone' : 'email')
);
const nextHistory = normalizedHistory.filter((item) => {
const itemRecordId = String(item.recordId || '').trim();
const itemEmailKey = String(item.email || '').trim().toLowerCase();
return itemRecordId !== recordId && itemEmailKey !== emailKey;
const itemPhoneKey = String(item.phoneNumber || '').trim().toLowerCase();
const itemIdentifierKey = buildRecordId(
item.accountIdentifier || item.email || item.phoneNumber,
item.accountIdentifierType || (itemPhoneKey && !itemEmailKey ? 'phone' : 'email')
);
return itemRecordId !== recordId
&& itemIdentifierKey !== identifierKey
&& (!emailKey || itemEmailKey !== emailKey)
&& (!phoneKey || itemPhoneKey !== phoneKey);
});
nextHistory.unshift(record);
@@ -283,7 +368,12 @@
}
const selectedIds = new Set(normalizedIds);
const nextHistory = normalizedHistory.filter((record) => !selectedIds.has(buildRecordId(record.recordId || record.email)));
const nextHistory = normalizedHistory.filter((record) => !selectedIds.has(buildRecordId(
record.recordId || record.accountIdentifier || record.email || record.phoneNumber,
String(record.recordId || '').startsWith('phone:') || String(record.accountIdentifierType || '').trim().toLowerCase() === 'phone'
? 'phone'
: 'email'
)));
return {
deletedCount: normalizedHistory.length - nextHistory.length,
+1
View File
@@ -417,6 +417,7 @@
autoRunDelayEnabled: prevState.autoRunDelayEnabled,
autoRunDelayMinutes: prevState.autoRunDelayMinutes,
autoStepDelaySeconds: prevState.autoStepDelaySeconds,
signupMethod: prevState.signupMethod,
mailProvider: prevState.mailProvider,
emailGenerator: prevState.emailGenerator,
gmailBaseEmail: prevState.gmailBaseEmail,
+51 -4
View File
@@ -48,6 +48,14 @@
getStepDefinitionForState,
getStepIdsForState,
getLastStepIdForState,
normalizeSignupMethod = (value = '') => String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email',
canUsePhoneSignup = (state = {}) => Boolean(state?.phoneVerificationEnabled)
&& !Boolean(state?.plusModeEnabled)
&& !Boolean(state?.contributionMode),
resolveSignupMethod = (state = {}) => {
const method = normalizeSignupMethod(state?.signupMethod);
return method === 'phone' && canUsePhoneSignup(state) ? 'phone' : 'email';
},
getTabId,
getStopRequested,
handleAutoRunLoopUnhandledError,
@@ -275,7 +283,13 @@
if (stepKey === 'fetch-login-code') {
await setState({
lastEmailTimestamp: payload.emailTimestamp || null,
...(payload.phoneVerification || payload.loginPhoneVerification ? {
currentPhoneVerificationCode: '',
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
} : {
lastEmailTimestamp: payload.emailTimestamp || null,
}),
loginVerificationRequestedAt: null,
});
return;
@@ -340,7 +354,8 @@
const step3Status = latestState.stepStatuses?.[3];
if (step3Status !== 'running' && step3Status !== 'completed' && step3Status !== 'manual_completed') {
await setStepStatus(3, 'skipped');
await addLog('步骤 2:提交邮箱后页面直接进入邮箱验证码页,已自动跳过步骤 3。', 'warn');
const identityLabel = payload.accountIdentifierType === 'phone' ? '手机号' : '邮箱';
await addLog(`步骤 2:提交${identityLabel}后页面直接进入验证码页,已自动跳过步骤 3。`, 'warn');
}
}
break;
@@ -349,13 +364,27 @@
if (payload.signupVerificationRequestedAt) {
await setState({ signupVerificationRequestedAt: payload.signupVerificationRequestedAt });
}
if (payload.skipProfileStep) {
const latestState = await getState();
const step5Status = latestState.stepStatuses?.[5];
if (step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') {
await setStepStatus(5, 'skipped');
await addLog('步骤 3:页面已直接进入已登录态,已自动跳过步骤 5。', 'warn');
}
}
if (payload.loginVerificationRequestedAt) {
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
}
break;
case 4:
await setState({
lastEmailTimestamp: payload.emailTimestamp || null,
...(payload.phoneVerification ? {
currentPhoneVerificationCode: '',
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
} : {
lastEmailTimestamp: payload.emailTimestamp || null,
}),
signupVerificationRequestedAt: null,
});
if (payload.skipProfileStep) {
@@ -373,7 +402,13 @@
break;
case 8:
await setState({
lastEmailTimestamp: payload.emailTimestamp || null,
...(payload.phoneVerification || payload.loginPhoneVerification ? {
currentPhoneVerificationCode: '',
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
} : {
lastEmailTimestamp: payload.emailTimestamp || null,
}),
loginVerificationRequestedAt: null,
});
break;
@@ -758,6 +793,18 @@
const currentState = await getState();
const updates = buildPersistentSettingsPayload(message.payload || {});
const sessionUpdates = buildLuckmailSessionSettingsPayload(message.payload || {});
const nextSignupState = {
...currentState,
...updates,
resolvedSignupMethod: null,
};
if (
Object.prototype.hasOwnProperty.call(updates, 'phoneVerificationEnabled')
|| Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
|| Object.prototype.hasOwnProperty.call(updates, 'signupMethod')
) {
updates.signupMethod = resolveSignupMethod(nextSignupState);
}
const modeChanged = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
&& Boolean(currentState?.plusModeEnabled) !== Boolean(updates.plusModeEnabled);
const plusPaymentChanged = Object.prototype.hasOwnProperty.call(updates, 'plusPaymentMethod')
+619 -1
View File
@@ -92,6 +92,7 @@
const MAX_ACTIVATION_PRICE_HINTS = 256;
const activationPriceHintsByKey = new Map();
let activePhoneVerificationLogStep = null;
let activePhoneVerificationLogStepKey = null;
function normalizeLogStep(value) {
const step = Math.floor(Number(value) || 0);
@@ -114,13 +115,26 @@
if (step) {
normalizedOptions.step = step;
if (!normalizedOptions.stepKey) {
normalizedOptions.stepKey = 'phone-verification';
normalizedOptions.stepKey = activePhoneVerificationLogStepKey || 'phone-verification';
}
}
delete normalizedOptions.visibleStep;
return rawAddLog(normalizePhoneVerificationLogMessage(message), level, normalizedOptions);
}
async function withPhoneVerificationLogContext(options = {}, action) {
const previousStep = activePhoneVerificationLogStep;
const previousStepKey = activePhoneVerificationLogStepKey;
activePhoneVerificationLogStep = normalizeLogStep(options.step || options.visibleStep) || previousStep;
activePhoneVerificationLogStepKey = String(options.stepKey || '').trim() || previousStepKey;
try {
return await action();
} finally {
activePhoneVerificationLogStep = previousStep;
activePhoneVerificationLogStepKey = previousStepKey;
}
}
function normalizeUrl(value, fallback = DEFAULT_HERO_SMS_BASE_URL) {
const trimmed = String(value || '').trim();
if (!trimmed) {
@@ -3661,6 +3675,60 @@
return result || {};
}
async function submitSignupPhoneVerificationCode(tabId, code, options = {}) {
const visibleStep = 4;
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(45000, { step: visibleStep, actionLabel: '提交注册手机验证码' })
: 45000;
const result = await sendToContentScriptResilient('signup-page', {
type: 'SUBMIT_PHONE_VERIFICATION_CODE',
step: visibleStep,
source: 'background',
payload: {
code,
purpose: 'signup',
signupProfile: options.signupProfile || null,
},
}, {
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 600,
logMessage: '步骤 4:等待注册手机验证码页面就绪后填写短信验证码...',
logStep: visibleStep,
logStepKey: 'fetch-signup-code',
});
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
async function resendSignupPhoneVerificationCode(tabId) {
const visibleStep = 4;
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(65000, { step: visibleStep, actionLabel: '重新发送注册手机验证码' })
: 65000;
const result = await sendToContentScriptResilient('signup-page', {
type: 'RESEND_VERIFICATION_CODE',
step: visibleStep,
source: 'background',
payload: {},
}, {
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 600,
logMessage: '步骤 4:等待注册手机验证码重发按钮出现...',
logStep: visibleStep,
logStepKey: 'fetch-signup-code',
});
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
async function returnToAddPhone(tabId) {
const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9;
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
@@ -3773,6 +3841,25 @@
});
}
async function persistSignupPhoneRuntimeState(updates = {}) {
await setPhoneRuntimeState({
signupPhoneNumber: '',
signupPhoneActivation: null,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
accountIdentifierType: null,
accountIdentifier: '',
...updates,
});
}
async function clearSignupPhoneRuntimeState(extraUpdates = {}) {
await persistSignupPhoneRuntimeState({
[PHONE_VERIFICATION_CODE_STATE_KEY]: '',
...extraUpdates,
});
}
async function acquirePhoneActivation(state = {}, options = {}) {
const provider = normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER);
const providerOrder = resolvePhoneProviderOrder(state, provider);
@@ -3982,6 +4069,28 @@
throw lastProviderError || new Error('Step 9: failed to acquire phone activation.');
}
async function prepareSignupPhoneActivation(state = {}, options = {}) {
return withPhoneVerificationLogContext({ step: 2, stepKey: 'submit-signup-email' }, async () => {
const activation = await acquirePhoneActivation(state, {
...options,
logLabel: options?.logLabel || '步骤 2',
});
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
throw new Error('步骤 2:接码平台返回的手机号订单无效。');
}
await persistSignupPhoneRuntimeState({
signupPhoneNumber: normalizedActivation.phoneNumber,
signupPhoneActivation: normalizedActivation,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: 'signup',
accountIdentifierType: 'phone',
accountIdentifier: normalizedActivation.phoneNumber,
});
return normalizedActivation;
});
}
async function markActivationReusableAfterSuccess(state, activation) {
const normalizedActivation = normalizeActivation(activation);
if (!isPhoneSmsReuseEnabled(state)) {
@@ -4151,8 +4260,505 @@
throw new Error('手机号验证未完成。');
}
function buildCompletedActivationSnapshot(activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
return null;
}
return {
...normalizedActivation,
successfulUses: normalizedActivation.successfulUses + 1,
};
}
async function waitForScopedPhoneCode(state = {}, activation, options = {}) {
const normalizedActivation = normalizeActivation(activation);
const visibleStep = normalizeLogStep(options?.step) || 4;
const stepKey = String(options?.stepKey || 'fetch-signup-code').trim() || 'fetch-signup-code';
const purpose = String(options?.purpose || 'signup').trim() || 'signup';
const actionLabelPrefix = String(options?.actionLabelPrefix || 'signup phone verification').trim() || 'phone verification';
if (!normalizedActivation) {
throw new Error(options?.missingActivationMessage || `步骤 ${visibleStep}:手机号激活记录缺失,请重新执行前置步骤。`);
}
return withPhoneVerificationLogContext({ step: visibleStep, stepKey }, async () => {
const providerLabel = getPhoneSmsProviderLabel(normalizedActivation.provider);
const waitSeconds = normalizePhoneCodeWaitSeconds(state?.phoneCodeWaitSeconds);
const timeoutWindows = normalizePhoneCodeTimeoutWindows(state?.phoneCodeTimeoutWindows);
const pollIntervalSeconds = normalizePhoneCodePollIntervalSeconds(state?.phoneCodePollIntervalSeconds);
const pollMaxRounds = normalizePhoneCodePollMaxRounds(state?.phoneCodePollMaxRounds);
let lastLoggedStatus = '';
let lastLoggedPollCount = 0;
for (let windowIndex = 1; windowIndex <= timeoutWindows; windowIndex += 1) {
await setPhoneRuntimeState({
signupPhoneActivation: normalizedActivation,
signupPhoneNumber: normalizedActivation.phoneNumber,
signupPhoneVerificationPurpose: purpose,
signupPhoneVerificationRequestedAt: Date.now(),
[PHONE_RUNTIME_COUNTDOWN_ENDS_AT_KEY]: Date.now() + waitSeconds * 1000,
[PHONE_RUNTIME_COUNTDOWN_WINDOW_INDEX_KEY]: windowIndex,
[PHONE_RUNTIME_COUNTDOWN_WINDOW_TOTAL_KEY]: timeoutWindows,
});
await addLog(
`步骤 ${visibleStep}:正在等待 ${normalizedActivation.phoneNumber} 的短信验证码(${windowIndex}/${timeoutWindows},最长 ${waitSeconds} 秒)。`,
'info',
{ step: visibleStep, stepKey }
);
try {
const code = await pollPhoneActivationCode(state, normalizedActivation, {
actionLabel: windowIndex === 1
? `poll ${actionLabelPrefix} code from ${providerLabel}`
: `poll resent ${actionLabelPrefix} code from ${providerLabel}`,
timeoutMs: waitSeconds * 1000,
intervalMs: pollIntervalSeconds * 1000,
maxRounds: pollMaxRounds,
onStatus: async ({ elapsedMs, pollCount, statusText }) => {
const shouldLog = (
pollCount === 1
|| statusText !== lastLoggedStatus
|| pollCount - lastLoggedPollCount >= 3
);
if (!shouldLog) {
return;
}
lastLoggedStatus = statusText;
lastLoggedPollCount = pollCount;
await addLog(
`步骤 ${visibleStep}${providerLabel} 状态 ${normalizedActivation.phoneNumber}: ${statusText}(已等待 ${Math.ceil(elapsedMs / 1000)} 秒,第 ${pollCount}/${pollMaxRounds} 轮)。`,
'info',
{ step: visibleStep, stepKey }
);
},
});
await clearPhoneRuntimeCountdown();
await setPhoneRuntimeState({
[PHONE_VERIFICATION_CODE_STATE_KEY]: String(code || '').trim(),
signupPhoneVerificationRequestedAt: Date.now(),
});
return code;
} catch (error) {
if (!isPhoneCodeTimeoutError(error)) {
if (isPhoneActivationOrderMissingError(error, normalizedActivation.provider)) {
throw new Error(`步骤 ${visibleStep}:当前手机号激活已失效,请重新执行前置步骤获取新短信。${error.message || error}`);
}
throw error;
}
if (windowIndex < timeoutWindows) {
await addLog(
`步骤 ${visibleStep}${normalizedActivation.phoneNumber}${waitSeconds} 秒内未收到短信,准备请求重发。`,
'warn',
{ step: visibleStep, stepKey }
);
await requestAdditionalPhoneSms(state, normalizedActivation);
if (typeof options.onTimeoutWindow === 'function') {
await options.onTimeoutWindow({
activation: normalizedActivation,
windowIndex,
timeoutWindows,
});
}
continue;
}
await clearPhoneRuntimeCountdown();
throw error;
}
}
throw new Error(`步骤 ${visibleStep}:手机验证码未能成功获取。`);
});
}
async function waitForSignupPhoneCode(state = {}, activation, options = {}) {
return waitForScopedPhoneCode(state, activation, {
...options,
step: 4,
stepKey: 'fetch-signup-code',
purpose: 'signup',
actionLabelPrefix: 'signup phone verification',
missingActivationMessage: '步骤 4:注册手机号激活记录缺失,请重新执行步骤 2。',
});
}
async function waitForLoginPhoneCode(state = {}, activation, options = {}) {
const visibleStep = normalizeLogStep(options?.visibleStep || options?.step) || 8;
return waitForScopedPhoneCode(state, activation, {
...options,
step: visibleStep,
stepKey: 'fetch-login-code',
purpose: 'login',
actionLabelPrefix: 'login phone verification',
missingActivationMessage: `步骤 ${visibleStep}:登录手机号激活记录缺失,请重新执行步骤 ${visibleStep >= 11 ? 10 : 7}`,
});
}
async function finalizeSignupPhoneActivationAfterSuccess(state = {}, activation = null) {
const normalizedActivation = normalizeActivation(activation || state?.signupPhoneActivation);
if (!normalizedActivation) {
await clearSignupPhoneRuntimeState();
return null;
}
await completePhoneActivation(state, normalizedActivation);
await markActivationReusableAfterSuccess(state, normalizedActivation);
await clearSignupPhoneRuntimeState({
signupPhoneCompletedActivation: buildCompletedActivationSnapshot(normalizedActivation),
signupPhoneNumber: normalizedActivation.phoneNumber,
accountIdentifierType: 'phone',
accountIdentifier: normalizedActivation.phoneNumber,
});
return normalizedActivation;
}
async function cancelSignupPhoneActivation(state = {}, activation = null) {
const normalizedActivation = normalizeActivation(activation || state?.signupPhoneActivation);
if (normalizedActivation) {
await cancelPhoneActivation(state, normalizedActivation);
}
await clearSignupPhoneRuntimeState();
}
async function completeSignupPhoneVerificationFlow(tabId, options = {}) {
return withPhoneVerificationLogContext({ step: 4, stepKey: 'fetch-signup-code' }, async () => {
let state = options?.state || await getState();
const activation = normalizeActivation(options?.activation || state?.signupPhoneActivation);
if (!activation) {
throw new Error('步骤 4:未找到当前注册手机号激活记录,请重新执行步骤 2。');
}
let shouldCancelActivation = true;
try {
for (let attempt = 1; attempt <= DEFAULT_PHONE_SUBMIT_ATTEMPTS; attempt += 1) {
throwIfStopped();
state = await getState();
const code = await waitForSignupPhoneCode(state, activation, {
onTimeoutWindow: async () => {
try {
await resendSignupPhoneVerificationCode(tabId);
await addLog('步骤 4:已点击注册手机验证码页面的“重新发送”。', 'info', {
step: 4,
stepKey: 'fetch-signup-code',
});
} catch (resendError) {
if (isStopRequestedError(resendError)) {
throw resendError;
}
await addLog(`步骤 4:注册手机验证码页面重发失败,将继续轮询短信。${resendError.message}`, 'warn', {
step: 4,
stepKey: 'fetch-signup-code',
});
}
},
});
await setPhoneRuntimeState({
[PHONE_VERIFICATION_CODE_STATE_KEY]: String(code || '').trim(),
signupPhoneVerificationRequestedAt: Date.now(),
signupPhoneVerificationPurpose: 'signup',
});
await addLog(`步骤 4:已获取手机验证码 ${code}`, 'info', {
step: 4,
stepKey: 'fetch-signup-code',
});
const submitResult = await submitSignupPhoneVerificationCode(tabId, code, {
signupProfile: options.signupProfile || null,
});
if (submitResult.invalidCode) {
const invalidErrorText = String(submitResult.errorText || submitResult.url || '未知错误').trim();
if (attempt >= DEFAULT_PHONE_SUBMIT_ATTEMPTS) {
throw new Error(`步骤 4:手机验证码连续 ${DEFAULT_PHONE_SUBMIT_ATTEMPTS} 次被拒绝:${invalidErrorText}`);
}
await requestAdditionalPhoneSms(state, activation);
try {
await resendSignupPhoneVerificationCode(tabId);
} catch (resendError) {
if (isStopRequestedError(resendError)) {
throw resendError;
}
await addLog(`步骤 4:验证码被拒后点击重发失败。${resendError.message}`, 'warn', {
step: 4,
stepKey: 'fetch-signup-code',
});
}
await addLog(
`步骤 4:手机验证码被拒绝,已请求新短信(${attempt + 1}/${DEFAULT_PHONE_SUBMIT_ATTEMPTS})。`,
'warn',
{ step: 4, stepKey: 'fetch-signup-code' }
);
continue;
}
await finalizeSignupPhoneActivationAfterSuccess(state, activation);
shouldCancelActivation = false;
await setPhoneRuntimeState({
[PHONE_VERIFICATION_CODE_STATE_KEY]: '',
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
});
await addLog('步骤 4:手机验证码已通过,继续进入资料填写。', 'ok', {
step: 4,
stepKey: 'fetch-signup-code',
});
return submitResult || {};
}
throw new Error('步骤 4:手机验证码未能成功提交。');
} catch (error) {
if (shouldCancelActivation && activation) {
await cancelSignupPhoneActivation(state, activation).catch(() => {});
}
await setPhoneRuntimeState({
[PHONE_VERIFICATION_CODE_STATE_KEY]: '',
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
});
throw sanitizePhoneCodeTimeoutError(error);
}
});
}
async function submitLoginPhoneVerificationCode(tabId, code, options = {}) {
const visibleStep = normalizeLogStep(options?.visibleStep || options?.step) || 8;
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(45000, { step: visibleStep, actionLabel: '提交登录手机验证码' })
: 45000;
const result = await sendToContentScriptResilient('signup-page', {
type: 'SUBMIT_PHONE_VERIFICATION_CODE',
step: visibleStep,
source: 'background',
payload: {
code,
purpose: 'login',
visibleStep,
},
}, {
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 600,
logMessage: `步骤 ${visibleStep}:等待登录手机验证码页面就绪后填写短信验证码...`,
logStep: visibleStep,
logStepKey: 'fetch-login-code',
});
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
async function resendLoginPhoneVerificationCode(tabId, options = {}) {
const visibleStep = normalizeLogStep(options?.visibleStep || options?.step) || 8;
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(65000, { step: visibleStep, actionLabel: '重新发送登录手机验证码' })
: 65000;
const result = await sendToContentScriptResilient('signup-page', {
type: 'RESEND_VERIFICATION_CODE',
step: visibleStep,
source: 'background',
payload: {},
}, {
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 600,
logMessage: `步骤 ${visibleStep}:等待登录手机验证码重发按钮出现...`,
logStep: visibleStep,
logStepKey: 'fetch-login-code',
});
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
async function prepareLoginPhoneActivation(state = {}, options = {}) {
const visibleStep = normalizeLogStep(options?.visibleStep || options?.step) || 8;
return withPhoneVerificationLogContext({ step: visibleStep, stepKey: 'fetch-login-code' }, async () => {
const preferredActivation = normalizeActivation(
options?.activation
|| state?.signupPhoneCompletedActivation
|| state?.signupPhoneActivation
);
if (!preferredActivation) {
throw new Error(`步骤 ${visibleStep}:缺少已注册手机号激活记录,无法继续手机号登录验证码流程。`);
}
const activeActivation = normalizeActivation(state?.signupPhoneActivation);
if (activeActivation && isSameActivation(activeActivation, preferredActivation)) {
await setPhoneRuntimeState({
signupPhoneNumber: activeActivation.phoneNumber,
signupPhoneVerificationPurpose: 'login',
});
return activeActivation;
}
const reactivated = await reactivatePhoneActivation(state, preferredActivation);
const normalizedActivation = normalizeActivation(reactivated);
if (!normalizedActivation) {
throw new Error(`步骤 ${visibleStep}:无法复用当前注册手机号,请重新执行步骤 ${visibleStep >= 11 ? 10 : 7}`);
}
await setPhoneRuntimeState({
signupPhoneActivation: normalizedActivation,
signupPhoneCompletedActivation: preferredActivation,
signupPhoneNumber: normalizedActivation.phoneNumber,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: 'login',
[PHONE_VERIFICATION_CODE_STATE_KEY]: '',
accountIdentifierType: 'phone',
accountIdentifier: normalizedActivation.phoneNumber,
});
return normalizedActivation;
});
}
async function finalizeLoginPhoneActivationAfterSuccess(state = {}, activation = null, options = {}) {
const normalizedActivation = normalizeActivation(activation || state?.signupPhoneActivation);
const visibleStep = normalizeLogStep(options?.visibleStep || options?.step) || 8;
if (!normalizedActivation) {
await setPhoneRuntimeState({
signupPhoneActivation: null,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
[PHONE_VERIFICATION_CODE_STATE_KEY]: '',
});
return null;
}
return withPhoneVerificationLogContext({ step: visibleStep, stepKey: 'fetch-login-code' }, async () => {
await completePhoneActivation(state, normalizedActivation);
await setPhoneRuntimeState({
signupPhoneActivation: null,
signupPhoneCompletedActivation: buildCompletedActivationSnapshot(normalizedActivation),
signupPhoneNumber: normalizedActivation.phoneNumber,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
[PHONE_VERIFICATION_CODE_STATE_KEY]: '',
accountIdentifierType: 'phone',
accountIdentifier: normalizedActivation.phoneNumber,
});
return normalizedActivation;
});
}
async function completeLoginPhoneVerificationFlow(tabId, options = {}) {
const visibleStep = normalizeLogStep(options?.visibleStep || options?.step) || 8;
return withPhoneVerificationLogContext({ step: visibleStep, stepKey: 'fetch-login-code' }, async () => {
let state = options?.state || await getState();
const baseActivation = normalizeActivation(
options?.activation
|| state?.signupPhoneCompletedActivation
|| state?.signupPhoneActivation
);
if (!baseActivation) {
throw new Error(`步骤 ${visibleStep}:未找到当前登录手机号激活记录,请重新执行步骤 ${visibleStep >= 11 ? 10 : 7}`);
}
let activation = await prepareLoginPhoneActivation(state, {
activation: baseActivation,
visibleStep,
});
let shouldCancelActivation = true;
try {
for (let attempt = 1; attempt <= DEFAULT_PHONE_SUBMIT_ATTEMPTS; attempt += 1) {
throwIfStopped();
state = await getState();
const code = await waitForLoginPhoneCode(state, activation, {
visibleStep,
onTimeoutWindow: async () => {
try {
await resendLoginPhoneVerificationCode(tabId, { visibleStep });
await addLog(`步骤 ${visibleStep}:已点击登录手机验证码页面的“重新发送”。`, 'info', {
step: visibleStep,
stepKey: 'fetch-login-code',
});
} catch (resendError) {
if (isStopRequestedError(resendError)) {
throw resendError;
}
await addLog(`步骤 ${visibleStep}:登录手机验证码页面重发失败,将继续轮询短信。${resendError.message}`, 'warn', {
step: visibleStep,
stepKey: 'fetch-login-code',
});
}
},
});
await setPhoneRuntimeState({
[PHONE_VERIFICATION_CODE_STATE_KEY]: String(code || '').trim(),
signupPhoneVerificationRequestedAt: Date.now(),
signupPhoneVerificationPurpose: 'login',
});
await addLog(`步骤 ${visibleStep}:已获取登录手机验证码 ${code}`, 'info', {
step: visibleStep,
stepKey: 'fetch-login-code',
});
const submitResult = await submitLoginPhoneVerificationCode(tabId, code, {
visibleStep,
});
if (submitResult.invalidCode) {
const invalidErrorText = String(submitResult.errorText || submitResult.url || '未知错误').trim();
if (attempt >= DEFAULT_PHONE_SUBMIT_ATTEMPTS) {
throw new Error(`步骤 ${visibleStep}:登录手机验证码连续 ${DEFAULT_PHONE_SUBMIT_ATTEMPTS} 次被拒绝:${invalidErrorText}`);
}
await requestAdditionalPhoneSms(state, activation);
try {
await resendLoginPhoneVerificationCode(tabId, { visibleStep });
} catch (resendError) {
if (isStopRequestedError(resendError)) {
throw resendError;
}
await addLog(`步骤 ${visibleStep}:登录手机验证码被拒后点击重发失败。${resendError.message}`, 'warn', {
step: visibleStep,
stepKey: 'fetch-login-code',
});
}
await addLog(
`步骤 ${visibleStep}:登录手机验证码被拒绝,已请求新短信(${attempt + 1}/${DEFAULT_PHONE_SUBMIT_ATTEMPTS})。`,
'warn',
{ step: visibleStep, stepKey: 'fetch-login-code' }
);
continue;
}
await finalizeLoginPhoneActivationAfterSuccess(state, activation, { visibleStep });
shouldCancelActivation = false;
await addLog(`步骤 ${visibleStep}:登录手机验证码已通过,继续进入后续授权流程。`, 'ok', {
step: visibleStep,
stepKey: 'fetch-login-code',
});
return submitResult || {};
}
throw new Error(`步骤 ${visibleStep}:登录手机验证码未能成功提交。`);
} catch (error) {
if (shouldCancelActivation && activation) {
await cancelPhoneActivation(state, activation).catch(() => {});
}
await setPhoneRuntimeState({
signupPhoneActivation: null,
[PHONE_VERIFICATION_CODE_STATE_KEY]: '',
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
});
throw sanitizePhoneCodeTimeoutError(error);
}
});
}
async function completePhoneVerificationFlow(tabId, initialPageState = null, options = {}) {
const previousLogStep = activePhoneVerificationLogStep;
const previousLogStepKey = activePhoneVerificationLogStepKey;
activePhoneVerificationLogStep = normalizeLogStep(options.visibleStep || options.step) || 9;
activePhoneVerificationLogStepKey = 'phone-verification';
let state = await getState();
let activation = normalizeActivation(state[PHONE_ACTIVATION_STATE_KEY]);
let pageState = initialPageState || await readPhonePageState(tabId);
@@ -4796,15 +5402,27 @@
}
await clearCurrentActivation();
throw sanitizePhoneRestartStep7Error(sanitizePhoneCodeTimeoutError(error));
} finally {
activePhoneVerificationLogStep = previousLogStep;
activePhoneVerificationLogStepKey = previousLogStepKey;
}
}
return {
cancelSignupPhoneActivation,
completeLoginPhoneVerificationFlow,
completePhoneVerificationFlow,
completeSignupPhoneVerificationFlow,
finalizeLoginPhoneActivationAfterSuccess,
finalizeSignupPhoneActivationAfterSuccess,
normalizeActivation,
pollPhoneActivationCode,
prepareLoginPhoneActivation,
prepareSignupPhoneActivation,
reactivatePhoneActivation,
requestPhoneActivation,
waitForLoginPhoneCode,
waitForSignupPhoneCode,
};
}
+51 -11
View File
@@ -17,6 +17,8 @@
isLuckmailProvider,
isSignupEmailVerificationPageUrl,
isSignupPasswordPageUrl,
isSignupPhoneVerificationPageUrl = null,
isSignupProfilePageUrl = null,
reuseOrCreateTab,
sendToContentScriptResilient,
setEmailState,
@@ -62,46 +64,79 @@
return { tabId, result: result || {} };
}
function resolveSignupPostEmailState(rawUrl) {
function parseUrlSafely(rawUrl) {
if (!rawUrl) return null;
try {
return new URL(rawUrl);
} catch {
return null;
}
}
function fallbackSignupPhoneVerificationPageUrl(rawUrl) {
const parsed = parseUrlSafely(rawUrl);
if (!parsed) return false;
return /\/phone-verification(?:[/?#]|$)/i.test(parsed.pathname || '');
}
function fallbackSignupProfilePageUrl(rawUrl) {
const parsed = parseUrlSafely(rawUrl);
if (!parsed) return false;
return /\/create-account\/profile(?:[/?#]|$)/i.test(parsed.pathname || '');
}
function resolveSignupPostIdentityState(rawUrl) {
if (isSignupPasswordPageUrl(rawUrl)) {
return 'password_page';
}
if (isSignupEmailVerificationPageUrl(rawUrl)) {
return 'verification_page';
}
const isPhoneVerificationUrl = typeof isSignupPhoneVerificationPageUrl === 'function'
? isSignupPhoneVerificationPageUrl(rawUrl)
: fallbackSignupPhoneVerificationPageUrl(rawUrl);
if (isPhoneVerificationUrl) {
return 'phone_verification_page';
}
const isProfileUrl = typeof isSignupProfilePageUrl === 'function'
? isSignupProfilePageUrl(rawUrl)
: fallbackSignupProfilePageUrl(rawUrl);
if (isProfileUrl) {
return 'profile_page';
}
return '';
}
async function ensureSignupPostEmailPageReadyInTab(tabId, step = 2, options = {}) {
async function ensureSignupPostIdentityPageReadyInTab(tabId, step = 2, options = {}) {
const { skipUrlWait = false } = options;
let landingUrl = '';
let landingState = '';
if (!skipUrlWait) {
const matchedTab = await waitForTabUrlMatch(tabId, (url) => Boolean(resolveSignupPostEmailState(url)), {
const matchedTab = await waitForTabUrlMatch(tabId, (url) => Boolean(resolveSignupPostIdentityState(url)), {
timeoutMs: 45000,
retryDelayMs: 300,
});
if (!matchedTab) {
throw new Error('等待邮箱提交后的页面跳转超时,请检查页面是否仍停留在邮箱输入页。');
throw new Error('等待注册身份提交后的页面跳转超时,请检查页面是否仍停留在输入页。');
}
landingUrl = matchedTab.url || '';
landingState = resolveSignupPostEmailState(landingUrl);
landingState = resolveSignupPostIdentityState(landingUrl);
}
if (!landingState) {
try {
const currentTab = await chrome.tabs.get(tabId);
landingUrl = landingUrl || currentTab?.url || '';
landingState = resolveSignupPostEmailState(landingUrl);
landingState = resolveSignupPostIdentityState(landingUrl);
} catch {
landingUrl = landingUrl || '';
}
}
if (!landingState) {
throw new Error(`邮箱提交后未能识别当前页面,既不是密码页也不是邮箱验证码页。URL: ${landingUrl || 'unknown'}`);
throw new Error(`注册身份提交后未能识别当前页面,既不是密码页、验证码页,也不是资料页。URL: ${landingUrl || 'unknown'}`);
}
await ensureContentScriptReadyOnTab('signup-page', tabId, {
@@ -109,12 +144,12 @@
injectSource: 'signup-page',
timeoutMs: 45000,
retryDelayMs: 900,
logMessage: landingState === 'verification_page'
? `步骤 ${step}邮箱验证码页仍在加载,正在等待页面恢复...`
: `步骤 ${step}密码页仍在加载,正在重试连接内容脚本...`,
logMessage: landingState === 'password_page'
? `步骤 ${step}码页仍在加载,正在重试连接内容脚本...`
: `步骤 ${step}注册后续页面仍在加载,正在等待页面恢复...`,
});
if (landingState === 'verification_page') {
if (landingState !== 'password_page') {
return {
ready: true,
state: landingState,
@@ -145,6 +180,10 @@
};
}
async function ensureSignupPostEmailPageReadyInTab(tabId, step = 2, options = {}) {
return ensureSignupPostIdentityPageReadyInTab(tabId, step, options);
}
async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) {
const result = await ensureSignupPostEmailPageReadyInTab(tabId, step, options);
if (result.state !== 'password_page') {
@@ -240,6 +279,7 @@
return {
ensureSignupEntryPageReady,
ensureSignupPostIdentityPageReadyInTab,
ensureSignupPostEmailPageReadyInTab,
finalizeSignupPasswordSubmitInTab,
ensureSignupPasswordPageReadyInTab,
+1
View File
@@ -52,6 +52,7 @@
async function executePlusCheckoutCreate(state = {}) {
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
const paymentMethodLabel = getPlusPaymentMethodLabel(paymentMethod);
const checkoutModeLabel = getCheckoutModeLabel(state);
await addLog('步骤 6:正在新打开 ChatGPT 会话页,准备创建 Plus Checkout...', 'info');
const tabId = await openFreshChatGptTabForCheckoutCreate();
+45 -10
View File
@@ -25,6 +25,8 @@
resolveVerificationStep,
rerunStep7ForStep8Recovery,
reuseOrCreateTab,
phoneVerificationHelpers = null,
resolveSignupMethod = () => 'email',
setState,
shouldUseCustomRegistrationEmail,
sleepWithStop,
@@ -97,6 +99,13 @@
return String(value || '').trim().toLowerCase();
}
function isPhoneLoginState(state = {}) {
return String(state?.accountIdentifierType || '').trim().toLowerCase() === 'phone'
|| resolveSignupMethod(state) === 'phone'
|| Boolean(state?.signupPhoneCompletedActivation)
|| Boolean(state?.signupPhoneActivation);
}
async function completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, options = {}) {
await setState({
step8VerificationTargetEmail: '',
@@ -206,9 +215,45 @@
return Math.max(0, Number(STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS) || 0);
}
async function executeLoginPhoneCodeStep(state, signupTabId, visibleStep) {
if (!Number.isInteger(signupTabId)) {
throw new Error(`步骤 ${visibleStep}:认证页面标签页已关闭,无法继续手机号登录验证码流程。`);
}
if (typeof phoneVerificationHelpers?.completeLoginPhoneVerificationFlow !== 'function') {
throw new Error(`步骤 ${visibleStep}:手机号登录验证码流程不可用,接码模块尚未初始化。`);
}
const result = await phoneVerificationHelpers.completeLoginPhoneVerificationFlow(signupTabId, {
state,
visibleStep,
});
await completeStepFromBackground(visibleStep, {
phoneVerification: true,
loginPhoneVerification: true,
code: result?.code || '',
});
return result || {};
}
async function runStep8Attempt(state, runtime = {}) {
const visibleStep = getVisibleStep(state, 8);
activeFetchLoginCodeStep = visibleStep;
const authTabId = await getTabId('signup-page');
if (authTabId) {
await chrome.tabs.update(authTabId, { active: true });
} else {
if (!state.oauthUrl) {
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}`);
}
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;
@@ -222,16 +267,6 @@
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: stepStartedAt;
const verificationSessionKey = `8:${stepStartedAt}`;
const authTabId = await getTabId('signup-page');
if (authTabId) {
await chrome.tabs.update(authTabId, { active: true });
} else {
if (!state.oauthUrl) {
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}`);
}
await reuseOrCreateTab('signup-page', state.oauthUrl);
}
throwIfStopped();
const pageState = await ensureStep8VerificationPageReady({
+39 -6
View File
@@ -27,6 +27,8 @@
shouldUseCustomRegistrationEmail,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
throwIfStopped,
phoneVerificationHelpers = null,
resolveSignupMethod = () => 'email',
} = deps;
function buildSignupProfileForVerificationStep() {
@@ -58,6 +60,32 @@
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
}
function isPhoneSignupState(state = {}) {
return resolveSignupMethod(state) === 'phone'
|| state?.accountIdentifierType === 'phone'
|| Boolean(state?.signupPhoneActivation);
}
async function executeSignupPhoneCodeStep(state, signupTabId) {
if (typeof phoneVerificationHelpers?.completeSignupPhoneVerificationFlow !== 'function') {
throw new Error('步骤 4:手机号注册验证码流程不可用,接码模块尚未初始化。');
}
const signupProfile = buildSignupProfileForVerificationStep();
const result = await phoneVerificationHelpers.completeSignupPhoneVerificationFlow(signupTabId, {
state,
signupProfile,
});
await completeStepFromBackground(4, {
phoneVerification: true,
code: result?.code || '',
...(result?.skipProfileStep ? { skipProfileStep: true } : {}),
...(result?.skipProfileStepReason ? { skipProfileStepReason: result.skipProfileStepReason } : {}),
});
return result || {};
}
async function focusOrOpenMailTab(mail) {
const alive = await isTabAlive(mail.source);
if (alive) {
@@ -81,13 +109,7 @@
}
async function executeStep4(state) {
const mail = getMailConfig(state);
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 = `4:${stepStartedAt}`;
const signupTabId = await getTabId('signup-page');
@@ -175,11 +197,22 @@
return;
}
if (isPhoneSignupState(state)) {
return executeSignupPhoneCodeStep(state, signupTabId);
}
if (shouldUseCustomRegistrationEmail(state)) {
await confirmCustomVerificationStepBypass(4);
return;
}
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const verificationFilterAfterTimestamp = mail.provider === '2925'
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: stepStartedAt;
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
await addLog('步骤 4:正在确认 iCloud 邮箱登录态...', 'info');
await ensureIcloudMailSession({
+43 -6
View File
@@ -15,10 +15,32 @@
SIGNUP_PAGE_INJECT_FILES,
} = deps;
function resolveStep3AccountIdentity(state = {}) {
const resolvedEmail = String(state?.email || '').trim();
const signupPhoneNumber = String(
state?.signupPhoneNumber
|| (state?.accountIdentifierType === 'phone' ? state?.accountIdentifier : '')
|| ''
).trim();
const accountIdentifierType = signupPhoneNumber && state?.accountIdentifierType === 'phone'
? 'phone'
: (resolvedEmail ? 'email' : (signupPhoneNumber ? 'phone' : 'email'));
const accountIdentifier = accountIdentifierType === 'phone'
? signupPhoneNumber
: resolvedEmail;
return {
accountIdentifierType,
accountIdentifier,
email: resolvedEmail,
phoneNumber: signupPhoneNumber,
};
}
async function executeStep3(state) {
const resolvedEmail = state.email;
if (!resolvedEmail) {
throw new Error('缺少邮箱地址,请先完成步骤 2。');
const identity = resolveStep3AccountIdentity(state);
if (!identity.accountIdentifier) {
throw new Error('缺少注册账号,请先完成步骤 2。');
}
const signupTabId = await getTabId('signup-page');
@@ -30,7 +52,13 @@
await setPasswordState(password);
const accounts = state.accounts || [];
accounts.push({ email: resolvedEmail, createdAt: new Date().toISOString() });
accounts.push({
email: identity.email,
phoneNumber: identity.phoneNumber,
accountIdentifierType: identity.accountIdentifierType,
accountIdentifier: identity.accountIdentifier,
createdAt: new Date().toISOString(),
});
await setState({ accounts });
await chrome.tabs.update(signupTabId, { active: true });
@@ -42,14 +70,23 @@
logMessage: '步骤 3:密码页内容脚本未就绪,正在等待页面恢复...',
});
const identityLabel = identity.accountIdentifierType === 'phone'
? `注册手机号为 ${identity.accountIdentifier}`
: `邮箱为 ${identity.accountIdentifier}`;
await addLog(
`步骤 3:正在填写密码,邮箱为 ${resolvedEmail},密码为${state.customPassword ? '自定义' : '自动生成'}${password.length} 位)`
`步骤 3:正在填写密码,${identityLabel},密码为${state.customPassword ? '自定义' : '自动生成'}${password.length} 位)`
);
await sendToContentScript('signup-page', {
type: 'EXECUTE_STEP',
step: 3,
source: 'background',
payload: { email: resolvedEmail, password },
payload: {
email: identity.email,
phoneNumber: identity.phoneNumber,
accountIdentifierType: identity.accountIdentifierType,
accountIdentifier: identity.accountIdentifier,
password,
},
});
}
+56 -5
View File
@@ -41,12 +41,30 @@
}
async function executeStep7(state) {
if (!state.email) {
throw new Error('缺少邮箱地址,请先完成步骤 3。');
}
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
const completionStep = visibleStep > 0 ? visibleStep : 7;
const resolvedIdentifierType = String(
state?.accountIdentifierType
|| (state?.signupPhoneNumber ? 'phone' : '')
|| ''
).trim().toLowerCase() === 'phone'
? 'phone'
: 'email';
const phoneNumber = String(
state?.signupPhoneNumber
|| (resolvedIdentifierType === 'phone' ? state?.accountIdentifier : '')
|| state?.signupPhoneCompletedActivation?.phoneNumber
|| state?.signupPhoneActivation?.phoneNumber
|| ''
).trim();
const email = String(
state?.email
|| (resolvedIdentifierType === 'email' ? state?.accountIdentifier : '')
|| ''
).trim();
if (!email && !phoneNumber) {
throw new Error('缺少登录账号,请先完成步骤 2 和步骤 3。');
}
let attempt = 0;
let lastError = null;
@@ -57,6 +75,28 @@
try {
const currentState = attempt === 1 ? state : await getState();
const password = currentState.password || currentState.customPassword || '';
const currentIdentifierType = String(
currentState?.accountIdentifierType
|| (currentState?.signupPhoneNumber ? 'phone' : '')
|| resolvedIdentifierType
).trim().toLowerCase() === 'phone'
? 'phone'
: 'email';
const currentPhoneNumber = String(
currentState?.signupPhoneNumber
|| (currentIdentifierType === 'phone' ? currentState?.accountIdentifier : '')
|| currentState?.signupPhoneCompletedActivation?.phoneNumber
|| currentState?.signupPhoneActivation?.phoneNumber
|| phoneNumber
).trim();
const currentEmail = String(
currentState?.email
|| (currentIdentifierType === 'email' ? currentState?.accountIdentifier : '')
|| email
).trim();
const accountIdentifier = currentIdentifierType === 'phone'
? currentPhoneNumber
: currentEmail;
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
if (typeof startOAuthFlowTimeoutWindow === 'function') {
await startOAuthFlowTimeoutWindow({ step: completionStep, oauthUrl });
@@ -90,7 +130,18 @@
step: 7,
source: 'background',
payload: {
email: currentState.email,
email: currentEmail,
phoneNumber: currentPhoneNumber,
countryId: currentState?.signupPhoneCompletedActivation?.countryId
?? currentState?.signupPhoneActivation?.countryId
?? null,
countryLabel: String(
currentState?.signupPhoneCompletedActivation?.countryLabel
|| currentState?.signupPhoneActivation?.countryLabel
|| ''
).trim(),
accountIdentifier,
loginIdentifierType: currentIdentifierType,
password,
visibleStep: completionStep,
},
+108 -4
View File
@@ -10,8 +10,11 @@
ensureSignupAuthEntryPageReady,
ensureSignupEntryPageReady,
ensureSignupPostEmailPageReadyInTab,
ensureSignupPostIdentityPageReadyInTab = ensureSignupPostEmailPageReadyInTab,
getTabId,
isTabAlive,
phoneVerificationHelpers = null,
resolveSignupMethod = () => 'email',
resolveSignupEmailForFlow,
sendToContentScriptResilient,
SIGNUP_PAGE_INJECT_FILES,
@@ -94,7 +97,7 @@
throw new Error(message);
}
async function submitSignupEmail(resolvedEmail, options = {}) {
async function sendSignupIdentity(payload = {}, options = {}) {
const {
timeoutMs = 35000,
retryDelayMs = 700,
@@ -106,7 +109,7 @@
type: 'EXECUTE_STEP',
step: 2,
source: 'background',
payload: { email: resolvedEmail },
payload,
}, {
timeoutMs,
retryDelayMs,
@@ -117,9 +120,23 @@
}
}
async function executeStep2(state) {
const resolvedEmail = await resolveSignupEmailForFlow(state);
async function submitSignupEmail(resolvedEmail, options = {}) {
return sendSignupIdentity({ email: resolvedEmail }, options);
}
async function submitSignupPhone(phoneNumber, activation, options = {}) {
return sendSignupIdentity({
signupMethod: 'phone',
phoneNumber,
countryId: activation?.countryId ?? null,
countryLabel: String(activation?.countryLabel || '').trim(),
}, {
logMessage: '步骤 2:官网注册入口正在切换,等待手机号注册入口恢复...',
...options,
});
}
async function ensureSignupTabForStep2() {
let signupTabId = await getTabId('signup-page');
if (!signupTabId || !(await isTabAlive('signup-page'))) {
await addLog('步骤 2:未发现可用的注册页标签,正在重新打开 ChatGPT 官网...', 'warn');
@@ -134,6 +151,84 @@
logMessage: '步骤 2:注册入口页内容脚本未就绪,正在等待页面恢复...',
});
}
return signupTabId;
}
async function executeSignupPhoneEntry(state) {
if (typeof phoneVerificationHelpers?.prepareSignupPhoneActivation !== 'function') {
throw new Error('手机号注册流程不可用:接码模块尚未初始化。');
}
let signupTabId = await ensureSignupTabForStep2();
if (await shouldForceAuthEntryRetry(signupTabId)) {
await addLog('步骤 2:检测到当前位于已登录 ChatGPT 首页,先切换认证入口页再提交手机号。', 'warn');
try {
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
} catch (entryError) {
const entryErrorMessage = getErrorMessage(entryError);
if (await failStep2OnLoggedInSession(signupTabId, entryErrorMessage)) {
return;
}
await addLog('步骤 2:切换认证入口失败,正在重新打开官网入口并重试提交手机号...', 'warn');
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
}
}
const activation = await phoneVerificationHelpers.prepareSignupPhoneActivation(state);
let step2Result = await submitSignupPhone(activation.phoneNumber, activation, {
timeoutMs: 45000,
retryDelayMs: 700,
logMessage: '步骤 2:官网注册入口正在切换,等待手机号注册入口恢复...',
});
if (step2Result?.error) {
const errorMessage = getErrorMessage(step2Result.error);
if (isSignupEntryUnavailableErrorMessage(errorMessage) || isRetryableStep2TransportErrorMessage(errorMessage)) {
await addLog('步骤 2:手机号注册入口不可用或通信超时,正在切换认证入口页后重试一次...', 'warn');
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
step2Result = await submitSignupPhone(activation.phoneNumber, activation, {
timeoutMs: 45000,
retryDelayMs: 700,
logMessage: '步骤 2:认证入口页已打开,正在重新提交手机号...',
});
}
}
if (step2Result?.error) {
const finalErrorMessage = getErrorMessage(step2Result.error);
if (
(isSignupEntryUnavailableErrorMessage(finalErrorMessage)
|| isRetryableStep2TransportErrorMessage(finalErrorMessage))
&& await failStep2OnLoggedInSession(signupTabId, finalErrorMessage)
) {
return;
}
if (typeof phoneVerificationHelpers?.cancelSignupPhoneActivation === 'function') {
await phoneVerificationHelpers.cancelSignupPhoneActivation(state, activation).catch(() => {});
}
throw new Error(finalErrorMessage);
}
await addLog(`步骤 2:手机号 ${activation.phoneNumber} 已提交,正在等待页面加载并确认下一步入口...`);
const landingResult = await ensureSignupPostIdentityPageReadyInTab(signupTabId, 2, {
skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage),
});
await completeStepFromBackground(2, {
accountIdentifierType: 'phone',
accountIdentifier: activation.phoneNumber,
signupPhoneNumber: activation.phoneNumber,
signupPhoneActivation: activation,
nextSignupState: landingResult?.state || step2Result?.state || 'password_page',
nextSignupUrl: landingResult?.url || step2Result?.url || '',
skippedPasswordStep: landingResult?.state === 'phone_verification_page' || landingResult?.state === 'profile_page',
});
}
async function executeSignupEmailEntry(state) {
const resolvedEmail = await resolveSignupEmailForFlow(state);
let signupTabId = await ensureSignupTabForStep2();
if (await shouldForceAuthEntryRetry(signupTabId)) {
await addLog('步骤 2:检测到当前位于已登录 ChatGPT 首页,先切换认证入口页再提交邮箱。', 'warn');
@@ -214,12 +309,21 @@
await completeStepFromBackground(2, {
email: resolvedEmail,
accountIdentifierType: 'email',
accountIdentifier: resolvedEmail,
nextSignupState: landingResult?.state || 'password_page',
nextSignupUrl: landingResult?.url || step2Result?.url || '',
skippedPasswordStep: landingResult?.state === 'verification_page',
});
}
async function executeStep2(state) {
if (resolveSignupMethod(state) === 'phone') {
return executeSignupPhoneEntry(state);
}
return executeSignupEmailEntry(state);
}
return { executeStep2 };
}