feat(flow): stabilize step7-9, expand HeroSMS, and update usage tutorial

This commit is contained in:
daniellee2015
2026-04-29 00:51:05 +08:00
parent 9e26b16f20
commit 4c091a3d32
21 changed files with 5265 additions and 1021 deletions
+346 -1
View File
@@ -265,6 +265,21 @@ const AUTO_STEP_DELAY_MAX_ALLOWED_SECONDS = 600;
const VERIFICATION_RESEND_COUNT_MIN = 0;
const VERIFICATION_RESEND_COUNT_MAX = 20;
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
const PHONE_CODE_POLL_ROUNDS_MIN = 1;
const PHONE_CODE_POLL_ROUNDS_MAX = 120;
const DEFAULT_PHONE_CODE_POLL_ROUNDS = 4;
const LEGACY_AUTO_STEP_DELAY_KEYS = ['autoStepRandomDelayMinSeconds', 'autoStepRandomDelayMaxSeconds'];
const LEGACY_VERIFICATION_RESEND_COUNT_KEYS = ['signupVerificationResendCount', 'loginVerificationResendCount'];
const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
@@ -283,6 +298,10 @@ const HERO_SMS_SERVICE_CODE = 'dr';
const HERO_SMS_SERVICE_LABEL = 'OpenAI';
const HERO_SMS_COUNTRY_ID = 52;
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
const DISPLAY_TIMEZONE = 'Asia/Shanghai';
const MICROSOFT_TOKEN_DNR_RULE_ID = 1001;
const PERSISTENT_ALIAS_STATE_KEYS = [
@@ -446,6 +465,11 @@ const PERSISTED_SETTING_DEFAULTS = {
autoStepDelaySeconds: null,
phoneVerificationEnabled: false,
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
phoneVerificationReplacementLimit: DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT,
phoneCodeWaitSeconds: DEFAULT_PHONE_CODE_WAIT_SECONDS,
phoneCodeTimeoutWindows: DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS,
phoneCodePollIntervalSeconds: DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS,
phoneCodePollMaxRounds: DEFAULT_PHONE_CODE_POLL_ROUNDS,
mailProvider: '163',
mail2925Mode: DEFAULT_MAIL_2925_MODE,
mail2925UseAccountPool: false,
@@ -481,9 +505,12 @@ const PERSISTED_SETTING_DEFAULTS = {
mail2925Accounts: [],
paypalAccounts: [],
heroSmsApiKey: '',
heroSmsReuseEnabled: DEFAULT_HERO_SMS_REUSE_ENABLED,
heroSmsAcquirePriority: DEFAULT_HERO_SMS_ACQUIRE_PRIORITY,
heroSmsMaxPrice: '',
heroSmsCountryId: HERO_SMS_COUNTRY_ID,
heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL,
heroSmsCountryFallback: [],
};
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
@@ -563,7 +590,13 @@ const DEFAULT_STATE = {
currentLuckmailPurchase: null,
currentLuckmailMailCursor: null,
currentPhoneActivation: null,
currentPhoneVerificationCode: '',
reusablePhoneActivation: null,
heroSmsLastPriceTiers: [],
heroSmsLastPriceCountryId: 0,
heroSmsLastPriceCountryLabel: '',
heroSmsLastPriceUserLimit: '',
heroSmsLastPriceAt: 0,
pendingPhoneActivationConfirmation: null,
autoRunning: false, // 当前是否处于自动运行中。
autoRunPhase: 'idle', // 当前自动运行阶段。
@@ -664,6 +697,143 @@ function normalizeVerificationResendCount(value, fallback) {
);
}
function normalizePhoneVerificationReplacementLimit(value, fallback = DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT) {
const rawValue = String(value ?? '').trim();
const numeric = Number(rawValue);
if (!rawValue || !Number.isFinite(numeric)) {
return Math.min(
PHONE_REPLACEMENT_LIMIT_MAX,
Math.max(PHONE_REPLACEMENT_LIMIT_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT))
);
}
return Math.min(
PHONE_REPLACEMENT_LIMIT_MAX,
Math.max(PHONE_REPLACEMENT_LIMIT_MIN, Math.floor(numeric))
);
}
function normalizePhoneCodeWaitSeconds(value, fallback = DEFAULT_PHONE_CODE_WAIT_SECONDS) {
const rawValue = String(value ?? '').trim();
const numeric = Number(rawValue);
if (!rawValue || !Number.isFinite(numeric)) {
return Math.min(
PHONE_CODE_WAIT_SECONDS_MAX,
Math.max(PHONE_CODE_WAIT_SECONDS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_WAIT_SECONDS))
);
}
return Math.min(
PHONE_CODE_WAIT_SECONDS_MAX,
Math.max(PHONE_CODE_WAIT_SECONDS_MIN, Math.floor(numeric))
);
}
function normalizePhoneCodeTimeoutWindows(value, fallback = DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS) {
const rawValue = String(value ?? '').trim();
const numeric = Number(rawValue);
if (!rawValue || !Number.isFinite(numeric)) {
return Math.min(
PHONE_CODE_TIMEOUT_WINDOWS_MAX,
Math.max(PHONE_CODE_TIMEOUT_WINDOWS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS))
);
}
return Math.min(
PHONE_CODE_TIMEOUT_WINDOWS_MAX,
Math.max(PHONE_CODE_TIMEOUT_WINDOWS_MIN, Math.floor(numeric))
);
}
function normalizePhoneCodePollIntervalSeconds(value, fallback = DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS) {
const rawValue = String(value ?? '').trim();
const numeric = Number(rawValue);
if (!rawValue || !Number.isFinite(numeric)) {
return Math.min(
PHONE_CODE_POLL_INTERVAL_SECONDS_MAX,
Math.max(PHONE_CODE_POLL_INTERVAL_SECONDS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS))
);
}
return Math.min(
PHONE_CODE_POLL_INTERVAL_SECONDS_MAX,
Math.max(PHONE_CODE_POLL_INTERVAL_SECONDS_MIN, Math.floor(numeric))
);
}
function normalizePhoneCodePollMaxRounds(value, fallback = DEFAULT_PHONE_CODE_POLL_ROUNDS) {
const rawValue = String(value ?? '').trim();
const numeric = Number(rawValue);
if (!rawValue || !Number.isFinite(numeric)) {
return Math.min(
PHONE_CODE_POLL_ROUNDS_MAX,
Math.max(PHONE_CODE_POLL_ROUNDS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_POLL_ROUNDS))
);
}
return Math.min(
PHONE_CODE_POLL_ROUNDS_MAX,
Math.max(PHONE_CODE_POLL_ROUNDS_MIN, Math.floor(numeric))
);
}
function normalizeHeroSmsMaxPrice(value = '') {
const rawValue = String(value ?? '').trim();
if (!rawValue) {
return '';
}
const numeric = Number(rawValue);
if (!Number.isFinite(numeric) || numeric <= 0) {
return '';
}
return String(Math.round(numeric * 10000) / 10000);
}
function normalizeHeroSmsAcquirePriority(value = '') {
return String(value || '').trim().toLowerCase() === HERO_SMS_ACQUIRE_PRIORITY_PRICE
? HERO_SMS_ACQUIRE_PRIORITY_PRICE
: HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
}
function normalizeHeroSmsCountryFallback(value = []) {
const source = Array.isArray(value)
? value
: String(value || '')
.split(/[\r\n,;]+/)
.map((entry) => String(entry || '').trim())
.filter(Boolean);
const seenIds = new Set();
const normalized = [];
for (const entry of source) {
let countryId = 0;
let countryLabel = '';
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
countryId = Math.floor(Number(entry.countryId ?? entry.id) || 0);
countryLabel = String((entry.countryLabel ?? entry.label) || '').trim();
} else {
const text = String(entry || '').trim();
const structuredMatch = text.match(/^(\d+)\s*(?:[:|/-]\s*(.+))?$/);
if (structuredMatch) {
countryId = Math.floor(Number(structuredMatch[1]) || 0);
countryLabel = String(structuredMatch[2] || '').trim();
} else {
countryId = Math.floor(Number(text) || 0);
}
}
if (!Number.isFinite(countryId) || countryId <= 0 || seenIds.has(countryId)) {
continue;
}
seenIds.add(countryId);
normalized.push({
id: countryId,
label: countryLabel || `Country #${countryId}`,
});
if (normalized.length >= 20) {
break;
}
}
return normalized;
}
function resolveLegacyAutoStepDelaySeconds(input = {}) {
const hasLegacyMin = input.autoStepRandomDelayMinSeconds !== undefined;
const hasLegacyMax = input.autoStepRandomDelayMaxSeconds !== undefined;
@@ -1264,6 +1434,16 @@ function normalizePersistentSettingValue(key, value) {
return normalizeAutoStepDelaySeconds(value, PERSISTED_SETTING_DEFAULTS.autoStepDelaySeconds);
case 'verificationResendCount':
return normalizeVerificationResendCount(value, DEFAULT_VERIFICATION_RESEND_COUNT);
case 'phoneVerificationReplacementLimit':
return normalizePhoneVerificationReplacementLimit(value, DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT);
case 'phoneCodeWaitSeconds':
return normalizePhoneCodeWaitSeconds(value, DEFAULT_PHONE_CODE_WAIT_SECONDS);
case 'phoneCodeTimeoutWindows':
return normalizePhoneCodeTimeoutWindows(value, DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS);
case 'phoneCodePollIntervalSeconds':
return normalizePhoneCodePollIntervalSeconds(value, DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS);
case 'phoneCodePollMaxRounds':
return normalizePhoneCodePollMaxRounds(value, DEFAULT_PHONE_CODE_POLL_ROUNDS);
case 'mailProvider':
return normalizeMailProvider(value);
case 'mail2925Mode':
@@ -1327,12 +1507,18 @@ function normalizePersistentSettingValue(key, value) {
return normalizePayPalAccounts(value);
case 'heroSmsApiKey':
return String(value || '');
case 'heroSmsReuseEnabled':
return Boolean(value);
case 'heroSmsAcquirePriority':
return normalizeHeroSmsAcquirePriority(value);
case 'heroSmsMaxPrice':
return String(value || '').trim();
return normalizeHeroSmsMaxPrice(value);
case 'heroSmsCountryId':
return Math.max(1, Math.floor(Number(value) || HERO_SMS_COUNTRY_ID));
case 'heroSmsCountryLabel':
return String(value || HERO_SMS_COUNTRY_LABEL).trim() || HERO_SMS_COUNTRY_LABEL;
case 'heroSmsCountryFallback':
return normalizeHeroSmsCountryFallback(value);
default:
return value;
}
@@ -1878,6 +2064,7 @@ async function resetState() {
'accounts',
'tabRegistry',
'sourceLastUrls',
'reusablePhoneActivation',
'luckmailApiKey',
'luckmailBaseUrl',
'luckmailEmailType',
@@ -1892,6 +2079,25 @@ async function resetState() {
getPersistedAliasState(),
]);
const contributionModeState = buildContributionModeState(Boolean(prev.contributionMode), persistedSettings, prev);
const reusablePhoneActivation = (
prev.reusablePhoneActivation
&& typeof prev.reusablePhoneActivation === 'object'
&& !Array.isArray(prev.reusablePhoneActivation)
&& String(
prev.reusablePhoneActivation.activationId
?? prev.reusablePhoneActivation.id
?? prev.reusablePhoneActivation.activation
?? ''
).trim()
&& String(
prev.reusablePhoneActivation.phoneNumber
?? prev.reusablePhoneActivation.number
?? prev.reusablePhoneActivation.phone
?? ''
).trim()
)
? prev.reusablePhoneActivation
: null;
await chrome.storage.session.clear();
await chrome.storage.session.set({
...DEFAULT_STATE,
@@ -1912,6 +2118,8 @@ async function resetState() {
luckmailPreserveTagName: String(prev.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
currentLuckmailPurchase: null,
currentLuckmailMailCursor: null,
// Keep reusable phone activation across round resets so the same number can be reactivated up to maxUses.
reusablePhoneActivation,
preferredIcloudHost: prev.preferredIcloudHost || '',
});
}
@@ -6042,6 +6250,7 @@ function getDownstreamStateResets(step, state = {}) {
lastSignupCode: null,
lastLoginCode: null,
localhostUrl: null,
currentPhoneVerificationCode: '',
};
}
if (step === 2) {
@@ -6057,6 +6266,7 @@ function getDownstreamStateResets(step, state = {}) {
lastSignupCode: null,
lastLoginCode: null,
localhostUrl: null,
currentPhoneVerificationCode: '',
};
}
if (step === 3 || step === 4) {
@@ -6071,6 +6281,7 @@ function getDownstreamStateResets(step, state = {}) {
lastSignupCode: null,
lastLoginCode: null,
localhostUrl: null,
currentPhoneVerificationCode: '',
};
}
if (step === 5 || step === 6 || step === 7 || step === 8) {
@@ -6092,6 +6303,7 @@ function getDownstreamStateResets(step, state = {}) {
oauthFlowDeadlineSourceUrl: null,
pendingPhoneActivationConfirmation: null,
localhostUrl: null,
currentPhoneVerificationCode: '',
};
}
if (step === 9) {
@@ -6099,6 +6311,7 @@ function getDownstreamStateResets(step, state = {}) {
pendingPhoneActivationConfirmation: null,
plusReturnUrl: '',
localhostUrl: null,
currentPhoneVerificationCode: '',
};
}
if (stepKey === 'oauth-login' || stepKey === 'fetch-login-code') {
@@ -6109,6 +6322,7 @@ function getDownstreamStateResets(step, state = {}) {
oauthFlowDeadlineSourceUrl: null,
pendingPhoneActivationConfirmation: null,
localhostUrl: null,
currentPhoneVerificationCode: '',
};
}
if (stepKey === 'confirm-oauth') {
@@ -8598,6 +8812,11 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create
const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.createPhoneVerificationHelpers({
addLog,
DEFAULT_HERO_SMS_BASE_URL,
DEFAULT_HERO_SMS_REUSE_ENABLED,
DEFAULT_PHONE_CODE_WAIT_SECONDS,
DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS,
DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS,
DEFAULT_PHONE_CODE_POLL_ROUNDS,
ensureStep8SignupPageReady,
getOAuthFlowStepTimeoutMs,
getState,
@@ -9448,6 +9667,10 @@ async function getPostStep6AutoRestartDecision(step, error) {
const hasTransientNetworkSignal = /connect:\s*connection refused|failed to fetch|i\/o timeout|context deadline exceeded|eof|connection reset by peer/i.test(normalizedMessage);
return mentionsTokenExchange && hasTransientNetworkSignal;
};
const isPhoneVerificationLocalFailure = (errorMessage = '') => {
const normalizedMessage = String(errorMessage || '');
return /HeroSMS|phone verification did not succeed|number replacements|sms_timeout_after_resend|phone number is already linked|add-phone keeps rejecting current number|接码|手机号|手机验证码|步骤\s*9.*(?:手机号|验证码)|Step\s*9.*phone verification/i.test(normalizedMessage);
};
const normalizedStep = Number(step);
const errorMessage = getErrorMessage(error);
@@ -9478,6 +9701,17 @@ async function getPostStep6AutoRestartDecision(step, error) {
};
}
if (isPhoneVerificationLocalFailure(errorMessage)) {
return {
shouldRestart: false,
blockedByAddPhone: true,
forcedByPhoneVerificationTimeout: false,
restartStep: authChainStartStep,
errorMessage,
authState: null,
};
}
if (shouldForceRestartFromStep7) {
return {
shouldRestart: true,
@@ -10080,6 +10314,116 @@ function getStep8EffectLabel(effect) {
}
}
function isStep9OAuthLocalhostTimeoutError(error, visibleStep = 9) {
const message = getErrorMessage(error);
if (!message) {
return false;
}
if (!/从拿到 OAuth 登录地址开始/.test(message)) {
return false;
}
if (!/localhost 回调|OAuth localhost 回调/i.test(message)) {
return false;
}
const normalizedStep = Number(visibleStep);
if (Number.isFinite(normalizedStep) && normalizedStep > 0) {
const stepPrefix = new RegExp(`步骤\\s*${normalizedStep}\\s*`);
if (!stepPrefix.test(message)) {
return false;
}
}
return true;
}
async function recoverOAuthLocalhostTimeout(details = {}) {
const {
error,
state,
visibleStep = 9,
} = details;
if (!isStep9OAuthLocalhostTimeoutError(error, visibleStep)) {
return null;
}
const authLoginStep = typeof getAuthChainStartStepId === 'function'
? getAuthChainStartStepId(state || {})
: FINAL_OAUTH_CHAIN_START_STEP;
const loginCodeStep = Number(visibleStep) >= 12 ? 11 : 8;
await addLog(
`步骤 ${visibleStep}:检测到 OAuth localhost 回调等待窗口已过期,正在复核认证页并回到步骤 ${authLoginStep} 重拉授权链路。`,
'warn'
);
let authState = null;
try {
authState = await getLoginAuthStateFromContent({
timeoutMs: 10000,
responseTimeoutMs: 10000,
logMessage: `步骤 ${visibleStep}:正在复核认证页状态,确认是否可自动恢复 localhost 回调链路...`,
});
} catch (inspectError) {
await addLog(
`步骤 ${visibleStep}:复核认证页状态失败(${getErrorMessage(inspectError)}),将先尝试按步骤 ${loginCodeStep} 收尾恢复。`,
'warn'
);
}
if (isAddPhoneAuthState(authState)) {
const stateLabel = getLoginAuthStateLabel(authState.state);
await addLog(
`步骤 ${visibleStep}:当前认证页为 ${stateLabel},将直接回到步骤 ${authLoginStep} 重新拉起授权链路,避免步骤 8/9 恢复冲突。`,
'warn'
);
} else if (authState && authState.state && !['verification_page', 'oauth_consent_page'].includes(authState.state)) {
const stateLabel = getLoginAuthStateLabel(authState.state);
await addLog(
`步骤 ${visibleStep}:当前认证页为 ${stateLabel},不满足快速恢复条件,将回到步骤 ${authLoginStep} 重开授权链路。`,
'warn'
);
}
const latestState = await getState();
if (!step7Executor?.executeStep7 || !step8Executor?.executeStep8) {
return null;
}
await addLog(
`步骤 ${visibleStep}:正在自动重开步骤 ${authLoginStep} -> ${loginCodeStep},恢复到可继续捕获 localhost 回调的状态。`,
'warn'
);
await step7Executor.executeStep7({
...latestState,
visibleStep: authLoginStep,
});
const stateAfterStep7 = await getState();
await step8Executor.executeStep8({
...stateAfterStep7,
visibleStep: loginCodeStep,
});
const recoveredState = await getState();
const oauthUrl = String(recoveredState?.oauthUrl || state?.oauthUrl || '').trim();
if (oauthUrl && typeof startOAuthFlowTimeoutWindow === 'function') {
await startOAuthFlowTimeoutWindow({
step: Number(visibleStep) || 9,
oauthUrl,
});
}
await setState({
localhostUrl: null,
});
await addLog(
`步骤 ${visibleStep}:已恢复到步骤 ${authLoginStep} -> ${loginCodeStep} 收尾状态,并刷新 OAuth localhost 回调等待窗口,准备重试当前步骤。`,
'warn'
);
return await getState();
}
const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
addLog,
chrome,
@@ -10097,6 +10441,7 @@ const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
getStep8TabUpdatedListener,
isTabAlive,
prepareStep8DebuggerClick,
recoverOAuthLocalhostTimeout,
reloadStep8ConsentPage,
reuseOrCreateTab,
setStep8PendingReject,