(function attachBackgroundPhoneVerification(root, factory) { root.MultiPageBackgroundPhoneVerification = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPhoneVerificationModule() { function createPhoneVerificationHelpers(deps = {}) { const { addLog: rawAddLog = async () => {}, ensureStep8SignupPageReady, fetchImpl = (...args) => fetch(...args), generateRandomBirthday, generateRandomName, getOAuthFlowStepTimeoutMs, getState, requestStop = null, readAuthTabSnapshot = null, sendToContentScript, sendToContentScriptResilient, navigateAuthTabToAddPhone = null, setState, broadcastDataUpdate = null, sleepWithStop, throwIfStopped, DEFAULT_HERO_SMS_OPERATOR = 'any', DEFAULT_HERO_SMS_REUSE_ENABLED = true, HERO_SMS_COUNTRY_ID = 52, HERO_SMS_COUNTRY_LABEL = 'Thailand', HERO_SMS_SERVICE_CODE = 'dr', HERO_SMS_SERVICE_LABEL = 'OpenAI', DEFAULT_PHONE_CODE_WAIT_SECONDS = 60, DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2, DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5, DEFAULT_PHONE_CODE_POLL_ROUNDS = 4, } = deps; const PHONE_ACTIVATION_STATE_KEY = 'currentPhoneActivation'; const PHONE_VERIFICATION_CODE_STATE_KEY = 'currentPhoneVerificationCode'; const REUSABLE_PHONE_ACTIVATION_STATE_KEY = 'reusablePhoneActivation'; const REUSABLE_PHONE_ACTIVATION_POOL_STATE_KEY = 'phoneReusableActivationPool'; const FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY = 'freeReusablePhoneActivation'; const PREFERRED_PHONE_ACTIVATION_STATE_KEY = 'phonePreferredActivation'; const PHONE_RUNTIME_COUNTDOWN_ENDS_AT_KEY = 'currentPhoneVerificationCountdownEndsAt'; const PHONE_RUNTIME_COUNTDOWN_WINDOW_INDEX_KEY = 'currentPhoneVerificationCountdownWindowIndex'; const PHONE_RUNTIME_COUNTDOWN_WINDOW_TOTAL_KEY = 'currentPhoneVerificationCountdownWindowTotal'; const PHONE_NO_SUPPLY_FAILURE_STREAK_STATE_KEY = 'phoneNoSupplyFailureStreak'; const PHONE_CODE_WAIT_SECONDS_MIN = 15; const PHONE_CODE_WAIT_SECONDS_MAX = 300; const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1; const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10; const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1; const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30; const PHONE_CODE_POLL_ROUNDS_MIN = 1; const PHONE_CODE_POLL_ROUNDS_MAX = 120; const DEFAULT_PHONE_POLL_INTERVAL_MS = DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS * 1000; const DEFAULT_PHONE_POLL_TIMEOUT_MS = 180000; const DEFAULT_PHONE_REQUEST_TIMEOUT_MS = 20000; const DEFAULT_PHONE_SUBMIT_ATTEMPTS = 3; const DEFAULT_PHONE_NUMBER_MAX_USES = 3; const DEFAULT_PHONE_NUMBER_REPLACEMENT_LIMIT = 3; const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country'; const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price'; const HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH = 'price_high'; const PHONE_SMS_PROVIDER_HERO = 'hero-sms'; const PHONE_SMS_PROVIDER_5SIM = '5sim'; const PHONE_SMS_PROVIDER_HERO_SMS = PHONE_SMS_PROVIDER_HERO; const PHONE_SMS_PROVIDER_FIVE_SIM = PHONE_SMS_PROVIDER_5SIM; const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms'; const PHONE_SMS_PROVIDER_MADAO = 'madao'; const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO; const DEFAULT_PHONE_SMS_PROVIDER_ORDER = Object.freeze([ PHONE_SMS_PROVIDER_HERO, PHONE_SMS_PROVIDER_5SIM, PHONE_SMS_PROVIDER_NEXSMS, PHONE_SMS_PROVIDER_MADAO, ]); const MAX_PHONE_REUSABLE_POOL = 12; const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::'; const PHONE_STALE_SIGNUP_EMAIL_VERIFICATION_ERROR_CODE = 'PHONE_SIGNUP_STALE_EMAIL_VERIFICATION'; const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::'; const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::'; const PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX = 'PHONE_RESEND_BANNED_NUMBER::'; const PHONE_RESEND_SERVER_ERROR_PREFIX = 'PHONE_RESEND_SERVER_ERROR::'; const PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX = 'PHONE_ROUTE_405_RECOVERY_FAILED::'; const PHONE_MANUAL_FREE_REUSE_ERROR_PREFIX = 'PHONE_MANUAL_FREE_REUSE::'; const PHONE_AUTO_FREE_REUSE_PREPARE_ERROR_PREFIX = 'PHONE_AUTO_FREE_REUSE_PREPARE::'; const FREE_PHONE_REUSE_PREPARE_TIMEOUT_MS = 20000; const FREE_PHONE_REUSE_PREPARE_INTERVAL_MS = 2000; const FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS = 10; const PHONE_SMS_FAILURE_SKIP_THRESHOLD = 2; const MAX_ACTIVATION_PRICE_HINTS = 256; const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze([ { prefix: '84', id: 10, iso: 'VN', label: 'Vietnam' }, { prefix: '66', id: 52, iso: 'TH', label: 'Thailand' }, { prefix: '62', id: 6, iso: 'ID', label: 'Indonesia' }, { prefix: '44', id: 16, iso: 'GB', label: 'United Kingdom' }, { prefix: '81', id: 151, iso: 'JP', label: 'Japan' }, { prefix: '49', id: 43, iso: 'DE', label: 'Germany' }, { prefix: '33', id: 73, iso: 'FR', label: 'France' }, { prefix: '1', id: 187, iso: 'US', label: 'USA' }, ]); const activationPriceHintsByKey = new Map(); const phoneSmsProviderAdaptersById = new Map(); let activePhoneVerificationLogStep = null; let activePhoneVerificationLogStepKey = null; function normalizeLogStep(value) { const step = Math.floor(Number(value) || 0); return step > 0 ? step : null; } function getActivePhoneVerificationVisibleStep(fallback = 9) { return normalizeLogStep(activePhoneVerificationLogStep) || fallback; } function normalizePhoneVerificationLogMessage(message) { return String(message || '') .replace(/^Step\s+9\s+diagnostics\s*:\s*/i, 'diagnostics: ') .replace(/^Step\s+9\s*[::]\s*/i, '') .replace(/^步骤\s*9\s*[::]\s*/, '') .replace(/\bstep\s+9\b/gi, 'current step') .trim(); } async function addLog(message, level = 'info', options = {}) { const normalizedOptions = options && typeof options === 'object' ? { ...options } : {}; const step = normalizeLogStep(normalizedOptions.step || normalizedOptions.visibleStep) || normalizeLogStep(activePhoneVerificationLogStep); if (step) { normalizedOptions.step = step; if (!normalizedOptions.stepKey) { 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 normalizePhoneSmsProvider(value = '') { const rootScope = typeof self !== 'undefined' ? self : globalThis; if (rootScope.PhoneSmsProviderRegistry?.normalizeProviderId) { return rootScope.PhoneSmsProviderRegistry.normalizeProviderId(value); } const normalized = String(value || '').trim().toLowerCase(); if (normalized === PHONE_SMS_PROVIDER_5SIM) { return PHONE_SMS_PROVIDER_5SIM; } if (normalized === 'nexsms') { return 'nexsms'; } if (normalized === PHONE_SMS_PROVIDER_MADAO) { return PHONE_SMS_PROVIDER_MADAO; } return PHONE_SMS_PROVIDER_HERO; } function normalizeUseCount(value) { return Math.max(0, Math.floor(Number(value) || 0)); } function normalizeStringList(value = []) { const source = Array.isArray(value) ? value : []; const seen = new Set(); const normalized = []; source.forEach((entry) => { const text = String(entry || '').trim(); if (!text || seen.has(text)) { return; } seen.add(text); normalized.push(text); }); return normalized; } function buildPhoneSmsCodeKey(message = {}) { return [ message.id ?? message.ID ?? '', message.created_at ?? message.date ?? '', message.code ?? '', message.text ?? '', message.message ?? '', ].map((part) => String(part || '').trim()).filter(Boolean).join('::'); } function collectPhoneSmsCodeKeys(payload) { const smsList = Array.isArray(payload?.sms) ? payload.sms : []; return normalizeStringList(smsList.map((message) => buildPhoneSmsCodeKey(message)).filter(Boolean)); } function normalizePhoneDigits(value) { return String(value || '').replace(/\D+/g, ''); } function phoneNumbersMatch(left, right) { const leftDigits = normalizePhoneDigits(left); const rightDigits = normalizePhoneDigits(right); return Boolean( leftDigits && rightDigits && ( leftDigits === rightDigits || leftDigits.endsWith(rightDigits) || rightDigits.endsWith(leftDigits) ) ); } function normalizeTimestampMs(value) { const numeric = Number(value); if (Number.isFinite(numeric) && numeric > 0) { if (numeric >= 1000000000000) { return Math.floor(numeric); } if (numeric >= 1000000000) { return Math.floor(numeric * 1000); } } const text = String(value || '').trim(); if (!text) { return 0; } const parsed = Date.parse(text); return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0; } function normalizePhoneReplacementLimit(value) { const parsed = Math.floor(Number(value)); if (!Number.isFinite(parsed) || parsed <= 0) { return DEFAULT_PHONE_NUMBER_REPLACEMENT_LIMIT; } return Math.max(1, Math.min(20, parsed)); } function normalizeHeroSmsPriceLimit(value) { if (value === undefined || value === null || String(value).trim() === '') { return null; } const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) { return null; } return Math.round(parsed * 10000) / 10000; } function formatPhonePriceRangeText(minPriceLimit = null, maxPriceLimit = null) { const minPrice = normalizeHeroSmsPriceLimit(minPriceLimit); const maxPrice = normalizeHeroSmsPriceLimit(maxPriceLimit); if (minPrice !== null && maxPrice !== null) { return `${minPrice}~${maxPrice}`; } if (minPrice !== null) { return `${minPrice}~`; } if (maxPrice !== null) { return `~${maxPrice}`; } return 'unbounded'; } function isPhoneNumberUsedError(value) { const text = String(value || '').trim(); if (!text) { return false; } return /phone_max_usage_exceeded|phone_number_in_use|already\s+linked\s+to\s+the\s+maximum\s+number\s+of\s+accounts|phone\s+number\s+is\s+already\s+(?:in\s+use|linked|registered)|phone\s+number\s+has\s+already\s+been\s+used|already\s+associated\s+with\s+another\s+account|not\s+eligible\s+to\s+be\s+used|cannot\s+be\s+used\s+for\s+verification|号码.*(?:已|被).*(?:使用|占用|绑定|注册)|手机号.*(?:已|被).*(?:使用|占用|绑定|注册)|该手机号.*(?:已|被).*(?:使用|占用|绑定|注册)/i.test(text); } function isPhoneNumberUsedFailureReason(value) { const normalized = String(value || '').trim().toLowerCase(); if (!normalized) { return false; } return normalized === 'phone_number_used' || normalized === 'phone_number_in_use' || normalized === 'phone_max_usage_exceeded' || isPhoneNumberUsedError(normalized); } function getPhoneReplacementReleaseAction(reason = '') { const normalized = String(reason || '').trim().toLowerCase(); if (normalized === 'code_rejected' || isPhoneNumberUsedFailureReason(normalized)) { return 'ban'; } return 'cancel'; } function isPhoneNumberInvalidError(value) { const text = String(value || '').trim(); if (!text) { return false; } return /phone\s+number\s+is\s+not\s+valid|invalid\s+phone\s+number|invalid\s+phone|not\s+a\s+valid\s+phone|号码.*无效|手机号.*无效|电话号码.*无效/i.test(text); } function isPhoneNumberDeliveryRefusedError(value) { const text = String(value || '').trim(); if (!text) { return false; } return /无法向此电话号码发送验证码|无法向.*(?:电话号码|手机号|号码).*发送(?:验证码|短信)|(?:不能|无法).*发送.*(?:验证码|短信).*(?:电话号码|手机号|号码)|(?:cannot|can't|could\s*not|couldn't|unable\s+to)\s+(?:send|deliver).{0,80}(?:verification\s+code|code|sms|text(?:\s+message)?).{0,80}(?:phone|number)|(?:verification\s+code|sms|text(?:\s+message)?).{0,80}(?:cannot|can't|could\s*not|couldn't|unable\s+to).{0,80}(?:send|deliver)/i.test(text); } function isWhatsAppPhoneResendResult(value) { if (!value) { return false; } const text = typeof value === 'string' ? value : [ value.channel, value.channelText, value.text, value.buttonText, value.label, value.message, ].filter(Boolean).join(' '); return /whats\s*app/i.test(String(text || '')); } function isRecoverableAddPhoneSubmitError(value) { const text = String(value || '').trim(); if (!text) { return false; } return ( isPhoneNumberInvalidError(text) || /failed\s+to\s+select\b.*add-phone\s+page|missing\s+the\s+country\s+option|could\s+not\s+determine\s+the\s+dial\s+code|add-phone\s+page\s+is\s+missing\s+the\s+phone\s+number\s+input|add-phone\s+page\s+is\s+missing\s+the\s+submit\s+button/i.test(text) ); } function normalizeCountryId(value, fallback = HERO_SMS_COUNTRY_ID) { const parsed = Math.floor(Number(value)); if (Number.isFinite(parsed) && parsed > 0) { return parsed; } const fallbackParsed = Math.floor(Number(fallback)); if (Number.isFinite(fallbackParsed) && fallbackParsed > 0) { return fallbackParsed; } return 0; } function normalizeCountryLabel(value = '', fallback = HERO_SMS_COUNTRY_LABEL) { return String(value || '').trim() || fallback; } function normalizeHeroSmsOperator(value = '', fallback = DEFAULT_HERO_SMS_OPERATOR) { const normalized = String(value || '') .trim() .toLowerCase() .replace(/[^a-z0-9_-]+/g, ''); if (normalized) { return normalized; } const fallbackNormalized = String(fallback || '') .trim() .toLowerCase() .replace(/[^a-z0-9_-]+/g, ''); return fallbackNormalized || DEFAULT_HERO_SMS_OPERATOR; } function inferHeroSmsCountryFromPhoneNumber(phoneNumber = '') { const digits = String(phoneNumber || '').replace(/\D+/g, ''); if (!digits) { return null; } const match = HERO_SMS_COUNTRY_BY_PHONE_PREFIX.find((entry) => digits.startsWith(entry.prefix)); if (!match) { return null; } return { id: normalizeCountryId(match.id, 0), iso: String(match.iso || '').trim().toUpperCase(), label: normalizeCountryLabel(match.label, `Country #${match.id}`), }; } function normalizePhoneCodeWaitSeconds(value) { const parsed = Math.floor(Number(value)); if (!Number.isFinite(parsed) || parsed <= 0) { return DEFAULT_PHONE_CODE_WAIT_SECONDS; } return Math.max(PHONE_CODE_WAIT_SECONDS_MIN, Math.min(PHONE_CODE_WAIT_SECONDS_MAX, parsed)); } function normalizePhoneCodeTimeoutWindows(value) { const parsed = Math.floor(Number(value)); if (!Number.isFinite(parsed) || parsed <= 0) { return DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS; } return Math.max(PHONE_CODE_TIMEOUT_WINDOWS_MIN, Math.min(PHONE_CODE_TIMEOUT_WINDOWS_MAX, parsed)); } function normalizePhoneCodePollIntervalSeconds(value) { const parsed = Math.floor(Number(value)); if (!Number.isFinite(parsed) || parsed <= 0) { return DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS; } return Math.max(PHONE_CODE_POLL_INTERVAL_SECONDS_MIN, Math.min(PHONE_CODE_POLL_INTERVAL_SECONDS_MAX, parsed)); } function normalizePhoneCodePollMaxRounds(value) { const parsed = Math.floor(Number(value)); if (!Number.isFinite(parsed) || parsed <= 0) { return DEFAULT_PHONE_CODE_POLL_ROUNDS; } return Math.max(PHONE_CODE_POLL_ROUNDS_MIN, Math.min(PHONE_CODE_POLL_ROUNDS_MAX, parsed)); } function resolvePhoneCodePollMaxRoundsForWindow(waitSeconds, pollIntervalSeconds, configuredMaxRounds) { const normalizedWaitSeconds = normalizePhoneCodeWaitSeconds(waitSeconds); const normalizedPollIntervalSeconds = normalizePhoneCodePollIntervalSeconds(pollIntervalSeconds); const normalizedConfiguredRounds = normalizePhoneCodePollMaxRounds(configuredMaxRounds); const roundsNeededForWaitWindow = Math.max( PHONE_CODE_POLL_ROUNDS_MIN, Math.ceil(normalizedWaitSeconds / normalizedPollIntervalSeconds) ); return Math.max(normalizedConfiguredRounds, roundsNeededForWaitWindow); } function normalizeHeroSmsReuseEnabled(value) { if (value === undefined || value === null) { return Boolean(DEFAULT_HERO_SMS_REUSE_ENABLED); } return Boolean(value); } function normalizePhoneSmsReuseEnabled(state = {}) { if (Object.prototype.hasOwnProperty.call(state, 'phoneSmsReuseEnabled')) { return Boolean(state.phoneSmsReuseEnabled); } return normalizeHeroSmsReuseEnabled(state?.heroSmsReuseEnabled); } function normalizeFreePhoneReuseEnabled(value) { return Boolean(value); } function normalizeFreePhoneReuseAutoEnabled(state = {}) { return normalizeFreePhoneReuseEnabled(state?.freePhoneReuseEnabled) && Boolean(state?.freePhoneReuseAutoEnabled); } function normalizeHeroSmsAcquirePriority(value = '') { const normalized = String(value || '').trim().toLowerCase(); if (normalized === HERO_SMS_ACQUIRE_PRIORITY_PRICE) { return HERO_SMS_ACQUIRE_PRIORITY_PRICE; } if (normalized === HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH) { return HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH; } return HERO_SMS_ACQUIRE_PRIORITY_COUNTRY; } function normalizePhoneSmsProviderOrder(value = [], fallbackOrder = []) { const rootScope = typeof self !== 'undefined' ? self : globalThis; if (rootScope.PhoneSmsProviderRegistry?.normalizeProviderOrder) { return rootScope.PhoneSmsProviderRegistry.normalizeProviderOrder(value, fallbackOrder); } const source = Array.isArray(value) ? value : String(value || '') .split(/[\r\n,]+/) .map((entry) => String(entry || '').trim()) .filter(Boolean); const normalized = []; const seen = new Set(); source.forEach((entry) => { const provider = normalizePhoneSmsProvider(entry); if (seen.has(provider)) { return; } seen.add(provider); normalized.push(provider); }); if (normalized.length) { return normalized.slice(0, DEFAULT_PHONE_SMS_PROVIDER_ORDER.length); } const fallback = Array.isArray(fallbackOrder) ? fallbackOrder : []; if (!fallback.length) { return []; } const fallbackNormalized = []; fallback.forEach((entry) => { const provider = normalizePhoneSmsProvider(entry); if (!provider || fallbackNormalized.includes(provider)) { return; } fallbackNormalized.push(provider); }); return fallbackNormalized.slice(0, DEFAULT_PHONE_SMS_PROVIDER_ORDER.length); } function resolvePhoneProviderOrder(state = {}, preferredProvider = '') { const currentProvider = normalizePhoneSmsProvider( preferredProvider || state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER ); const hasExplicitOrder = Array.isArray(state?.phoneSmsProviderOrder) ? state.phoneSmsProviderOrder.length > 0 : String(state?.phoneSmsProviderOrder || '').trim().length > 0; if (hasExplicitOrder) { const explicitOrder = normalizePhoneSmsProviderOrder( state?.phoneSmsProviderOrder, [] ); if (explicitOrder.length) { return explicitOrder; } return [currentProvider]; } const fallbackOrder = normalizePhoneSmsProviderOrder( [currentProvider], DEFAULT_PHONE_SMS_PROVIDER_ORDER ); if (fallbackOrder[0] === currentProvider) { return fallbackOrder; } const withoutCurrent = fallbackOrder.filter((provider) => provider !== currentProvider); return [currentProvider, ...withoutCurrent].slice(0, DEFAULT_PHONE_SMS_PROVIDER_ORDER.length); } function getActivationProviderId(activation = {}, state = {}) { return normalizePhoneSmsProvider(activation?.provider || state?.phoneSmsProvider); } function scopeStateToActivationProvider(state = {}, activation = {}) { const provider = getActivationProviderId(activation, state); if (!provider) { return state || {}; } return { ...(state || {}), phoneSmsProvider: provider, }; } function getPhoneSmsProviderLabel(providerId) { const provider = normalizePhoneSmsProvider(providerId); if (provider === PHONE_SMS_PROVIDER_FIVE_SIM) { return '5sim'; } if (provider === PHONE_SMS_PROVIDER_NEXSMS) { return 'NexSMS'; } if (provider === PHONE_SMS_PROVIDER_MADAO) { return 'MaDao'; } return 'HeroSMS'; } function formatStep9Reason(reason = '') { const text = String(reason || '').trim(); if (!text) { return '未知'; } const normalized = text.toLowerCase(); const reasonMap = { returned_to_add_phone_loop: '反复返回添加手机号页', phone_number_used: '手机号已被使用', sms_not_received: '未收到短信', sms_timeout: '短信超时', resend_throttled: '重发短信被限流', code_rejected: '验证码被拒绝', add_phone_rejected: '添加手机号被拒绝', activation_not_found: '接码订单不存在或已失效', resend_phone_banned: 'OpenAI 无法向该号码发送短信', phone_max_usage_exceeded: '手机号达到使用上限', resend_server_error: '重发短信后进入服务器错误页', whatsapp_resend_channel: '页面重发入口切换为 WhatsApp 通道', unknown: '未知', }; if (reasonMap[normalized]) { return reasonMap[normalized]; } const timeoutWindowMatch = text.match(/^sms_timeout_after_(\d+)_windows$/i); if (timeoutWindowMatch) { return `连续 ${timeoutWindowMatch[1]} 轮等待后仍未收到短信`; } return text; } function formatPhoneSmsApiFailureReason(reason = '') { const text = String(reason || '').trim(); if (!text) { return '未知错误'; } if (/\bBAD_KEY\b|\bWRONG_KEY\b|\bINVALID_KEY\b/i.test(text)) { return 'API Key 无效(BAD_KEY)'; } if (/\bNO_BALANCE\b|\bNOT_ENOUGH_BALANCE\b/i.test(text)) { return '余额不足'; } if (/\bBANNED\b|\bACCOUNT_BANNED\b/i.test(text)) { return '账号已被封禁'; } if (/\bNO_NUMBERS\b/i.test(text)) { return '暂无可用号码(NO_NUMBERS)'; } if (/no\s+free\s+phones|numbers?\s+not\s+found|no\s+numbers\s+available|no\s+numbers\s+within|暂无可用号码|均无可用号码|无可用号码/i.test(text)) { return '暂无可用号码'; } const wrongMaxPrice = text.match(/\bWRONG_MAX_PRICE(?::|\s+requires\s+)?(\d+(?:\.\d+)?)?\b/i); if (wrongMaxPrice) { return wrongMaxPrice[1] ? `价格上限过低,平台要求至少 ${wrongMaxPrice[1]}(WRONG_MAX_PRICE)` : '价格上限不符合平台要求(WRONG_MAX_PRICE)'; } if (/rate\s*limit|too\s+many\s+requests|限流/i.test(text)) { return '请求限流'; } if (/unauthorized|forbidden|invalid\s+token|bad\s+key|wrong\s+key/i.test(text)) { return 'API Key 无效'; } if (/order\s+not\s+found|activation\s+not\s+found|no\s+such\s+order/i.test(text)) { return '订单不存在或已失效'; } if (/timed\s*out|timeout/i.test(text)) { return '请求超时'; } if (/failed\s+to\s+fetch|networkerror|load\s+failed/i.test(text)) { return '网络请求失败'; } if (/empty\s+response/i.test(text)) { return '空响应'; } if (/unknown\s+terminal\s+error/i.test(text)) { return '未知终止错误'; } return text; } function formatHeroSmsActionName(action = '') { const normalized = String(action || '').trim().toLowerCase(); if (normalized === 'getnumber' || normalized === 'getnumberv2') { return '获取手机号'; } if (normalized === 'getstatus' || normalized === 'getstatusv2') { return '查询短信状态'; } if (normalized === 'setstatus') { return '更新订单状态'; } if (normalized === 'getprices' || normalized === 'getpricesextended') { return '查询价格'; } return action ? `${action} 请求` : '请求'; } function stripRepeatedHeroSmsFailurePrefix(action, reason = '') { const actionText = String(action || '').trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); if (!actionText) { return String(reason || '').trim(); } let text = String(reason || '').trim(); const prefixPattern = new RegExp(`^HeroSMS\\s+${actionText}\\s+failed\\s*:\\s*`, 'i'); while (prefixPattern.test(text)) { text = text.replace(prefixPattern, '').trim(); } return text; } function formatProviderAcquireFailure(providerId, message = '') { const providerLabel = getPhoneSmsProviderLabel(providerId); let text = String(message || '').trim(); if (!text) { return '未知错误'; } text = text.replace(/^Step\s+\d+\s*[::]\s*/i, '').trim(); const heroFailureMatch = text.match(/^HeroSMS\s+([A-Za-z0-9]+)\s+failed\s*:\s*(.+)$/i); if (heroFailureMatch) { return `${formatHeroSmsActionName(heroFailureMatch[1])}失败:${formatPhoneSmsApiFailureReason(stripRepeatedHeroSmsFailurePrefix(heroFailureMatch[1], heroFailureMatch[2]))}`; } if (normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_HERO && /^HeroSMS\s+.+失败:/.test(text)) { return text.replace(/^HeroSMS\s+/, '').trim(); } if (/countries\s+are\s+empty|未选择国家/i.test(text)) { return '未选择国家,请先在接码设置中至少选择 1 个国家'; } if (/failed\s+to\s+acquire\s+(?:a\s+)?phone(?:\s+number|\s+activation)?/i.test(text)) { return '获取手机号失败'; } if (/no\s+numbers\s+available\s+across|no\s+free\s+phones|numbers?\s+not\s+found|no\s+numbers\s+within|暂无可用号码|均无可用号码|无可用号码|\bNO_NUMBERS\b/i.test(text)) { return formatPhoneSmsApiFailureReason(text); } if (/buy activation failed|purchase failed|price lookup failed|check activation failed/i.test(text)) { return text .replace(/^5sim\s+buy activation failed\s*:\s*/i, '购买手机号失败:') .replace(/^5sim\s+check activation failed\s*:\s*/i, '查询短信状态失败:') .replace(/^NexSMS\s+purchase failed\s*:\s*/i, '购买手机号失败:') .replace(/^NexSMS\s+price lookup failed\s*:\s*/i, '查询价格失败:'); } if (providerLabel && text.startsWith(`${providerLabel}:`)) { return text.slice(providerLabel.length + 1).trim() || text; } return text; } function isPhoneSmsReuseEnabled(state = {}) { if (isPhoneSignupIdentityState(state)) { return false; } return normalizePhoneSmsReuseEnabled(state); } function getPhoneSmsProviderAdapterForState(state = {}, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER)) { const rootScope = typeof self !== 'undefined' ? self : globalThis; if (!rootScope.PhoneSmsProviderRegistry?.createProvider) { throw new Error('接码平台 registry 未加载。'); } const normalizedProviderId = normalizePhoneSmsProvider(providerId); if (phoneSmsProviderAdaptersById.has(normalizedProviderId)) { return phoneSmsProviderAdaptersById.get(normalizedProviderId); } const providerAdapter = rootScope.PhoneSmsProviderRegistry.createProvider(normalizedProviderId, { addLog, fetchImpl, requestTimeoutMs: DEFAULT_PHONE_REQUEST_TIMEOUT_MS, setState, sleepWithStop, throwIfStopped, }); phoneSmsProviderAdaptersById.set(normalizedProviderId, providerAdapter); return providerAdapter; } function getActivationProviderAdapterForState(state = {}, activation = null) { return getPhoneSmsProviderAdapterForState( state, getActivationProviderId(activation || {}, state) ); } function readPhoneSmsProviderCapability(provider, capabilityName, fallback = false) { if (!provider || !capabilityName) { return Boolean(fallback); } const capabilities = provider.capabilities && typeof provider.capabilities === 'object' ? provider.capabilities : {}; if (typeof capabilities[capabilityName] === 'boolean') { return capabilities[capabilityName]; } return Boolean(fallback); } function callPhoneSmsProviderCapability(provider, methodName, args = [], fallbackCapability = '', fallback = false) { if (provider && typeof provider[methodName] === 'function') { return Boolean(provider[methodName](...args)); } return readPhoneSmsProviderCapability(provider, fallbackCapability, fallback); } function getPhoneSmsProviderAdapterForActivation(state = {}, activation = null) { const scopedState = scopeStateToActivationProvider(state, activation || {}); return getActivationProviderAdapterForState(scopedState, activation); } function callPhoneSmsProviderMethod(provider, methodName, args = [], fallback = undefined) { if (provider && typeof provider[methodName] === 'function') { return provider[methodName](...args); } return fallback; } function getProviderCountryKey(provider, value) { const normalized = callPhoneSmsProviderMethod(provider, 'normalizeCountryKey', [value], null); if (normalized !== null && normalized !== undefined) { return String(normalized || '').trim(); } const countryId = normalizeCountryId(value, 0); return countryId > 0 ? String(countryId) : ''; } function getProviderActivationCountryKey(state = {}, activation = null) { const provider = getPhoneSmsProviderAdapterForActivation(state, activation || {}); const providerKey = callPhoneSmsProviderMethod(provider, 'getActivationCountryKey', [activation], null); if (providerKey !== null && providerKey !== undefined) { return String(providerKey || '').trim(); } return getProviderCountryKey(provider, activation?.countryId ?? activation?.country); } function getProviderCountryLabel(state = {}, providerId = DEFAULT_PHONE_SMS_PROVIDER, countryId = '') { const scopedState = scopeStateToActivationProvider(state, { provider: providerId }); const provider = getPhoneSmsProviderAdapterForState(scopedState, providerId); const label = callPhoneSmsProviderMethod(provider, 'resolveCountryLabel', [scopedState, countryId], ''); return String(label || countryId || 'Unknown country').trim() || 'Unknown country'; } function getProviderActivationPrice(state = {}, activation = null) { const provider = getPhoneSmsProviderAdapterForActivation(state, activation || {}); const providerPrice = callPhoneSmsProviderMethod(provider, 'getActivationPrice', [activation], null); const normalizedProviderPrice = normalizeHeroSmsPrice(providerPrice); if (normalizedProviderPrice !== null && normalizedProviderPrice > 0) { return normalizedProviderPrice; } return normalizeHeroSmsPrice( activation?.price ?? activation?.maxPrice ?? activation?.selectedPrice ?? activation?.madaoPrice ); } function normalizeCountryFallbackList(value = []) { const source = Array.isArray(value) ? value : String(value || '') .split(/[\r\n,,;;]+/) .map((entry) => String(entry || '').trim()) .filter(Boolean); const seen = new Set(); const normalized = []; for (const entry of source) { let id = 0; let label = ''; if (entry && typeof entry === 'object' && !Array.isArray(entry)) { id = normalizeCountryId(entry.id ?? entry.countryId, 0); label = String((entry.label ?? entry.countryLabel) || '').trim(); } else { const text = String(entry || '').trim(); const structured = text.match(/^(\d+)\s*(?:[:|/-]\s*(.+))?$/); if (structured) { id = normalizeCountryId(structured[1], 0); label = String(structured[2] || '').trim(); } else { id = normalizeCountryId(text, 0); } } if (!Number.isFinite(id) || id <= 0 || seen.has(id)) { continue; } seen.add(id); normalized.push({ id, label: label || `Country #${id}`, }); } return normalized; } function resolveCountryConfig(state = {}) { const hasExplicitPrimaryCountry = Object.prototype.hasOwnProperty.call(state || {}, 'heroSmsCountryId'); const fallbackList = normalizeCountryFallbackList(state.heroSmsCountryFallback); const primaryCountryId = normalizeCountryId(state.heroSmsCountryId, 0); if (primaryCountryId > 0) { return { id: primaryCountryId, label: normalizeCountryLabel(state.heroSmsCountryLabel, HERO_SMS_COUNTRY_LABEL), }; } if (hasExplicitPrimaryCountry) { if (fallbackList.length) { const firstFallback = fallbackList[0]; return { id: normalizeCountryId(firstFallback.id, 0), label: normalizeCountryLabel(firstFallback.label, `Country #${firstFallback.id}`), }; } return null; } return { id: normalizeCountryId(HERO_SMS_COUNTRY_ID, HERO_SMS_COUNTRY_ID), label: normalizeCountryLabel(state.heroSmsCountryLabel, HERO_SMS_COUNTRY_LABEL), }; } function resolveCountryCandidates(state = {}) { const primary = resolveCountryConfig(state); const fallbackList = normalizeCountryFallbackList(state.heroSmsCountryFallback); if (!primary || !Number.isFinite(primary.id) || primary.id <= 0) { return fallbackList .map((entry) => ({ id: normalizeCountryId(entry.id, 0), label: normalizeCountryLabel(entry.label, `Country #${entry.id}`), })) .filter((entry) => entry.id > 0); } const seen = new Set([primary.id]); const candidates = [primary]; fallbackList.forEach((entry) => { const nextId = normalizeCountryId(entry.id, 0); if (!Number.isFinite(nextId) || nextId <= 0 || seen.has(nextId)) { return; } seen.add(nextId); candidates.push({ id: nextId, label: normalizeCountryLabel(entry.label, `Country #${nextId}`), }); }); return candidates; } function normalizeActivation(record) { if (!record || typeof record !== 'object' || Array.isArray(record)) { return null; } const rawProvider = String(record.provider || '').trim(); const provider = normalizePhoneSmsProvider(rawProvider); try { const adapter = getPhoneSmsProviderAdapterForState({ phoneSmsProvider: provider }, provider); const adapterActivation = callPhoneSmsProviderMethod(adapter, 'normalizeActivation', [record], null); if (adapterActivation) { const expiresAt = normalizeTimestampMs(record.expiresAt || adapterActivation.expiresAt); const ignoredPhoneCodeKeys = normalizeStringList( record.ignoredPhoneCodeKeys || adapterActivation.ignoredPhoneCodeKeys ); return { ...adapterActivation, provider: normalizePhoneSmsProvider(adapterActivation.provider || provider), successfulUses: normalizeUseCount(adapterActivation.successfulUses), maxUses: Math.max(1, Math.floor(Number(adapterActivation.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)), ...(expiresAt > 0 ? { expiresAt } : {}), ...(record.source ? { source: String(record.source || '').trim() } : {}), ...(record.phoneCodeReceived || adapterActivation.phoneCodeReceived ? { phoneCodeReceived: true } : {}), ...(record.phoneCodeReceivedAt || adapterActivation.phoneCodeReceivedAt ? { phoneCodeReceivedAt: Math.max(0, Number(record.phoneCodeReceivedAt || adapterActivation.phoneCodeReceivedAt) || 0) } : {}), ...(ignoredPhoneCodeKeys.length ? { ignoredPhoneCodeKeys } : {}), }; } } catch (_) { // Registry is loaded in normal runtime; keep the small legacy fallback below for isolated tests. } const activationId = String( record.activationId ?? record.id ?? record.activation ?? '' ).trim(); const phoneNumber = String( record.phoneNumber ?? record.number ?? record.phone ?? '' ).trim(); if (!activationId || !phoneNumber) { return null; } const statusAction = String(record.statusAction || '').trim(); const countryLabel = String(record.countryLabel || '').trim(); const rawCountryId = record.countryId ?? record.country; const fallbackCountryId = HERO_SMS_COUNTRY_ID; const expiresAt = normalizeTimestampMs(record.expiresAt); const serviceCode = String(record.serviceCode || HERO_SMS_SERVICE_CODE).trim(); const countryId = normalizeCountryId(rawCountryId, fallbackCountryId); const ignoredPhoneCodeKeys = normalizeStringList(record.ignoredPhoneCodeKeys); return { activationId, phoneNumber, provider, serviceCode, countryId, ...(countryLabel ? { countryLabel } : {}), successfulUses: normalizeUseCount(record.successfulUses), maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)), ...(expiresAt > 0 ? { expiresAt } : {}), ...(statusAction ? { statusAction } : {}), ...(record.source ? { source: String(record.source || '').trim() } : {}), ...(record.phoneCodeReceived ? { phoneCodeReceived: true } : {}), ...(record.phoneCodeReceivedAt ? { phoneCodeReceivedAt: Math.max(0, Number(record.phoneCodeReceivedAt) || 0) } : {}), ...(ignoredPhoneCodeKeys.length ? { ignoredPhoneCodeKeys } : {}), }; } function normalizeManualFreeReusablePhoneActivation(record) { if (!record || typeof record !== 'object' || Array.isArray(record)) { return null; } const phoneNumber = String( record.phoneNumber ?? record.number ?? record.phone ?? '' ).trim(); if (!phoneNumber) { return null; } const activationId = String( record.activationId ?? record.id ?? record.activation ?? '' ).trim(); const inferredCountry = inferHeroSmsCountryFromPhoneNumber(phoneNumber); const countryId = normalizeCountryId(record.countryId, inferredCountry?.id || HERO_SMS_COUNTRY_ID); const countryLabel = String( record.countryLabel || (inferredCountry && inferredCountry.id === countryId ? inferredCountry.label : '') ).trim(); const statusAction = String(record.statusAction || '').trim(); return { ...(activationId ? { activationId } : {}), phoneNumber, provider: PHONE_SMS_PROVIDER_HERO, serviceCode: String(record.serviceCode || HERO_SMS_SERVICE_CODE).trim() || HERO_SMS_SERVICE_CODE, countryId, ...(countryLabel ? { countryLabel } : {}), successfulUses: normalizeUseCount(record.successfulUses), maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)), ...(statusAction ? { statusAction } : {}), source: 'free-manual-reuse', recordedAt: Math.max(0, Number(record.recordedAt) || Date.now()), manualOnly: !activationId, }; } function normalizeFreeReusablePhoneActivation(record) { const normalized = normalizeActivation(record) || normalizeManualFreeReusablePhoneActivation(record); if (!normalized) { return null; } const recordedAt = Math.max(0, Number(record?.recordedAt) || 0); const reusableActivation = { ...normalized, provider: normalized.provider, source: 'free-manual-reuse', ...(recordedAt ? { recordedAt } : {}), }; delete reusableActivation.phoneCodeReceived; delete reusableActivation.phoneCodeReceivedAt; delete reusableActivation.ignoredPhoneCodeKeys; return reusableActivation; } function markActivationPhoneCodeReceived(activation) { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { return null; } return { ...normalizedActivation, phoneCodeReceived: true, phoneCodeReceivedAt: normalizedActivation.phoneCodeReceivedAt || Date.now(), }; } function normalizeActivationPool(value = []) { const source = Array.isArray(value) ? value : []; const normalized = []; const seen = new Set(); source.forEach((entry) => { const activation = normalizeActivation(entry); if (!activation) { return; } const key = buildActivationIdentityKey(activation); if (!key || seen.has(key)) { return; } seen.add(key); normalized.push(activation); }); return normalized.slice(0, MAX_PHONE_REUSABLE_POOL); } function buildActivationIdentityKey(activation) { const normalized = normalizeActivation(activation); if (!normalized) { return ''; } return [ normalizePhoneSmsProvider(normalized.provider || ''), String(normalized.activationId || '').trim(), String(normalized.phoneNumber || '').trim(), ].join('::'); } function isSameActivation(left, right) { const leftKey = buildActivationIdentityKey(left); const rightKey = buildActivationIdentityKey(right); return Boolean(leftKey && rightKey && leftKey === rightKey); } function rememberActivationAcquiredPrice(activation, price) { const key = buildActivationIdentityKey(activation); const normalizedPrice = normalizeHeroSmsPrice(price); if (!key || normalizedPrice === null || normalizedPrice <= 0) { return; } const roundedPrice = Math.round(normalizedPrice * 10000) / 10000; activationPriceHintsByKey.set(key, roundedPrice); while (activationPriceHintsByKey.size > MAX_ACTIVATION_PRICE_HINTS) { const oldest = activationPriceHintsByKey.keys().next(); if (oldest?.done) { break; } activationPriceHintsByKey.delete(oldest.value); } } function getActivationAcquiredPriceHint(activation) { const key = buildActivationIdentityKey(activation); if (!key) { return null; } const raw = activationPriceHintsByKey.get(key); const normalizedPrice = normalizeHeroSmsPrice(raw); return normalizedPrice === null || normalizedPrice <= 0 ? null : Math.round(normalizedPrice * 10000) / 10000; } function forgetActivationAcquiredPriceHint(activation) { const key = buildActivationIdentityKey(activation); if (!key) { return; } activationPriceHintsByKey.delete(key); } async function setPhoneRuntimeState(updates = {}) { await setState(updates); if (typeof broadcastDataUpdate === 'function') { broadcastDataUpdate(updates); } } async function persistFreeReusableActivation(activation) { await setPhoneRuntimeState({ [FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY]: normalizeFreeReusablePhoneActivation(activation), }); } async function clearFreeReusableActivation() { await setPhoneRuntimeState({ [FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY]: null, }); } function buildPhoneCodeTimeoutError(lastResponse = '') { const suffix = lastResponse ? ` HeroSMS 最后状态:${lastResponse}` : ''; return new Error(`${PHONE_CODE_TIMEOUT_ERROR_PREFIX}等待手机验证码超时。${suffix}`); } function isSignupEmailVerificationPageState(pageState = {}) { const url = String(pageState?.url || pageState?.href || '').trim(); return Boolean( pageState?.emailVerificationPage || pageState?.emailVerificationRequired || /\/email-verification(?:[/?#]|$)/i.test(url) ); } function buildSignupPhoneStaleEmailVerificationError(pageState = {}) { const url = String(pageState?.url || pageState?.href || '').trim(); const message = `步骤 4:OpenAI 在手机短信验证码提交前已切到邮箱验证${url ? `(URL: ${url})` : ''}。这通常表示当前手机号已关联现有账号或登录路径,请更换手机号后重新开始注册。`; const error = new Error(message); error.code = PHONE_STALE_SIGNUP_EMAIL_VERIFICATION_ERROR_CODE; error.stalePhoneSignupEmailVerification = true; if (url) { error.url = url; } error.pageState = pageState; return error; } function isPhoneCodeTimeoutError(error) { return String(error?.message || '').startsWith(PHONE_CODE_TIMEOUT_ERROR_PREFIX); } function isStaleSignupPhoneEmailVerificationError(error) { return Boolean( error?.stalePhoneSignupEmailVerification || error?.code === PHONE_STALE_SIGNUP_EMAIL_VERIFICATION_ERROR_CODE ); } function isPhoneResendThrottledError(error) { const message = String(error?.message || error || '').trim(); if (!message) { return false; } if (message.startsWith(PHONE_RESEND_THROTTLED_ERROR_PREFIX)) { return true; } return /tried\s+to\s+resend\s+too\s+many\s+times|please\s+try\s+again\s+later|too\s+many\s+resend|resend\s+too\s+many|发送.*过于频繁|稍后再试/i.test(message); } function isPhoneResendBannedNumberError(error) { const message = String(error?.message || error || '').trim(); if (!message) { return false; } if (message.startsWith(PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX)) { return true; } return /无法向此电话号码发送短信|无法向此手机号发送短信|无法发送短信到此电话号码|无法发送短信到此手机号|can(?:not|'t)\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number|unable\s+to\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number/i.test(message); } function isPhoneResendServerError(error) { const message = String(error?.message || error || '').trim(); if (!message) { return false; } if (message.startsWith(PHONE_RESEND_SERVER_ERROR_PREFIX)) { return true; } return /this\s+page\s+isn['’]?t\s+working|currently\s+unable\s+to\s+handle\s+this\s+request|http\s+error\s+500|500\s+internal\s+server\s+error/i.test(message); } function buildPhoneResendServerError(error) { const message = String(error?.message || error || '').trim(); if (message.startsWith(PHONE_RESEND_SERVER_ERROR_PREFIX)) { return new Error(message); } return new Error(`${PHONE_RESEND_SERVER_ERROR_PREFIX}${message || 'OpenAI contact-verification 页面在重发短信后返回 HTTP ERROR 500。'}`); } function getPhoneResendServerErrorFromSnapshot(snapshot = {}) { const rawUrl = String(snapshot?.url || snapshot?.href || '').trim(); if (!/\/contact-verification(?:[/?#]|$)/i.test(rawUrl)) { return ''; } const bodyText = [ snapshot?.text, snapshot?.bodyText, ] .filter(Boolean) .join(' ') .replace(/\s+/g, ' ') .trim(); const titleText = String(snapshot?.title || '').replace(/\s+/g, ' ').trim(); if (!bodyText) { return isPhoneResendServerError(titleText) ? (titleText || 'OpenAI contact-verification 页面在重发短信后返回 HTTP ERROR 500。') : ''; } const combined = [ bodyText, titleText, ] .filter(Boolean) .join(' ') .replace(/\s+/g, ' ') .trim(); if (!isPhoneResendServerError(combined)) { return ''; } return combined || 'OpenAI contact-verification 页面在重发短信后返回 HTTP ERROR 500。'; } async function readPhoneResendServerErrorFromAuthTab(tabId) { if (typeof readAuthTabSnapshot !== 'function') { return ''; } try { return getPhoneResendServerErrorFromSnapshot(await readAuthTabSnapshot(tabId)); } catch (_) { return ''; } } async function throwPhoneResendServerErrorIfAuthTabShowsIt(tabId) { const serverErrorText = await readPhoneResendServerErrorFromAuthTab(tabId); if (serverErrorText) { throw buildPhoneResendServerError(serverErrorText); } } function shouldTreatResendThrottledAsBanned(state = {}) { return Boolean(state?.phoneResendThrottledAsBannedEnabled); } function buildHighRiskResendThrottledError(message = '') { return new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${message || 'OpenAI 重发短信被限流,且当前配置会按高概率封禁手机号处理。'}`); } function buildPhoneMaxUsageExceededError(message = '') { return new Error(`PHONE_MAX_USAGE_EXCEEDED::${message || 'OpenAI 返回 phone_max_usage_exceeded,当前手机号已达到使用上限。'}`); } function isPhoneMaxUsageExceededFlowError(error) { const message = String(error?.message || error || '').trim(); return message.startsWith('PHONE_MAX_USAGE_EXCEEDED::') || isPhoneNumberUsedError(message); } function isPhoneRoute405RecoveryError(error) { const message = String(error?.message || error || '').trim(); if (!message) { return false; } if (message.startsWith(PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX)) { return true; } return /route\s+error.*405|405\s+method\s+not\s+allowed|post\s+request\s+to\s+["']?\/phone-verification|did\s+not\s+provide\s+an?\s+[`'"]?action/i.test(message); } function isPhoneActivationOrderMissingError(error, provider = '') { const message = String(error?.message || error || '').trim(); if (!message) { return false; } const normalizedProvider = normalizePhoneSmsProvider(provider); if (normalizedProvider === PHONE_SMS_PROVIDER_5SIM) { return /5sim\s+check\s+activation\s+failed.*order\s+not\s+found|order\s+not\s+found|activation\s+not\s+found|no\s+such\s+order|订单不存在|订单.*失效/i.test(message); } return /activation\s+not\s+found|order\s+not\s+found|no\s+such\s+order|订单不存在|订单.*失效/i.test(message); } function isStopRequestedError(error) { const message = String(error?.message || error || '').trim(); if (!message) { return false; } return message === '流程已被用户停止。' || /已被用户停止/.test(message) || /flow\s+was\s+stopped|stopped\s+by\s+user/i.test(message); } function isAuthContentScriptUnreachableError(error) { const message = String(error?.message || error || '').trim(); return /Receiving end does not exist|Could not establish connection|Frame with ID \d+ is showing error page|等待认证页状态检查超时/i.test(message); } function buildPhoneRestartStep7Error(phoneNumber = '') { const suffix = phoneNumber ? ` 当前号码:${phoneNumber}。` : ''; return new Error( `${PHONE_RESTART_STEP7_ERROR_PREFIX}手机验证重发后仍未收到短信,请从步骤 7 重新获取新号码。${suffix}` ); } function buildPhoneReplacementLimitError(maxNumberReplacementAttempts, reason = '') { const safeMax = Math.max(0, Math.floor(Number(maxNumberReplacementAttempts) || 0)); const safeReason = String(reason || 'unknown').trim() || 'unknown'; return new Error( `步骤 9:更换 ${safeMax} 次号码后手机号验证仍未成功。最后原因:${formatStep9Reason(safeReason)}。` ); } function sanitizePhoneCodeTimeoutError(error) { const message = String(error?.message || ''); if (!message.startsWith(PHONE_CODE_TIMEOUT_ERROR_PREFIX)) { return error; } return new Error(message.slice(PHONE_CODE_TIMEOUT_ERROR_PREFIX.length).trim() || '等待手机验证码超时。'); } function sanitizePhoneRestartStep7Error(error) { const message = String(error?.message || ''); if (!message.startsWith(PHONE_RESTART_STEP7_ERROR_PREFIX)) { return error; } return new Error( message.slice(PHONE_RESTART_STEP7_ERROR_PREFIX.length).trim() || '手机验证重发后仍未收到短信,请从步骤 7 重新获取新号码。' ); } const LATEST_PHONE_SETTING_KEYS = Object.freeze([ 'phoneSmsProvider', 'phoneSmsProviderOrder', 'phoneSmsReuseEnabled', 'madaoBaseUrl', 'madaoHttpSecret', 'madaoMode', 'madaoRoutingPlanId', 'madaoProviderId', 'madaoCountry', 'madaoAutoPickCountry', 'madaoReusePhone', 'madaoMinPrice', 'madaoMaxPrice', 'heroSmsApiKey', 'heroSmsBaseUrl', 'heroSmsCountryId', 'heroSmsCountryLabel', 'heroSmsCountryFallback', 'heroSmsAcquirePriority', 'heroSmsOperator', 'heroSmsMinPrice', 'heroSmsMaxPrice', 'heroSmsPreferredPrice', 'heroSmsActivationRetryRounds', 'heroSmsActivationRetryDelayMs', 'fiveSimApiKey', 'fiveSimBaseUrl', 'fiveSimCountryId', 'fiveSimCountryLabel', 'fiveSimCountryFallback', 'fiveSimCountryOrder', 'fiveSimOperator', 'fiveSimProduct', 'fiveSimMinPrice', 'fiveSimMaxPrice', 'nexSmsApiKey', 'nexSmsBaseUrl', 'nexSmsCountryOrder', 'nexSmsServiceCode', 'phoneVerificationReplacementLimit', 'phoneCodeWaitSeconds', 'phoneCodeTimeoutWindows', 'phoneCodePollIntervalSeconds', 'phoneCodePollMaxRounds', 'freePhoneReuseEnabled', 'freePhoneReuseAutoEnabled', ]); async function mergeLatestPhoneSettingsState(state = {}, options = {}) { if (typeof getState !== 'function') { return state || {}; } try { const latestState = await getState(); if (!latestState || typeof latestState !== 'object') { return state || {}; } const mergedState = { ...latestState, ...(state || {}), }; const preservePhoneSmsProvider = Boolean(options?.preservePhoneSmsProvider); LATEST_PHONE_SETTING_KEYS.forEach((key) => { if (preservePhoneSmsProvider && key === 'phoneSmsProvider') { return; } if (Object.prototype.hasOwnProperty.call(latestState, key)) { mergedState[key] = latestState[key]; } }); mergedState.heroSmsOperator = normalizeHeroSmsOperator( mergedState.heroSmsOperator, DEFAULT_HERO_SMS_OPERATOR ); return mergedState; } catch (_) { return state || {}; } } function normalizeHeroSmsPrice(value) { const direct = Number(value); if (Number.isFinite(direct) && direct >= 0) { return direct; } const text = String(value ?? '').trim(); if (!text) { return null; } // HeroSMS occasionally returns formatted price strings such as "$0.1183". // Extract the first decimal token so those tiers can still participate in // fallback selection and pricing diagnostics. const matched = text.match(/-?\d+(?:[.,]\d+)?/); if (!matched) { return null; } const normalizedText = String(matched[0] || '').replace(',', '.'); const parsed = Number(normalizedText); if (!Number.isFinite(parsed) || parsed < 0) { return null; } return parsed; } function isProviderNoSupplyFailureMessage(message = '') { const text = String(message || '').trim(); if (!text) { return false; } return /no\s+numbers\s+available\s+across|no\s+free\s+phones|numbers?\s+not\s+found|no\s+numbers\s+within\s+(?:maxprice|price\s+range)|price\s+range\s+is\s+invalid|step\s*9:\s*(?:5sim|nexsms)\s+countries\s+are\s+empty|暂无可用号码|均无可用号码|无可用号码|价格区间|未选择国家|\bNO_NUMBERS\b/i.test(text); } function resolveNoSupplyDiagnosticsContext(state = {}, providerOrder = []) { const order = Array.isArray(providerOrder) && providerOrder.length ? providerOrder : resolvePhoneProviderOrder(state, state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER); const providerCountryCounts = Object.fromEntries( order.map((providerId) => [ providerId, resolveCountryCandidatesForProvider({ ...state, phoneSmsProvider: providerId, }, providerId).length, ]) ); const activeProvider = normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER); const activeAdapter = getPhoneSmsProviderAdapterForState(state, activeProvider); const priceRange = callPhoneSmsProviderMethod( activeAdapter, 'resolvePriceRange', [state], { minPriceLimit: null, maxPriceLimit: null, invalidRange: false, } ); const priceRangeText = callPhoneSmsProviderMethod( activeAdapter, 'formatPriceRangeText', [priceRange.minPriceLimit, priceRange.maxPriceLimit], formatPhonePriceRangeText(priceRange.minPriceLimit, priceRange.maxPriceLimit) ); const minPrice = priceRange.minPriceLimit; const maxPrice = priceRange.maxPriceLimit; const acquirePriority = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority); return { order, providerCountryCounts, heroCountryCount: providerCountryCounts[PHONE_SMS_PROVIDER_HERO] || 0, fiveSimCountryCount: providerCountryCounts[PHONE_SMS_PROVIDER_5SIM] || 0, nexSmsCountryCount: providerCountryCounts.nexsms || 0, minPrice, maxPrice, priceRangeInvalid: priceRange.invalidRange, priceRangeText, acquirePriority, }; } function isPhoneSignupIdentityState(state = {}) { const signupMethod = String(state?.resolvedSignupMethod || state?.signupMethod || '').trim().toLowerCase(); const identifierType = String(state?.accountIdentifierType || '').trim().toLowerCase(); if (signupMethod === 'phone' || identifierType === 'phone') { return true; } return Boolean( normalizeActivation(state?.signupPhoneActivation) || normalizeActivation(state?.signupPhoneCompletedActivation) ); } function formatNoSupplySuggestion(context = {}) { const suggestions = []; const minPrice = Number(context?.minPrice); const maxPrice = Number(context?.maxPrice); const hasMinPrice = Number.isFinite(minPrice) && minPrice > 0; const hasMaxPrice = Number.isFinite(maxPrice) && maxPrice > 0; if (context?.priceRangeInvalid) { suggestions.push('先修正价格区间(最低购买价不能高于价格上限)'); } else if (hasMinPrice && hasMaxPrice) { suggestions.push(`先适当放宽价格区间(当前 ${context.priceRangeText || `${minPrice}~${maxPrice}`})`); } else if (hasMinPrice) { suggestions.push(`可适当降低最低购买价(当前 ${context.priceRangeText || `${minPrice}~`})`); } else if (!hasMaxPrice) { suggestions.push('先设置价格上限(建议 >= 0.12)'); } else if (maxPrice < 0.12) { suggestions.push('先提高价格上限(当前偏低)'); } if ((context?.heroCountryCount || 0) <= 1) { suggestions.push('HeroSMS 增加国家回退'); } if ((context?.fiveSimCountryCount || 0) <= 0) { suggestions.push('5sim 至少选择 1 个国家'); } if ((context?.nexSmsCountryCount || 0) <= 0) { suggestions.push('NexSMS 至少选择 1 个国家'); } if (String(context?.acquirePriority || '') === HERO_SMS_ACQUIRE_PRIORITY_COUNTRY) { suggestions.push('可尝试切到“价格优先”'); } const unique = Array.from(new Set(suggestions)); if (!unique.length) { return '优先提高价格上限,并调整服务商/国家优先级后重试'; } return unique.slice(0, 3).join(';'); } async function resetPhoneNoSupplyFailureStreak(state = {}) { const latestState = (typeof getState === 'function') ? (await getState().catch(() => state)) : state; const current = Math.max(0, Math.floor(Number(latestState?.[PHONE_NO_SUPPLY_FAILURE_STREAK_STATE_KEY]) || 0)); if (current > 0) { await setPhoneRuntimeState({ [PHONE_NO_SUPPLY_FAILURE_STREAK_STATE_KEY]: 0, }); } } async function logNoSupplyDiagnostics(state = {}, providerOrder = [], providerErrors = []) { const allNoSupply = Array.isArray(providerErrors) && providerErrors.length > 0 && providerErrors.every((entry) => isProviderNoSupplyFailureMessage(entry)); if (!allNoSupply) { await resetPhoneNoSupplyFailureStreak(state); return false; } const latestState = (typeof getState === 'function') ? (await getState().catch(() => state)) : state; const previousStreak = Math.max( 0, Math.floor(Number(latestState?.[PHONE_NO_SUPPLY_FAILURE_STREAK_STATE_KEY]) || 0) ); const nextStreak = previousStreak + 1; await setPhoneRuntimeState({ [PHONE_NO_SUPPLY_FAILURE_STREAK_STATE_KEY]: nextStreak, }); const context = resolveNoSupplyDiagnosticsContext( latestState && typeof latestState === 'object' ? latestState : state, providerOrder ); const minPriceText = context.minPrice === null ? '未设置' : String(context.minPrice); const maxPriceText = context.maxPrice === null ? '未设置' : String(context.maxPrice); const priceRangeText = context.priceRangeText || formatPhonePriceRangeText(context.minPrice, context.maxPrice); const providerOrderText = context.order.join(' > '); const suggestion = formatNoSupplySuggestion(context); await addLog( `步骤 9 诊断:无号连续失败 ${nextStreak} 次;价格区间=${priceRangeText};最低价=${minPriceText};最高价=${maxPriceText};平台顺序=${providerOrderText};国家数 HeroSMS=${context.heroCountryCount}, 5sim=${context.fiveSimCountryCount}, NexSMS=${context.nexSmsCountryCount}。建议:${suggestion}。`, nextStreak >= 2 ? 'warn' : 'info' ); return true; } async function requestPhoneActivation(state = {}, options = {}) { state = await mergeLatestPhoneSettingsState(state, { preservePhoneSmsProvider: Boolean(options?.preservePhoneSmsProvider), }); const provider = getPhoneSmsProviderAdapterForState(state); if (!provider || typeof provider.requestActivation !== 'function') { throw new Error(`${getPhoneSmsProviderLabel(state?.phoneSmsProvider)} 接码平台模块未加载。`); } const activation = await provider.requestActivation(state, options); const normalizedActivation = normalizeActivation(activation) || activation; rememberActivationAcquiredPrice( normalizedActivation, getProviderActivationPrice(state, activation) ?? getProviderActivationPrice(state, normalizedActivation) ); return normalizedActivation; } async function reactivatePhoneActivation(state = {}, activation) { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { throw new Error('缺少可复用的手机号接码订单。'); } const scopedState = scopeStateToActivationProvider(state, normalizedActivation); const provider = getActivationProviderAdapterForState(scopedState, normalizedActivation); if (!provider || typeof provider.reuseActivation !== 'function') { throw new Error(`${getPhoneSmsProviderLabel(getActivationProviderId(normalizedActivation, state))} 当前流程不支持复用手机号订单。`); } return provider.reuseActivation(scopedState, normalizedActivation); } async function completePhoneActivation(state = {}, activation) { if (shouldSkipTerminalStatusForFreeReuse(state, activation)) { const normalizedActivation = normalizeActivation(activation); const identifier = normalizedActivation?.phoneNumber || normalizedActivation?.activationId || 'current activation'; await addLog( `步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的接码完成状态。`, 'info' ); return; } const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { return; } const scopedState = scopeStateToActivationProvider(state, normalizedActivation); const provider = getActivationProviderAdapterForState(scopedState, normalizedActivation); if (typeof provider?.finishActivation !== 'function') { throw new Error(`${getPhoneSmsProviderLabel(getActivationProviderId(normalizedActivation, state))} 不支持完成接码订单。`); } await provider.finishActivation(scopedState, normalizedActivation); } async function cancelPhoneActivation(state = {}, activation) { try { const normalizedActivation = normalizeActivation(activation); if (shouldSkipTerminalStatusForFreeReuse(state, activation)) { const identifier = normalizedActivation?.phoneNumber || normalizedActivation?.activationId || 'current activation'; await addLog( `步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的接码取消状态。`, 'info' ); return; } if (!normalizedActivation) { return; } const scopedState = scopeStateToActivationProvider(state, normalizedActivation); const provider = getActivationProviderAdapterForState(scopedState, normalizedActivation); if (typeof provider?.cancelActivation === 'function') { await provider.cancelActivation(scopedState, normalizedActivation); } } catch (_) { // Best-effort cleanup. } } async function retireFreeReusableActivation(reason = '') { const suffix = reason ? ` ${reason}` : ''; await addLog(`步骤 9:已清除白嫖复用手机号记录。${suffix}`, 'warn'); await clearFreeReusableActivation(); } async function discardPhoneActivationFromReuse(reason = '', activation = null, state = {}) { const rejectedPhoneNumber = String(activation?.phoneNumber || '').trim(); if (!rejectedPhoneNumber) { return; } const updates = {}; const currentActivation = normalizeActivation(state[PHONE_ACTIVATION_STATE_KEY]); if (phoneNumbersMatch(currentActivation?.phoneNumber, rejectedPhoneNumber)) { updates[PHONE_ACTIVATION_STATE_KEY] = null; updates[PHONE_VERIFICATION_CODE_STATE_KEY] = ''; } const reusableActivation = normalizeActivation(state[REUSABLE_PHONE_ACTIVATION_STATE_KEY]); if (phoneNumbersMatch(reusableActivation?.phoneNumber, rejectedPhoneNumber)) { updates[REUSABLE_PHONE_ACTIVATION_STATE_KEY] = null; } const reusablePool = readReusableActivationPoolFromState(state); const nextReusablePool = reusablePool.filter((entry) => ( !phoneNumbersMatch(entry?.phoneNumber, rejectedPhoneNumber) )); if (nextReusablePool.length !== reusablePool.length) { updates[REUSABLE_PHONE_ACTIVATION_POOL_STATE_KEY] = nextReusablePool; } const freeReusableActivation = normalizeFreeReusablePhoneActivation(state[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY]); if (phoneNumbersMatch(freeReusableActivation?.phoneNumber, rejectedPhoneNumber)) { updates[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY] = null; } if (Object.keys(updates).length) { await setPhoneRuntimeState(updates); await addLog( `步骤 9:已从复用记录中移除手机号 ${rejectedPhoneNumber}。${reason || '目标站拒绝该号码。'}`, 'warn' ); } } function isFreeAutoReuseActivation(activation) { return normalizeActivation(activation)?.source === 'free-auto-reuse'; } function isRetainedReuseActivation(activation) { return normalizeActivation(activation)?.source === '5sim-retained-reuse'; } function shouldRetireFreeReusableActivationOnFailure(state, activation) { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { return false; } if (normalizedActivation.phoneCodeReceived) { return false; } if (isFreeAutoReuseActivation(normalizedActivation)) { return true; } const savedFreeActivation = normalizeFreeReusablePhoneActivation( state?.[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY] ); return Boolean( savedFreeActivation && ( isSameActivation(savedFreeActivation, normalizedActivation) || phoneNumbersMatch(savedFreeActivation.phoneNumber, normalizedActivation.phoneNumber) ) ); } async function banPhoneActivation(state = {}, activation) { try { if (shouldSkipTerminalStatusForFreeReuse(state, activation)) { const normalizedActivation = normalizeActivation(activation); const identifier = normalizedActivation?.phoneNumber || normalizedActivation?.activationId || 'current activation'; await addLog( `步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的接码封禁状态。`, 'info' ); return; } const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { return; } const scopedState = scopeStateToActivationProvider(state, normalizedActivation); const provider = getActivationProviderAdapterForState(scopedState, normalizedActivation); if (typeof provider?.banActivation === 'function') { await provider.banActivation(scopedState, normalizedActivation); } } catch (_) { // Best-effort cleanup. } } async function requestAdditionalPhoneSms(state = {}, activation) { try { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { return; } const scopedState = scopeStateToActivationProvider(state, normalizedActivation); const provider = getActivationProviderAdapterForState(scopedState, normalizedActivation); if (typeof provider?.requestAdditionalSms === 'function') { await provider.requestAdditionalSms(scopedState, normalizedActivation); } } catch (_) { // Best-effort request only. } } async function prepareFreeReusablePhoneActivation(state = {}, activation) { const normalizedActivation = normalizeFreeReusablePhoneActivation(activation); if (!normalizedActivation) { return { ok: false, reason: 'missing_free_reusable_activation', message: '免费复用手机号激活记录缺失。', }; } if (!String(normalizedActivation.activationId || '').trim()) { return { ok: false, reason: 'missing_activation_id', message: '已保存的免费复用手机号缺少激活 ID,无法自动重新激活。', }; } const scopedState = scopeStateToActivationProvider(state, normalizedActivation); const provider = getActivationProviderAdapterForState(scopedState, normalizedActivation); if (provider && typeof provider.prepareActivationForReuse === 'function') { const prepared = await provider.prepareActivationForReuse( scopedState, normalizedActivation, { timeoutMs: FREE_PHONE_REUSE_PREPARE_TIMEOUT_MS, intervalMs: FREE_PHONE_REUSE_PREPARE_INTERVAL_MS, maxRounds: FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS, } ); if (prepared?.ok && prepared.activation) { return { ...prepared, activation: { ...prepared.activation, source: 'free-auto-reuse', }, }; } return prepared; } return { ok: false, reason: 'prepare_unsupported', message: `${getPhoneSmsProviderLabel(normalizedActivation.provider)} 不支持自动白嫖复用准备。`, }; } async function pollPhoneActivationCode(state = {}, activation, options = {}) { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { throw new Error('缺少手机号接码订单。'); } const scopedState = scopeStateToActivationProvider(state, normalizedActivation); const configuredTimeoutMs = Math.max(1000, Number(options.timeoutMs) || 0); const timeoutMs = configuredTimeoutMs || ( typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs( DEFAULT_PHONE_POLL_TIMEOUT_MS, { step: 9, actionLabel: options.actionLabel || 'poll phone verification code' } ) : DEFAULT_PHONE_POLL_TIMEOUT_MS ); const provider = getActivationProviderAdapterForState(scopedState, normalizedActivation); if (!provider || typeof provider.pollActivationCode !== 'function') { throw new Error(`${getPhoneSmsProviderLabel(getActivationProviderId(normalizedActivation, state))} 不支持查询短信验证码。`); } return provider.pollActivationCode(scopedState, normalizedActivation, { ...options, timeoutMs, }); } async function readPhonePageState(tabId, timeoutMs = 10000) { const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; const deadlineMs = Math.max(1, Math.floor(Number(timeoutMs) || 0)); let timeoutId = null; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(`步骤 ${visibleStep}:等待认证页状态检查超时。`)); }, deadlineMs); }); const readPromise = (async () => { await ensureStep8SignupPageReady(tabId, { timeoutMs: deadlineMs, logMessage: '步骤 9:等待认证页脚本恢复后继续手机号验证。', visibleStep, logStepKey: 'phone-verification', }); const result = await sendToContentScriptResilient('openai-auth', { type: 'STEP8_GET_STATE', source: 'background', payload: { visibleStep }, }, { timeoutMs: deadlineMs, responseTimeoutMs: deadlineMs, retryDelayMs: 600, logMessage: '步骤 9:认证页正在切换,等待后重新检查手机号验证状态...', logStep: visibleStep, logStepKey: 'phone-verification', }); if (result?.error) { throw new Error(result.error); } return result || {}; })(); try { return await Promise.race([readPromise, timeoutPromise]); } finally { if (timeoutId) { clearTimeout(timeoutId); } } } function resolveCountryCandidatesForProvider(state = {}, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) { const normalizedProviderId = normalizePhoneSmsProvider(providerId); try { const provider = getPhoneSmsProviderAdapterForState(state, normalizedProviderId); if (provider && typeof provider.resolveCountryCandidates === 'function') { return provider.resolveCountryCandidates(state); } } catch (_) { // Fall back to local normalization for diagnostics and legacy tests. } return resolveCountryCandidates(state); } function resolveCountryConfigFromActivation(activation, fallbackState = {}) { const providerId = getActivationProviderId(activation, fallbackState); try { const scopedState = scopeStateToActivationProvider(fallbackState, { provider: providerId }); const provider = getPhoneSmsProviderAdapterForState(scopedState, providerId); const resolved = callPhoneSmsProviderMethod(provider, 'resolveActivationCountry', [activation, scopedState], null); if (resolved && (resolved.id || resolved.label)) { return resolved; } } catch (_) { // Fall back below for isolated legacy tests. } const candidates = resolveCountryCandidatesForProvider(fallbackState, providerId); return candidates[0] || resolveCountryConfig(fallbackState); } async function submitPhoneNumber(tabId, phoneNumber, activation = null) { const state = await getState(); const countryConfig = resolveCountryConfigFromActivation(activation, state); const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(30000, { step: visibleStep, actionLabel: '提交添加手机号' }) : 30000; const result = await sendToContentScriptResilient('openai-auth', { type: 'SUBMIT_PHONE_NUMBER', source: 'background', payload: { phoneNumber, countryId: countryConfig.id, countryLabel: countryConfig.label, }, }, { timeoutMs, responseTimeoutMs: timeoutMs, retryDelayMs: 600, logMessage: '步骤 9:等待添加手机号页面就绪...', logStep: visibleStep, logStepKey: 'phone-verification', }); if (result?.error) { throw new Error(result.error); } return result || {}; } async function submitPhoneVerificationCode(tabId, code) { const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; const signupProfile = ( typeof generateRandomName === 'function' && typeof generateRandomBirthday === 'function' ) ? (() => { const name = generateRandomName(); const birthday = generateRandomBirthday(); if (!name?.firstName || !name?.lastName || !birthday) { return null; } return { firstName: name.firstName, lastName: name.lastName, year: birthday.year, month: birthday.month, day: birthday.day, }; })() : null; const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(45000, { step: visibleStep, actionLabel: '提交手机验证码' }) : 45000; const result = await sendToContentScriptResilient('openai-auth', { type: 'SUBMIT_PHONE_VERIFICATION_CODE', source: 'background', payload: { code, ...(signupProfile ? { signupProfile } : {}), }, }, { timeoutMs, responseTimeoutMs: timeoutMs, retryDelayMs: 600, logMessage: '步骤 9:等待手机验证码页面就绪后填写短信验证码...', logStep: visibleStep, logStepKey: 'phone-verification', }); if (result?.error) { if (isPhoneNumberUsedError(result.error)) { return { invalidCode: true, errorText: String(result.error || ''), }; } throw new Error(result.error); } return result || {}; } async function resendPhoneVerificationCode(tabId, options = {}) { const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(65000, { step: visibleStep, actionLabel: 'resend phone verification code' }) : 65000; const result = await sendToContentScriptResilient('openai-auth', { type: 'RESEND_PHONE_VERIFICATION_CODE', source: 'background', payload: options || {}, }, { timeoutMs, responseTimeoutMs: timeoutMs, retryDelayMs: 600, logMessage: '步骤 9:等待手机验证码重发按钮出现...', logStep: visibleStep, logStepKey: 'phone-verification', }); if (result?.error) { throw new Error(result.error); } 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('openai-auth', { 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('openai-auth', { 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' ? await getOAuthFlowStepTimeoutMs(30000, { step: visibleStep, actionLabel: 'return to add-phone page' }) : 30000; const result = await sendToContentScriptResilient('openai-auth', { type: 'RETURN_TO_ADD_PHONE', source: 'background', payload: {}, }, { timeoutMs, responseTimeoutMs: timeoutMs, retryDelayMs: 600, logMessage: '步骤 9:返回添加手机号页面以更换号码...', logStep: visibleStep, logStepKey: 'phone-verification', }); if (result?.error) { throw new Error(result.error); } return result || {}; } async function checkPhoneResendPageError(tabId, state = {}) { if (!usePageProbeForPhoneResend(state)) { return { hasError: false, reason: '', message: '', }; } const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; try { const result = await sendToContentScriptResilient('openai-auth', { type: 'CHECK_PHONE_RESEND_ERROR', source: 'background', payload: { visibleStep }, }, { timeoutMs: 3000, responseTimeoutMs: 3000, retryDelayMs: 500, logStep: visibleStep, logStepKey: 'phone-verification', }); if (result?.error) { throw new Error(result.error); } return result || {}; } catch (error) { if (isStopRequestedError(error)) { throw error; } if (isPhoneResendBannedNumberError(error)) { return { hasError: true, reason: 'resend_phone_banned', message: error.message, }; } if (isPhoneResendThrottledError(error)) { return { hasError: true, reason: 'resend_throttled', message: error.message, }; } if (isPhoneResendServerError(error)) { return { hasError: true, reason: 'resend_server_error', message: error.message, }; } if (isPhoneMaxUsageExceededFlowError(error)) { return { hasError: true, reason: 'phone_max_usage_exceeded', message: error.message, }; } await addLog(`步骤 9:检查手机重发错误时遇到暂时性问题,已忽略。${error.message}`, 'warn'); return { hasError: false, reason: '', message: '', }; } } function usePageProbeForPhoneResend(state = {}) { const provider = getPhoneSmsProviderAdapterForState(state); return callPhoneSmsProviderCapability( provider, 'shouldProbePageResend', [state], 'supportsPageResendProbe', false ); } async function persistCurrentActivation(activation) { const normalizedActivation = normalizeActivation(activation); const updates = { [PHONE_ACTIVATION_STATE_KEY]: normalizedActivation || null, [PHONE_VERIFICATION_CODE_STATE_KEY]: '', }; if (!normalizedActivation) { updates[PHONE_RUNTIME_COUNTDOWN_ENDS_AT_KEY] = 0; updates[PHONE_RUNTIME_COUNTDOWN_WINDOW_INDEX_KEY] = 0; updates[PHONE_RUNTIME_COUNTDOWN_WINDOW_TOTAL_KEY] = 0; } await setPhoneRuntimeState(updates); } async function persistReusableActivation(activation) { await setPhoneRuntimeState({ [REUSABLE_PHONE_ACTIVATION_STATE_KEY]: normalizeActivation(activation) || null, }); } function readReusableActivationPoolFromState(state = {}) { return normalizeActivationPool(state?.[REUSABLE_PHONE_ACTIVATION_POOL_STATE_KEY]); } async function persistReusableActivationPool(pool = []) { await setPhoneRuntimeState({ [REUSABLE_PHONE_ACTIVATION_POOL_STATE_KEY]: normalizeActivationPool(pool), }); } async function upsertReusableActivationPool(activation, options = {}) { const normalized = normalizeActivation(activation); if (!normalized) { return []; } const state = options?.state || await getState(); const existingPool = readReusableActivationPoolFromState(state); const filtered = existingPool.filter((entry) => !isSameActivation(entry, normalized)); const nextPool = [normalized, ...filtered].slice(0, MAX_PHONE_REUSABLE_POOL); await persistReusableActivationPool(nextPool); return nextPool; } async function removeReusableActivationFromPool(activation, options = {}) { const normalized = normalizeActivation(activation); if (!normalized) { return []; } const state = options?.state || await getState(); const existingPool = readReusableActivationPoolFromState(state); const nextPool = existingPool.filter((entry) => !isSameActivation(entry, normalized)); if (nextPool.length === existingPool.length) { return existingPool; } await persistReusableActivationPool(nextPool); return nextPool; } async function clearCurrentActivation() { await persistCurrentActivation(null); } async function clearReusableActivation() { await persistReusableActivation(null); } async function handoffFreeReusablePhone(tabId, state = {}) { if (isPhoneSignupIdentityState(state)) { return null; } if (!normalizeFreePhoneReuseEnabled(state?.freePhoneReuseEnabled)) { return null; } const freeReusableActivation = normalizeFreeReusablePhoneActivation( state[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY] ); if (!freeReusableActivation) { return null; } if (freeReusableActivation.successfulUses >= freeReusableActivation.maxUses) { await retireFreeReusableActivation( `保存的手机号 ${freeReusableActivation.phoneNumber} 已达到 ${freeReusableActivation.successfulUses}/${freeReusableActivation.maxUses} 次。` ); return null; } const canPrepareAutomaticFreeReuse = normalizeFreePhoneReuseAutoEnabled(state) && !freeReusableActivation.manualOnly && Boolean(String(freeReusableActivation.activationId || '').trim()); if (canPrepareAutomaticFreeReuse) { await addLog( `步骤 9:准备自动白嫖复用已保存手机号 ${freeReusableActivation.phoneNumber}(${freeReusableActivation.successfulUses + 1}/${freeReusableActivation.maxUses})。`, 'info' ); const prepared = await prepareFreeReusablePhoneActivation(state, freeReusableActivation); if (!prepared.ok) { const reason = prepared.message || prepared.reason || 'unknown error'; const stopMessage = `自动白嫖复用准备失败:${freeReusableActivation.phoneNumber} 未确认进入等待短信状态,本次不购买新 HeroSMS 号码。原因:${reason}`; await addLog( `步骤 9:自动白嫖复用准备失败,停止本次接码且不购买新 HeroSMS 号码。${reason}`, 'error' ); if (prepared.reason === 'activation_cancelled') { await retireFreeReusableActivation( `自动白嫖复用号码 ${freeReusableActivation.phoneNumber} 已被 HeroSMS 取消。` ); } if (typeof requestStop === 'function') { await requestStop({ logMessage: stopMessage }); } throw new Error(`${PHONE_AUTO_FREE_REUSE_PREPARE_ERROR_PREFIX}${stopMessage}`); } await persistCurrentActivation(prepared.activation); return prepared.activation; } const fillResult = await submitPhoneNumber(tabId, freeReusableActivation.phoneNumber, freeReusableActivation); await clearCurrentActivation(); const message = `开始手动复用手机 ${freeReusableActivation.phoneNumber},请到 SMS 上刷新。`; await addLog(`步骤 9:${message}`, 'warn'); if (typeof requestStop === 'function') { await requestStop({ logMessage: message }); } const handoffError = new Error(`${PHONE_MANUAL_FREE_REUSE_ERROR_PREFIX}${message}`); handoffError.result = { manualFreePhoneReuse: true, phoneNumber: freeReusableActivation.phoneNumber, fillResult, }; throw handoffError; } async function setPhoneRuntimeCountdown(activation, waitSeconds, windowIndex, windowTotal) { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { return; } const safeWaitSeconds = Math.max(0, Math.floor(Number(waitSeconds) || 0)); await setPhoneRuntimeState({ [PHONE_ACTIVATION_STATE_KEY]: normalizedActivation, [PHONE_RUNTIME_COUNTDOWN_ENDS_AT_KEY]: Date.now() + safeWaitSeconds * 1000, [PHONE_RUNTIME_COUNTDOWN_WINDOW_INDEX_KEY]: Math.max(0, Math.floor(Number(windowIndex) || 0)), [PHONE_RUNTIME_COUNTDOWN_WINDOW_TOTAL_KEY]: Math.max(0, Math.floor(Number(windowTotal) || 0)), }); } async function clearPhoneRuntimeCountdown() { await setPhoneRuntimeState({ [PHONE_RUNTIME_COUNTDOWN_ENDS_AT_KEY]: 0, [PHONE_RUNTIME_COUNTDOWN_WINDOW_INDEX_KEY]: 0, [PHONE_RUNTIME_COUNTDOWN_WINDOW_TOTAL_KEY]: 0, }); } 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 = {}) { state = await mergeLatestPhoneSettingsState(state); const provider = normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER); const providerOrder = resolvePhoneProviderOrder(state, provider); const activeProviderAdapter = getPhoneSmsProviderAdapterForState(state, provider); const countryCandidates = resolveCountryCandidatesForProvider(state, provider); const providerExpectsConfiguredCountries = readPhoneSmsProviderCapability( activeProviderAdapter, 'requiresCountrySelection', true ); if (providerExpectsConfiguredCountries && !countryCandidates.length) { throw new Error(`步骤 ${getActivePhoneVerificationVisibleStep()}:${getPhoneSmsProviderLabel(provider)} 未选择国家,请先在接码设置中至少选择 1 个国家。`); } const normalizeCountryKey = (value) => getProviderCountryKey(activeProviderAdapter, value); const normalizeActivationCountryKey = (activationRecord) => ( getProviderActivationCountryKey(state, activationRecord) ); const blockedCountryIds = new Set( (Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : []) .map((value) => normalizeCountryKey(value)) .filter(Boolean) ); const allowedCountryIds = new Set( countryCandidates .map((entry) => normalizeCountryKey(entry.id || entry.code)) .filter((id) => Boolean(id && !blockedCountryIds.has(id))) ); const preferredCountryLabel = countryCandidates[0]?.label || ''; const resolveCountryLabelById = (countryId) => { const normalizedCountryKey = normalizeCountryKey(countryId); return countryCandidates.find((entry) => normalizeCountryKey(entry.id || entry.code) === normalizedCountryKey)?.label || getProviderCountryLabel(state, provider, countryId) || preferredCountryLabel; }; const scopedStateForProvider = (providerName) => ({ ...state, phoneSmsProvider: normalizePhoneSmsProvider(providerName), }); const canUseSavedActivationForCurrentFlow = !isPhoneSignupIdentityState(state); const preferredActivation = normalizeActivation(state[PREFERRED_PHONE_ACTIVATION_STATE_KEY]); let failedPreferredActivation = null; const canTryPreferredActivation = ( canUseSavedActivationForCurrentFlow && !Boolean(options?.skipPreferredActivation) && preferredActivation && preferredActivation.provider === provider && !blockedCountryIds.has(normalizeActivationCountryKey(preferredActivation)) && ( !allowedCountryIds.size || allowedCountryIds.has(normalizeActivationCountryKey(preferredActivation)) ) && preferredActivation.successfulUses < preferredActivation.maxUses ); if (canTryPreferredActivation) { try { const reactivated = await reactivatePhoneActivation(state, preferredActivation); await addLog( `步骤 9:优先复用手动选择号码 ${reactivated.phoneNumber}${reactivated.countryId ? `(${resolveCountryLabelById(reactivated.countryId)})` : ''}。`, 'info' ); await resetPhoneNoSupplyFailureStreak(state); return reactivated; } catch (error) { failedPreferredActivation = preferredActivation; await removeReusableActivationFromPool(preferredActivation, { state }).catch(() => {}); await addLog( `步骤 9:手动选择号码 ${preferredActivation.phoneNumber} 不可用,将改为获取新号码。${error.message}`, 'warn' ); } } const reuseEnabled = isPhoneSmsReuseEnabled(state); const reusableActivation = normalizeActivation(state[REUSABLE_PHONE_ACTIVATION_STATE_KEY]); const reusableActivationPool = readReusableActivationPoolFromState(state); const reusableCandidates = []; const seenReusableKeys = new Set(); const pushReusableCandidate = (candidate) => { const normalizedCandidate = normalizeActivation(candidate); if (!normalizedCandidate) { return; } const candidateKey = buildActivationIdentityKey(normalizedCandidate); if (!candidateKey || seenReusableKeys.has(candidateKey)) { return; } seenReusableKeys.add(candidateKey); reusableCandidates.push(normalizedCandidate); }; pushReusableCandidate(reusableActivation); reusableActivationPool.forEach((candidate) => pushReusableCandidate(candidate)); if (reuseEnabled) { for (const candidateActivation of reusableCandidates) { if (candidateActivation.provider !== provider) { continue; } if (isSameActivation(candidateActivation, failedPreferredActivation)) { continue; } if (candidateActivation.successfulUses >= candidateActivation.maxUses) { continue; } const candidateCountryKey = normalizeActivationCountryKey(candidateActivation); if (blockedCountryIds.has(candidateCountryKey)) { continue; } if (allowedCountryIds.size && !allowedCountryIds.has(candidateCountryKey)) { continue; } try { const reactivated = await reactivatePhoneActivation(state, candidateActivation); await addLog( `步骤 9:复用 ${resolveCountryLabelById(reactivated.countryId)} 号码 ${reactivated.phoneNumber}(第 ${reactivated.successfulUses + 1}/${reactivated.maxUses} 次)。`, 'info' ); await resetPhoneNoSupplyFailureStreak(state); return reactivated; } catch (error) { await addLog(`步骤 9:复用号码 ${candidateActivation.phoneNumber} 失败,将改为获取新号码。${error.message}`, 'warn'); await removeReusableActivationFromPool(candidateActivation, { state }).catch(() => {}); if (isSameActivation(reusableActivation, candidateActivation)) { await clearReusableActivation(); } } } } let lastProviderError = null; const providerErrors = []; const skippedFallbackProviders = []; for (const providerCandidate of providerOrder) { const useBlockedCountryIds = providerCandidate === provider ? Array.from(blockedCountryIds) : []; const useCountryPriceFloorByCountryId = ( providerCandidate === provider && options?.countryPriceFloorByCountryId && typeof options.countryPriceFloorByCountryId === 'object' ) ? options.countryPriceFloorByCountryId : {}; try { const activation = await requestPhoneActivation( scopedStateForProvider(providerCandidate), { blockedCountryIds: useBlockedCountryIds, countryPriceFloorByCountryId: useCountryPriceFloorByCountryId, preservePhoneSmsProvider: true, } ); const providerLabel = getPhoneSmsProviderLabel(providerCandidate); const providerCountryLabel = providerCandidate === provider ? resolveCountryLabelById(activation.countryId) : String(activation?.countryLabel || activation?.countryId || '').trim(); if (providerCandidate !== provider) { await addLog( `步骤 9:主接码平台 ${getPhoneSmsProviderLabel(provider)} 暂无可用号码,已回退到 ${providerLabel}${providerCountryLabel ? ` / ${providerCountryLabel}` : ''}。`, 'warn' ); } await addLog( `步骤 9:已从 ${providerLabel}${providerCountryLabel ? ` / ${providerCountryLabel}` : ''} 获取号码 ${activation.phoneNumber}。`, 'info' ); await resetPhoneNoSupplyFailureStreak(state); return activation; } catch (error) { if (isStopRequestedError(error)) { throw error; } const providerErrorMessage = String(error?.message || error || 'unknown error'); const providerLabel = getPhoneSmsProviderLabel(providerCandidate); if ( providerCandidate !== provider && /(?:step|步骤)\s*9\s*[::]\s*(?:5sim|nexsms).*(?:countries\s+are\s+empty|未选择国家)/i.test(providerErrorMessage) ) { skippedFallbackProviders.push(`${providerLabel}:未选择国家`); await addLog( `步骤 9:跳过回退接码平台 ${providerLabel},因为接码设置中未选择国家。`, 'warn' ); continue; } lastProviderError = error; providerErrors.push(`${providerLabel}:${formatProviderAcquireFailure(providerCandidate, providerErrorMessage)}`); } } if (providerErrors.length) { await logNoSupplyDiagnostics(state, providerOrder, providerErrors); const skippedSuffix = skippedFallbackProviders.length ? `;已跳过回退平台:${skippedFallbackProviders.join(';')}` : ''; throw new Error(`步骤 ${getActivePhoneVerificationVisibleStep()}:所有接码平台候选均未获取到手机号。${providerErrors.join(';')}${skippedSuffix}`); } throw lastProviderError || new Error(`步骤 ${getActivePhoneVerificationVisibleStep()}:获取手机号订单失败。`); } 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:接码平台返回的手机号订单无效。'); } const countryConfig = resolveCountryConfigFromActivation(normalizedActivation, state); const signupActivation = normalizeActivation({ ...normalizedActivation, countryId: countryConfig?.id ?? normalizedActivation.countryId, countryLabel: normalizedActivation.countryLabel || countryConfig?.label || '', }) || normalizedActivation; await persistSignupPhoneRuntimeState({ signupPhoneNumber: signupActivation.phoneNumber, signupPhoneActivation: signupActivation, signupPhoneVerificationRequestedAt: null, signupPhoneVerificationPurpose: 'signup', accountIdentifierType: 'phone', accountIdentifier: signupActivation.phoneNumber, }); return signupActivation; }); } async function markActivationReusableAfterSuccess(state, activation) { const normalizedActivation = normalizeActivation(activation); if (isPhoneSignupIdentityState(state)) { return; } if (!isPhoneSmsReuseEnabled(state)) { await clearReusableActivation(); return; } if (!normalizedActivation) { await clearReusableActivation(); return; } const provider = getPhoneSmsProviderAdapterForActivation(state, normalizedActivation); if (!callPhoneSmsProviderCapability( provider, 'canPersistReusableActivation', [state, normalizedActivation], 'supportsReusableActivation', false )) { await clearReusableActivation(); return; } const successfulUses = normalizedActivation.successfulUses + 1; const nextReusableActivation = { ...normalizedActivation, successfulUses, }; delete nextReusableActivation.phoneCodeReceived; delete nextReusableActivation.phoneCodeReceivedAt; delete nextReusableActivation.ignoredPhoneCodeKeys; await upsertReusableActivationPool(nextReusableActivation, { state }); if (!normalizePhoneSmsReuseEnabled(state)) { await clearReusableActivation(); return; } if (successfulUses >= normalizedActivation.maxUses) { await clearReusableActivation(); await removeReusableActivationFromPool(nextReusableActivation, { state }); return; } await persistReusableActivation(nextReusableActivation); } function shouldPreserveActivationForFreeReuse(state, activation) { if (isPhoneSignupIdentityState(state)) { return false; } if (!normalizeFreePhoneReuseEnabled(state?.freePhoneReuseEnabled)) { return false; } const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { return false; } const activationSnapshot = { ...normalizedActivation, ...(activation && typeof activation === 'object' && activation.phoneCodeReceived ? { phoneCodeReceived: true } : {}), ...(activation && typeof activation === 'object' && activation.phoneCodeReceivedAt ? { phoneCodeReceivedAt: activation.phoneCodeReceivedAt } : {}), }; const provider = getPhoneSmsProviderAdapterForActivation(state, activationSnapshot); return callPhoneSmsProviderCapability( provider, 'canPreserveActivationForFreeReuse', [state, activationSnapshot], 'supportsFreeReusePreservation', false ); } function shouldSkipTerminalStatusForFreeReuse(state, activation) { if (isPhoneSignupIdentityState(state)) { return false; } const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { return false; } if (isFreeAutoReuseActivation(normalizedActivation)) { return true; } if (isRetainedReuseActivation(normalizedActivation)) { return true; } if (normalizedActivation.source === 'free-manual-reuse') { return true; } const savedFreeActivation = normalizeFreeReusablePhoneActivation( state?.[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY] ); if ( savedFreeActivation && ( isSameActivation(savedFreeActivation, normalizedActivation) || phoneNumbersMatch(savedFreeActivation.phoneNumber, normalizedActivation.phoneNumber) ) ) { return true; } return shouldPreserveActivationForFreeReuse(state, normalizedActivation); } async function markFreeReusableActivationAfterCode(state, activation) { const latestState = { ...(state || {}), ...(typeof getState === 'function' ? await getState() : {}), }; if (isPhoneSignupIdentityState(latestState)) { return; } if (!normalizeFreePhoneReuseEnabled(latestState?.freePhoneReuseEnabled)) { return; } if (normalizeFreeReusablePhoneActivation(latestState[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY])) { return; } const normalizedActivation = normalizeActivation(activation); const activationSnapshot = normalizedActivation ? { ...normalizedActivation, ...(activation && typeof activation === 'object' && activation.phoneCodeReceived ? { phoneCodeReceived: true } : {}), ...(activation && typeof activation === 'object' && activation.phoneCodeReceivedAt ? { phoneCodeReceivedAt: activation.phoneCodeReceivedAt } : {}), } : null; const provider = activationSnapshot ? getPhoneSmsProviderAdapterForActivation(latestState, activationSnapshot) : null; if ( !normalizedActivation || !callPhoneSmsProviderCapability( provider, 'canPreserveActivationForFreeReuse', [latestState, activationSnapshot], 'supportsFreeReusePreservation', false ) || !activationSnapshot.phoneCodeReceived || isFreeAutoReuseActivation(normalizedActivation) ) { return; } const countryConfig = resolveCountryConfigFromActivation(normalizedActivation, latestState); await persistFreeReusableActivation({ ...normalizedActivation, source: 'free-manual-reuse', countryId: countryConfig.id, ...(countryConfig.label ? { countryLabel: countryConfig.label } : {}), recordedAt: Date.now(), }); await addLog( `步骤 9:收到有效短信后已保存白嫖复用手机号 ${normalizedActivation.phoneNumber}。`, 'info' ); } async function markFreeReusableActivationAfterAutoSuccess(state, activation) { const normalizedActivation = normalizeFreeReusablePhoneActivation(activation); if (!normalizedActivation || !isFreeAutoReuseActivation(activation)) { return; } const latestState = { ...(state || {}), ...(typeof getState === 'function' ? await getState() : {}), }; if (isPhoneSignupIdentityState(latestState)) { return; } const savedActivation = normalizeFreeReusablePhoneActivation( latestState[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY] ); if (!savedActivation || savedActivation.activationId !== normalizedActivation.activationId) { return; } const successfulUses = savedActivation.successfulUses + 1; const maxUses = Math.max(1, Math.floor(Number(savedActivation.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)); if (successfulUses >= maxUses) { await clearFreeReusableActivation(); await addLog( `步骤 9:自动白嫖复用手机号 ${savedActivation.phoneNumber} 已达到 ${successfulUses}/${maxUses} 次,已清除本地记录。`, 'info' ); return; } await persistFreeReusableActivation({ ...savedActivation, source: 'free-manual-reuse', successfulUses, maxUses, }); await addLog( `步骤 9:自动白嫖复用手机号 ${savedActivation.phoneNumber} 成功(${successfulUses}/${maxUses}),保留供后续注册使用。`, 'info' ); } async function markFreeReusableActivationAfterInitialSuccess(state, activation) { const normalizedActivation = normalizeActivation(activation); const activationSnapshot = normalizedActivation ? { ...normalizedActivation, ...(activation && typeof activation === 'object' && activation.phoneCodeReceived ? { phoneCodeReceived: true } : {}), ...(activation && typeof activation === 'object' && activation.phoneCodeReceivedAt ? { phoneCodeReceivedAt: activation.phoneCodeReceivedAt } : {}), } : null; const provider = activationSnapshot ? getPhoneSmsProviderAdapterForActivation(state, activationSnapshot) : null; if ( !normalizedActivation || !callPhoneSmsProviderCapability( provider, 'canPreserveActivationForFreeReuse', [state, activationSnapshot], 'supportsFreeReusePreservation', false ) || isFreeAutoReuseActivation(normalizedActivation) ) { return; } const latestState = { ...(state || {}), ...(typeof getState === 'function' ? await getState() : {}), }; if (isPhoneSignupIdentityState(latestState)) { return; } const savedActivation = normalizeFreeReusablePhoneActivation( latestState[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY] ); if ( !savedActivation || !( isSameActivation(savedActivation, normalizedActivation) || phoneNumbersMatch(savedActivation.phoneNumber, normalizedActivation.phoneNumber) ) ) { return; } const maxUses = Math.max(1, Math.floor(Number(savedActivation.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)); const successfulUses = Math.min(maxUses, normalizeUseCount(savedActivation.successfulUses) + 1); if (successfulUses >= maxUses) { await clearFreeReusableActivation(); await addLog( `步骤 9:白嫖复用手机号 ${savedActivation.phoneNumber} 已达到 ${successfulUses}/${maxUses} 次,已清除本地记录。`, 'info' ); return; } if (successfulUses !== savedActivation.successfulUses || savedActivation.maxUses !== maxUses) { await persistFreeReusableActivation({ ...savedActivation, source: 'free-manual-reuse', successfulUses, maxUses, }); } } async function waitForPhoneCodeOrRotateNumber(tabId, state, activation) { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { throw new Error('缺少手机号接码订单。'); } const provider = getPhoneSmsProviderAdapterForActivation(state, normalizedActivation); const providerLabel = getPhoneSmsProviderLabel(normalizedActivation.provider); const usePageResend = callPhoneSmsProviderCapability( provider, 'shouldUsePageResend', [state, normalizedActivation], 'supportsPageResend', true ); const waitSeconds = normalizePhoneCodeWaitSeconds(state?.phoneCodeWaitSeconds); const timeoutWindows = normalizePhoneCodeTimeoutWindows(state?.phoneCodeTimeoutWindows); const pollIntervalSeconds = normalizePhoneCodePollIntervalSeconds(state?.phoneCodePollIntervalSeconds); const pollMaxRounds = resolvePhoneCodePollMaxRoundsForWindow( waitSeconds, pollIntervalSeconds, state?.phoneCodePollMaxRounds ); let resendTriggeredForCurrentNumber = false; for (let windowIndex = 1; windowIndex <= timeoutWindows; windowIndex += 1) { await setPhoneRuntimeCountdown(normalizedActivation, waitSeconds, windowIndex, timeoutWindows); await addLog( `步骤 9:等待号码 ${normalizedActivation.phoneNumber} 接收短信(等待窗口 ${windowIndex}/${timeoutWindows},最长 ${waitSeconds} 秒,每 ${pollIntervalSeconds} 秒轮询一次,最多 ${pollMaxRounds} 次轮询)。`, 'info' ); try { const code = await pollPhoneActivationCode(state, normalizedActivation, { actionLabel: windowIndex === 1 ? '从接码平台轮询手机验证码' : '从接码平台轮询重发后的手机验证码', timeoutMs: waitSeconds * 1000, intervalMs: pollIntervalSeconds * 1000, maxRounds: pollMaxRounds, onStatus: async ({ elapsedMs, pollCount, statusText }) => { if (/^STATUS_(WAIT_CODE|WAIT_RETRY|WAIT_RESEND)(?::.+)?$/i.test(String(statusText || '').trim())) { const pageError = await checkPhoneResendPageError(tabId, state); if (pageError?.reason === 'resend_phone_banned') { throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${pageError.message || 'OpenAI 无法向此手机号发送短信。'}`); } if (pageError?.reason === 'phone_max_usage_exceeded') { throw buildPhoneMaxUsageExceededError(pageError.message); } if (pageError?.reason === 'resend_server_error') { throw buildPhoneResendServerError(pageError.message); } if (pageError?.reason === 'resend_throttled') { if (shouldTreatResendThrottledAsBanned(state)) { throw buildHighRiskResendThrottledError(pageError.message); } await addLog( `步骤 9:检测到号码 ${normalizedActivation.phoneNumber} 重发限流,但未启用“按疑似封禁处理”,继续等待短信。${pageError.message || ''}`.trim(), 'warn' ); } } await addLog( `步骤 9:${getPhoneSmsProviderLabel(normalizedActivation.provider)} 号码 ${normalizedActivation.phoneNumber} 状态:${statusText}(已等待 ${Math.ceil(elapsedMs / 1000)} 秒,第 ${pollCount}/${pollMaxRounds} 次轮询)。`, 'info' ); }, }); await clearPhoneRuntimeCountdown(); return { code, replaceNumber: false, }; } catch (error) { if (!isPhoneCodeTimeoutError(error)) { if (isPhoneResendBannedNumberError(error)) { await addLog( `步骤 9:OpenAI 无法向号码 ${normalizedActivation.phoneNumber} 发送短信,立即更换号码。${error.message}`, 'warn' ); await clearPhoneRuntimeCountdown(); return { code: '', replaceNumber: true, reason: 'resend_phone_banned', }; } if (isPhoneMaxUsageExceededFlowError(error)) { await addLog( `步骤 9:OpenAI 提示号码 ${normalizedActivation.phoneNumber} 达到使用上限,立即更换号码。${error.message}`, 'warn' ); await clearPhoneRuntimeCountdown(); return { code: '', replaceNumber: true, reason: 'phone_max_usage_exceeded', }; } if (isPhoneResendServerError(error)) { await addLog( `步骤 9:重发短信后进入 contact-verification 500 页面,立即更换号码。${error.message}`, 'warn' ); await clearPhoneRuntimeCountdown(); return { code: '', replaceNumber: true, reason: 'resend_server_error', }; } if (isPhoneResendThrottledError(error)) { if (shouldTreatResendThrottledAsBanned(state)) { await addLog( `步骤 9:号码 ${normalizedActivation.phoneNumber} 重发限流且配置为高风险封禁,立即更换号码。${error.message}`, 'warn' ); await clearPhoneRuntimeCountdown(); return { code: '', replaceNumber: true, reason: 'resend_throttled_high_risk_banned', }; } await addLog( `步骤 9:号码 ${normalizedActivation.phoneNumber} 重发限流,但未启用高风险换号,继续原等待逻辑。${error.message}`, 'warn' ); await sleepWithStop(pollIntervalSeconds * 1000); continue; } if (isPhoneActivationOrderMissingError(error, normalizedActivation.provider)) { await addLog( `步骤 9:${providerLabel} 号码 ${normalizedActivation.phoneNumber} 的接码订单已失效(${error.message || error}),立即更换号码。`, 'warn' ); await clearPhoneRuntimeCountdown(); return { code: '', replaceNumber: true, reason: 'activation_not_found', }; } throw error; } if (windowIndex < timeoutWindows) { await addLog( `步骤 9:号码 ${normalizedActivation.phoneNumber} 在 ${waitSeconds} 秒内未收到短信,正在请求再次发送。`, 'warn' ); if (!usePageResend) { await addLog( `步骤 9:${providerLabel} 保持当前验证码页会话并跳过页面重发,避免触发 405 或重发限流;继续轮询当前号码。`, 'warn' ); continue; } if (resendTriggeredForCurrentNumber) { await addLog( `步骤 9:号码 ${normalizedActivation.phoneNumber} 已触发过一次页面重发;为避免限流,将继续轮询不再点击重发。`, 'warn' ); continue; } try { const resendProbeResult = await resendPhoneVerificationCode(tabId, { probeOnly: true }); if (isWhatsAppPhoneResendResult(resendProbeResult)) { await addLog( `步骤 9:页面重发入口显示 WhatsApp 通道(${resendProbeResult.channelText || resendProbeResult.text || 'WhatsApp'}),当前接码平台无法读取 WhatsApp 消息,立即更换号码。`, 'warn' ); await clearPhoneRuntimeCountdown(); return { code: '', replaceNumber: true, reason: 'whatsapp_resend_channel', }; } await requestAdditionalPhoneSms(state, normalizedActivation); if (resendProbeResult?.probed) { const resendResult = await resendPhoneVerificationCode(tabId); if (isWhatsAppPhoneResendResult(resendResult)) { await addLog( `步骤 9:页面重发入口切换为 WhatsApp 通道(${resendResult.channelText || resendResult.text || 'WhatsApp'}),当前接码平台无法读取 WhatsApp 消息,立即更换号码。`, 'warn' ); await clearPhoneRuntimeCountdown(); return { code: '', replaceNumber: true, reason: 'whatsapp_resend_channel', }; } } resendTriggeredForCurrentNumber = true; await addLog('步骤 9:已点击手机验证码页面的“重新发送短信”。', 'info'); } catch (resendError) { if (isStopRequestedError(resendError)) { throw resendError; } if (isPhoneResendBannedNumberError(resendError)) { await addLog( `步骤 9:OpenAI 无法向号码 ${normalizedActivation.phoneNumber} 发送短信,立即更换号码。${resendError.message}`, 'warn' ); await clearPhoneRuntimeCountdown(); return { code: '', replaceNumber: true, reason: 'resend_phone_banned', }; } if (isPhoneResendThrottledError(resendError)) { await addLog( `步骤 9:号码 ${normalizedActivation.phoneNumber} 重发短信被限流,立即更换号码。${resendError.message}`, 'warn' ); await clearPhoneRuntimeCountdown(); return { code: '', replaceNumber: true, reason: shouldTreatResendThrottledAsBanned(state) ? 'resend_throttled_high_risk_banned' : 'resend_throttled', }; } if (isPhoneResendServerError(resendError)) { await addLog( `步骤 9:重发短信后进入 contact-verification 500 页面,立即更换号码。${resendError.message}`, 'warn' ); await clearPhoneRuntimeCountdown(); return { code: '', replaceNumber: true, reason: 'resend_server_error', }; } await addLog(`步骤 9:点击手机验证码页面重发按钮失败。${resendError.message}`, 'warn'); } continue; } await addLog( `步骤 9:号码 ${normalizedActivation.phoneNumber} 连续 ${timeoutWindows} 轮未收到短信,将在步骤 9 内更换号码。`, 'warn' ); await clearPhoneRuntimeCountdown(); return { code: '', replaceNumber: true, reason: `sms_timeout_after_${timeoutWindows}_windows`, }; } } 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'; const onPollStatus = typeof options?.onPollStatus === 'function' ? options.onPollStatus : null; 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 = resolvePhoneCodePollMaxRoundsForWindow( waitSeconds, pollIntervalSeconds, state?.phoneCodePollMaxRounds ); 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} 秒,每 ${pollIntervalSeconds} 秒轮询一次,最多 ${pollMaxRounds} 次轮询)。`, '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 }) => { await addLog( `步骤 ${visibleStep}:${providerLabel} 状态 ${normalizedActivation.phoneNumber}: ${statusText}(已等待 ${Math.ceil(elapsedMs / 1000)} 秒,第 ${pollCount}/${pollMaxRounds} 次轮询)。`, 'info', { step: visibleStep, stepKey } ); }, onWaitingForCode: async ({ elapsedMs, pollCount, statusText }) => { if (onPollStatus) { await onPollStatus({ elapsedMs, pollCount, statusText }); } }, }); 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); const pageStateCheckTimeoutMs = Math.max(1, Math.floor(Number(options?.pageStateCheckTimeoutMs) || 5000)); if (!activation) { throw new Error('步骤 4:未找到当前注册手机号激活记录,请重新执行步骤 2。'); } const assertSignupPhoneStillApplicable = async (phaseLabel) => { try { const pageState = await readPhonePageState(tabId, pageStateCheckTimeoutMs); if (isSignupEmailVerificationPageState(pageState)) { throw buildSignupPhoneStaleEmailVerificationError(pageState); } return pageState; } catch (error) { if (isStopRequestedError(error) || isStaleSignupPhoneEmailVerificationError(error)) { throw error; } await throwPhoneResendServerErrorIfAuthTabShowsIt(tabId); await addLog( `步骤 4:检查注册手机号页面状态(${phaseLabel})失败,将继续等待短信。${error.message}`, 'warn', { step: 4, stepKey: 'fetch-signup-code', } ); return null; } }; let shouldCancelActivation = true; try { for (let attempt = 1; attempt <= DEFAULT_PHONE_SUBMIT_ATTEMPTS; attempt += 1) { throwIfStopped(); state = await getState(); await assertSignupPhoneStillApplicable('waiting for SMS code'); const code = await waitForSignupPhoneCode(state, activation, { onPollStatus: async () => { await assertSignupPhoneStillApplicable('while waiting for SMS code'); }, onTimeoutWindow: async () => { try { await resendSignupPhoneVerificationCode(tabId); await addLog('步骤 4:已点击注册手机验证码页面的“重新发送”。', 'info', { step: 4, stepKey: 'fetch-signup-code', }); } catch (resendError) { if (isStopRequestedError(resendError)) { throw resendError; } if (isPhoneResendServerError(resendError)) { throw buildPhoneResendServerError(resendError); } await throwPhoneResendServerErrorIfAuthTabShowsIt(tabId); await addLog(`步骤 4:注册手机验证码页面重发失败,将继续轮询短信。${resendError.message}`, 'warn', { step: 4, stepKey: 'fetch-signup-code', }); } }, }); await assertSignupPhoneStillApplicable('before submitting SMS 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; } if (isPhoneResendServerError(resendError)) { throw buildPhoneResendServerError(resendError); } await throwPhoneResendServerErrorIfAuthTabShowsIt(tabId); 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('openai-auth', { 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('openai-auth', { 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; } if (isPhoneResendServerError(resendError)) { throw buildPhoneResendServerError(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; } if (isPhoneResendServerError(resendError)) { throw buildPhoneResendServerError(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); let shouldCancelActivation = false; let remainingResendRequests = Math.max(0, Number(state.verificationResendCount) || 0); const maxNumberReplacementAttempts = normalizePhoneReplacementLimit( state.phoneVerificationReplacementLimit ); let usedNumberReplacementAttempts = 0; let preferredActivationExhausted = false; let preferReuseExistingActivationOnAddPhone = false; let addPhoneReentryWithSameActivation = 0; const countrySmsFailureCounts = new Map(); const countryPriceFloorByKey = new Map(); const normalizeCountryFailureKey = (countryId, provider = activation?.provider || state?.phoneSmsProvider || '') => { const normalizedProvider = normalizePhoneSmsProvider(provider || state?.phoneSmsProvider || ''); const scopedState = scopeStateToActivationProvider(state, { provider: normalizedProvider }); const providerAdapter = getPhoneSmsProviderAdapterForState(scopedState, normalizedProvider); const countryKey = getProviderCountryKey(providerAdapter, countryId); return countryKey ? `${normalizedProvider}:${countryKey}` : ''; }; const splitCountryFailureKey = (countryKey, providerHint = '') => { const fallbackProvider = normalizePhoneSmsProvider( providerHint || activation?.provider || state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER ); const raw = String(countryKey || '').trim(); if (!raw) { return { provider: fallbackProvider, countryKey: '' }; } const idx = raw.indexOf(':'); if (idx <= 0) { return { provider: fallbackProvider, countryKey: raw }; } const providerPrefix = normalizePhoneSmsProvider(raw.slice(0, idx)); const keyPart = raw.slice(idx + 1).trim(); return { provider: providerPrefix || fallbackProvider, countryKey: keyPart, }; }; const resolveCountryLabelByFailureKey = (countryKey, provider = activation?.provider || state?.phoneSmsProvider || '') => { const parsed = splitCountryFailureKey(countryKey, provider); const normalizedProvider = normalizePhoneSmsProvider(parsed.provider || provider || state?.phoneSmsProvider || ''); const normalizedCountryKey = String(parsed.countryKey || '').trim(); if (!normalizedCountryKey) { return 'Unknown country'; } return getProviderCountryLabel(state, normalizedProvider, normalizedCountryKey); }; const directNavigateToAddPhone = async (attemptLabel = 'after replace-number rotation') => { if (typeof navigateAuthTabToAddPhone !== 'function') { return null; } const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; const result = await navigateAuthTabToAddPhone(tabId, { visibleStep, timeoutMs: 30000, logMessage: '步骤 9:认证页已失联,直接打开添加手机号页面后等待脚本恢复。', logStepKey: 'phone-verification', attemptLabel, }); if (result?.error) { throw new Error(result.error); } return { addPhonePage: true, phoneVerificationPage: false, url: 'https://auth.openai.com/add-phone', ...(result || {}), }; }; const ensureAddPhonePageBeforeSubmit = async (attemptLabel = 'before submit', options = {}) => { const allowDirectNavigation = Boolean(options.allowDirectNavigation); let snapshot = null; let snapshotError = null; try { snapshot = await readPhonePageState(tabId, 12000); } catch (error) { snapshotError = error; await addLog( `步骤 9:检查认证页状态失败(${attemptLabel})。${error.message}`, 'warn' ); snapshot = null; } if (snapshot?.addPhonePage) { return snapshot; } let returnError = null; try { const returned = await returnToAddPhone(tabId); const merged = { ...(snapshot || {}), ...(returned || {}), }; if (merged?.addPhonePage) { return merged; } } catch (error) { returnError = error; await addLog( `步骤 9:返回添加手机号页面失败(${attemptLabel})。${error.message}`, 'warn' ); } if ( allowDirectNavigation && ( isAuthContentScriptUnreachableError(snapshotError) || isAuthContentScriptUnreachableError(returnError) ) ) { const navigated = await directNavigateToAddPhone(attemptLabel); if (navigated) { return navigated; } } let latest = null; try { latest = await readPhonePageState(tabId, 15000); } catch (error) { if (allowDirectNavigation && isAuthContentScriptUnreachableError(error)) { const navigated = await directNavigateToAddPhone(attemptLabel); if (navigated) { return navigated; } } throw error; } if (!latest?.addPhonePage) { throw new Error( `步骤 9:提交手机号前认证页未停留在添加手机号页面(${attemptLabel})。URL: ${latest?.url || 'unknown'}` ); } return latest; }; const getCountryFailureCount = (countryId, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) => { const countryKey = normalizeCountryFailureKey(countryId, providerId); if (!countryKey) { return 0; } return Math.max(0, Math.floor(Number(countrySmsFailureCounts.get(countryKey)) || 0)); }; const markCountrySmsFailure = async (countryId, reason = 'sms_timeout', providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) => { const countryKey = normalizeCountryFailureKey(countryId, providerId); if (!countryKey) { return; } const parsed = splitCountryFailureKey(countryKey, providerId); const nextCount = getCountryFailureCount(parsed.countryKey, parsed.provider) + 1; countrySmsFailureCounts.set(countryKey, nextCount); if (nextCount >= PHONE_SMS_FAILURE_SKIP_THRESHOLD) { const countryLabel = resolveCountryLabelByFailureKey(countryKey, providerId); await addLog( `步骤 9:${countryLabel} 已累计 ${nextCount} 次短信失败(${formatStep9Reason(reason)});下次获取号码会优先尝试其它已选国家。`, 'warn' ); } }; const clearCountrySmsFailure = (countryId, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) => { const countryKey = normalizeCountryFailureKey(countryId, providerId); if (!countryKey) { return; } countrySmsFailureCounts.delete(countryKey); countryPriceFloorByKey.delete(countryKey); }; const getBlockedCountryIds = () => { const activeProvider = normalizePhoneSmsProvider( state?.phoneSmsProvider || activation?.provider || DEFAULT_PHONE_SMS_PROVIDER ); return Array.from(countrySmsFailureCounts.entries()) .filter(([countryKey, count]) => ( Number(count) >= PHONE_SMS_FAILURE_SKIP_THRESHOLD || !countryPriceFloorByKey.has(countryKey) )) .map(([countryKey]) => splitCountryFailureKey(countryKey, activeProvider)) .filter((entry) => entry.provider === activeProvider) .map((entry) => String(entry.countryKey || '').trim()) .filter(Boolean); }; const getCountryPriceFloorById = () => { const activeProvider = normalizePhoneSmsProvider( state?.phoneSmsProvider || activation?.provider || DEFAULT_PHONE_SMS_PROVIDER ); const floorById = {}; countryPriceFloorByKey.forEach((price, compoundCountryKey) => { const numeric = normalizeHeroSmsPrice(price); if (numeric === null || numeric <= 0) { return; } const parsed = splitCountryFailureKey(compoundCountryKey, activeProvider); if (parsed.provider !== activeProvider) { return; } const keyPart = String(parsed.countryKey || '').trim(); if (!keyPart) { return; } floorById[keyPart] = Math.round(numeric * 10000) / 10000; }); return floorById; }; const setCountryPriceFloorFromActivation = async (activationCandidate, reason = '') => { const normalizedActivation = normalizeActivation(activationCandidate); if (!normalizedActivation) { return; } const activationCountryKey = getProviderActivationCountryKey(state, normalizedActivation); const countryKey = normalizeCountryFailureKey(activationCountryKey, normalizedActivation.provider); if (!countryKey) { return; } const floorPrice = getProviderActivationPrice(state, normalizedActivation) ?? getActivationAcquiredPriceHint(normalizedActivation); if (floorPrice === null || floorPrice <= 0) { return; } const currentFloor = normalizeHeroSmsPrice(countryPriceFloorByKey.get(countryKey)); if (currentFloor !== null && currentFloor >= floorPrice) { return; } const normalizedFloor = Math.round(floorPrice * 10000) / 10000; countryPriceFloorByKey.set(countryKey, normalizedFloor); const countryLabel = resolveCountryLabelByFailureKey(countryKey, normalizedActivation.provider); await addLog( `步骤 9:${countryLabel} 因 ${formatStep9Reason(reason || 'sms_timeout')} 将尝试更高价格档位(> ${normalizedFloor})。`, 'warn' ); }; const isPreferredActivation = (activationCandidate, stateSnapshot = {}) => ( isSameActivation( stateSnapshot?.[PREFERRED_PHONE_ACTIVATION_STATE_KEY], activationCandidate ) ); const markPreferredActivationExhausted = async (reason = '') => { if (preferredActivationExhausted || !activation || !isPreferredActivation(activation, state)) { return; } preferredActivationExhausted = true; await addLog( `步骤 9:优先号码 ${activation.phoneNumber} 失败(${formatStep9Reason(reason || 'unknown')}),将改为获取新号码。`, 'warn' ); }; const rotateCurrentActivation = async (reason = '', releaseAction = 'cancel') => { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { return { handled: false, nextActivation: null }; } const scopedState = scopeStateToActivationProvider(state, normalizedActivation); const provider = getActivationProviderAdapterForState(scopedState, normalizedActivation); if (!provider || typeof provider.rotateActivation !== 'function') { return { handled: false, nextActivation: null }; } const rotated = await provider.rotateActivation( scopedState, normalizedActivation, { releaseAction, reason, } ); const nextActivation = normalizeActivation(rotated?.nextActivation); if (!nextActivation) { return { handled: true, nextActivation: null }; } activation = nextActivation; shouldCancelActivation = true; await persistCurrentActivation(nextActivation); await addLog( `步骤 9:${getPhoneSmsProviderLabel(nextActivation.provider)} 已切换到新号码 ${nextActivation.phoneNumber}。`, 'info' ); return { handled: true, nextActivation }; }; const rotateActivationAfterAddPhoneFailure = async (failureReason, failureCode, submitState = {}) => { await markPreferredActivationExhausted(failureCode || failureReason); usedNumberReplacementAttempts += 1; if (usedNumberReplacementAttempts > maxNumberReplacementAttempts) { throw buildPhoneReplacementLimitError(maxNumberReplacementAttempts, failureCode || 'add_phone_rejected'); } await addLog( `步骤 9:添加手机号失败后正在更换号码(${formatStep9Reason(failureReason)},${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`, 'warn' ); const rotated = shouldCancelActivation && activation ? await rotateCurrentActivation( failureCode || failureReason, getPhoneReplacementReleaseAction(failureCode || failureReason) ) : { handled: false, nextActivation: null }; if (rotated.nextActivation) { preferReuseExistingActivationOnAddPhone = false; addPhoneReentryWithSameActivation = 0; pageState = { ...pageState, ...submitState, addPhonePage: true, phoneVerificationPage: false, }; return; } if (rotated.handled) { await clearCurrentActivation(); activation = null; shouldCancelActivation = false; preferReuseExistingActivationOnAddPhone = false; addPhoneReentryWithSameActivation = 0; pageState = { ...pageState, ...submitState, addPhonePage: true, phoneVerificationPage: false, }; return; } if (shouldCancelActivation && activation) { await cancelPhoneActivation(state, activation); } await clearCurrentActivation(); activation = null; shouldCancelActivation = false; preferReuseExistingActivationOnAddPhone = false; addPhoneReentryWithSameActivation = 0; let addPhoneSnapshot = { ...pageState, ...submitState, addPhonePage: true, phoneVerificationPage: false, }; try { const returned = await returnToAddPhone(tabId); addPhoneSnapshot = { ...addPhoneSnapshot, ...returned, addPhonePage: true, phoneVerificationPage: false, }; } catch (returnError) { await addLog( `步骤 9:号码被拒后返回添加手机号页面失败,将用当前可用状态继续。${returnError.message}`, 'warn' ); } try { const verified = await ensureAddPhonePageBeforeSubmit('after add-phone rejection'); addPhoneSnapshot = { ...addPhoneSnapshot, ...verified, addPhonePage: true, phoneVerificationPage: false, }; } catch (verifyError) { await addLog( `步骤 9:号码被拒后确认添加手机号页面状态失败。${verifyError.message}`, 'warn' ); } pageState = addPhoneSnapshot; }; try { while (true) { state = await getState(); if (!activation) { activation = normalizeActivation(state[PHONE_ACTIVATION_STATE_KEY]); } if (pageState?.addPhonePage) { const addPhoneUrlText = String(pageState?.url || '').trim().toLowerCase(); const looksLikeAddPhoneUrl = /\/add-phone(?:[/?#]|$)/i.test(addPhoneUrlText); if (!looksLikeAddPhoneUrl) { pageState = await ensureAddPhonePageBeforeSubmit( activation ? 'with current activation' : 'with new activation' ); } if (!activation) { activation = await handoffFreeReusablePhone(tabId, state); if (activation) { shouldCancelActivation = false; } else { activation = await acquirePhoneActivation(state, { blockedCountryIds: getBlockedCountryIds(), countryPriceFloorByCountryId: getCountryPriceFloorById(), skipPreferredActivation: preferredActivationExhausted, }); shouldCancelActivation = true; await persistCurrentActivation(activation); } addPhoneReentryWithSameActivation = 0; } else if (preferReuseExistingActivationOnAddPhone) { addPhoneReentryWithSameActivation += 1; if (addPhoneReentryWithSameActivation > 1) { usedNumberReplacementAttempts += 1; if (usedNumberReplacementAttempts > maxNumberReplacementAttempts) { throw buildPhoneReplacementLimitError(maxNumberReplacementAttempts, 'returned_to_add_phone_loop'); } await addLog( `步骤 9:当前号码 ${activation.phoneNumber} 反复返回添加手机号页,正在更换号码(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`, 'warn' ); if (shouldRetireFreeReusableActivationOnFailure(await getState(), activation)) { await retireFreeReusableActivation( `自动白嫖复用号码 ${activation.phoneNumber} 反复返回添加手机号页。` ); } const rotated = shouldCancelActivation && activation ? await rotateCurrentActivation('returned_to_add_phone_loop', 'cancel') : { handled: false, nextActivation: null }; if (!rotated.nextActivation) { if (!rotated.handled && shouldCancelActivation && activation) { await cancelPhoneActivation(state, activation); } await clearCurrentActivation(); activation = null; shouldCancelActivation = false; } preferReuseExistingActivationOnAddPhone = false; addPhoneReentryWithSameActivation = 0; pageState = { ...pageState, addPhonePage: true, phoneVerificationPage: false, }; continue; } await addLog( `步骤 9:页面返回添加手机号,将先重新提交当前号码 ${activation.phoneNumber},暂不获取新号码。`, 'warn' ); } let submitResult = null; try { submitResult = await submitPhoneNumber(tabId, activation.phoneNumber, activation); } catch (submitError) { const submitErrorText = String(submitError?.message || submitError || 'unknown error'); if (isPhoneNumberDeliveryRefusedError(submitErrorText) || isRecoverableAddPhoneSubmitError(submitErrorText)) { await rotateActivationAfterAddPhoneFailure( submitErrorText, isPhoneNumberDeliveryRefusedError(submitErrorText) ? 'phone_delivery_refused' : 'add_phone_submit_failed', { url: pageState?.url || '' } ); continue; } throw submitError; } if (submitResult.addPhoneRejected) { const addPhoneRejectText = String(submitResult.errorText || submitResult.url || 'unknown error'); if (isPhoneNumberUsedError(addPhoneRejectText)) { usedNumberReplacementAttempts += 1; if (usedNumberReplacementAttempts > maxNumberReplacementAttempts) { throw new Error( `步骤 9:更换 ${maxNumberReplacementAttempts} 次号码后手机号验证仍未成功。最后原因:${formatStep9Reason('phone_number_used')}。` ); } await addLog( `步骤 9:添加手机号页面提示 ${activation.phoneNumber} 已被使用(${addPhoneRejectText}),正在更换号码(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`, 'warn' ); await discardPhoneActivationFromReuse( `目标站拒绝该号码(${addPhoneRejectText})。`, activation, await getState() ); if (shouldRetireFreeReusableActivationOnFailure(await getState(), activation)) { await retireFreeReusableActivation( `自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。` ); } const rotated = shouldCancelActivation && activation ? await rotateCurrentActivation('phone_number_used', 'ban') : { handled: false, nextActivation: null }; if (!rotated.nextActivation) { if (!rotated.handled && shouldCancelActivation && activation) { await banPhoneActivation(state, activation); } await clearCurrentActivation(); activation = null; shouldCancelActivation = false; } preferReuseExistingActivationOnAddPhone = false; addPhoneReentryWithSameActivation = 0; pageState = { ...pageState, ...submitResult, addPhonePage: true, phoneVerificationPage: false, }; continue; } if (isPhoneNumberDeliveryRefusedError(addPhoneRejectText)) { await rotateActivationAfterAddPhoneFailure( addPhoneRejectText, 'phone_delivery_refused', submitResult || {} ); continue; } await addLog( `步骤 9:添加手机号页面拒绝当前号码,但未明确提示已使用(${addPhoneRejectText}),将用同一号码再试一次。`, 'warn' ); let retrySubmitError = null; try { submitResult = await submitPhoneNumber(tabId, activation.phoneNumber, activation); } catch (submitError) { retrySubmitError = submitError; } if (retrySubmitError || submitResult.addPhoneRejected) { const retryRejectText = String( retrySubmitError?.message || submitResult?.errorText || submitResult?.url || 'unknown error' ); if ( isPhoneNumberUsedError(retryRejectText) || isPhoneNumberDeliveryRefusedError(retryRejectText) || isRecoverableAddPhoneSubmitError(retryRejectText) ) { await rotateActivationAfterAddPhoneFailure( `add-phone keeps rejecting ${activation.phoneNumber} (${retryRejectText})`, isPhoneNumberUsedError(retryRejectText) ? 'phone_number_used' : (isPhoneNumberDeliveryRefusedError(retryRejectText) ? 'phone_delivery_refused' : 'add_phone_rejected'), submitResult || {} ); continue; } throw new Error( `步骤 9:添加手机号页面持续拒绝当前号码,但没有明确“已使用”状态:${submitResult.errorText || submitResult.url || '未知错误'}。` ); } } await addLog('步骤 9:已在添加手机号页面提交号码。', 'info'); pageState = { ...pageState, ...submitResult, addPhonePage: false, phoneVerificationPage: true, }; preferReuseExistingActivationOnAddPhone = false; addPhoneReentryWithSameActivation = 0; } if (!pageState?.phoneVerificationPage) { pageState = await readPhonePageState(tabId); } if (!pageState?.phoneVerificationPage) { return pageState; } if (!activation) { throw new Error('认证页面正在等待手机验证码,但当前运行没有保存手机号接码订单。'); } let shouldReplaceNumber = false; let replaceReason = ''; for (let attempt = 1; attempt <= DEFAULT_PHONE_SUBMIT_ATTEMPTS; attempt += 1) { throwIfStopped(); const codeResult = await waitForPhoneCodeOrRotateNumber(tabId, state, activation); if (codeResult.replaceNumber) { await markPreferredActivationExhausted(codeResult.reason || 'sms_timeout'); shouldReplaceNumber = true; replaceReason = codeResult.reason || 'sms_not_received'; break; } await setPhoneRuntimeState({ [PHONE_VERIFICATION_CODE_STATE_KEY]: String(codeResult.code || '').trim(), }); activation = markActivationPhoneCodeReceived(activation) || activation; await persistCurrentActivation(activation); await setPhoneRuntimeState({ [PHONE_VERIFICATION_CODE_STATE_KEY]: String(codeResult.code || '').trim(), }); await markFreeReusableActivationAfterCode(state, activation); await addLog(`步骤 9:已收到手机验证码 ${codeResult.code}。`, 'info'); const submitResult = await submitPhoneVerificationCode(tabId, codeResult.code); if (submitResult.returnedToAddPhone) { await addLog( '步骤 9:提交验证码后返回添加手机号页面,将先重试当前号码。', 'warn' ); preferReuseExistingActivationOnAddPhone = true; pageState = { ...pageState, ...submitResult, addPhonePage: true, phoneVerificationPage: false, }; break; } if (submitResult.invalidCode) { const invalidErrorText = String(submitResult.errorText || submitResult.url || 'unknown error'); if (isPhoneNumberUsedError(invalidErrorText)) { shouldReplaceNumber = true; replaceReason = 'phone_number_used'; await discardPhoneActivationFromReuse( `目标站拒绝该号码(${invalidErrorText})。`, activation, await getState() ); if (shouldRetireFreeReusableActivationOnFailure(await getState(), activation)) { await retireFreeReusableActivation( `自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。` ); } if (shouldCancelActivation && activation) { await banPhoneActivation(state, activation); shouldCancelActivation = false; } await addLog( `步骤 9:手机号被提示已使用(${invalidErrorText}),立即更换新号码。`, 'warn' ); break; } if (attempt >= DEFAULT_PHONE_SUBMIT_ATTEMPTS) { shouldReplaceNumber = true; replaceReason = 'code_rejected'; if (shouldCancelActivation && activation) { await banPhoneActivation(state, activation); shouldCancelActivation = false; } await addLog( `步骤 9:手机验证码连续 ${DEFAULT_PHONE_SUBMIT_ATTEMPTS} 次被拒(${invalidErrorText}),将更换号码。`, 'warn' ); break; } if (remainingResendRequests > 0) { remainingResendRequests -= 1; try { const resendProbeResult = await resendPhoneVerificationCode(tabId, { probeOnly: true }); if (isWhatsAppPhoneResendResult(resendProbeResult)) { shouldReplaceNumber = true; replaceReason = 'whatsapp_resend_channel'; await addLog( `步骤 9:验证码被拒后的重发入口显示 WhatsApp 通道(${resendProbeResult.channelText || resendProbeResult.text || 'WhatsApp'}),当前接码平台无法读取 WhatsApp 消息,将更换号码。`, 'warn' ); break; } await requestAdditionalPhoneSms(state, activation); if (resendProbeResult?.probed) { const resendResult = await resendPhoneVerificationCode(tabId); if (isWhatsAppPhoneResendResult(resendResult)) { shouldReplaceNumber = true; replaceReason = 'whatsapp_resend_channel'; await addLog( `步骤 9:验证码被拒后的重发入口切换为 WhatsApp 通道(${resendResult.channelText || resendResult.text || 'WhatsApp'}),当前接码平台无法读取 WhatsApp 消息,将更换号码。`, 'warn' ); break; } } await addLog('步骤 9:手机验证码被拒后已点击“重新发送短信”。', 'info'); } catch (resendError) { await addLog(`步骤 9:验证码被拒后点击重发失败。${resendError.message}`, 'warn'); } if (shouldReplaceNumber) { break; } await addLog( `步骤 9:手机验证码被拒,已请求再次发送短信(剩余 ${remainingResendRequests} 次重发)。`, 'warn' ); } else { await addLog( '步骤 9:手机验证码被拒,配置的重发次数已用完,将在当前接码窗口内继续重试。', 'warn' ); } continue; } const latestSuccessState = await getState(); if (shouldSkipTerminalStatusForFreeReuse(latestSuccessState, activation)) { const terminalProviderLabel = getPhoneSmsProviderLabel(activation.provider); await addLog( `步骤 9:已跳过 ${terminalProviderLabel} 完成状态,保留 ${activation.phoneNumber} 供白嫖复用。`, 'info' ); await markFreeReusableActivationAfterInitialSuccess(latestSuccessState, activation); } else { await completePhoneActivation(latestSuccessState, activation); } await markFreeReusableActivationAfterAutoSuccess(state, activation); if (!isFreeAutoReuseActivation(activation)) { await markActivationReusableAfterSuccess(state, activation); } clearCountrySmsFailure(activation.countryId, activation.provider); shouldCancelActivation = false; await clearCurrentActivation(); await setPhoneRuntimeState({ phoneNumber: activation.phoneNumber, }); addPhoneReentryWithSameActivation = 0; await addLog('步骤 9:手机号验证已完成,等待 OAuth 授权页。', 'ok'); return submitResult; } if (!shouldReplaceNumber) { if (pageState?.addPhonePage) { continue; } throw new Error('手机号验证未完成。'); } if ( activation && ( replaceReason === 'resend_throttled' || replaceReason === 'route_405_retry_loop' || /^sms_timeout_after_/i.test(String(replaceReason || '')) ) ) { await setCountryPriceFloorFromActivation(activation, replaceReason || 'sms_timeout'); await markCountrySmsFailure(activation.countryId, replaceReason || 'sms_timeout', activation.provider); } await markPreferredActivationExhausted(replaceReason || 'replace_number'); usedNumberReplacementAttempts += 1; if (usedNumberReplacementAttempts > maxNumberReplacementAttempts) { throw buildPhoneReplacementLimitError(maxNumberReplacementAttempts, replaceReason || 'unknown'); } const rotated = shouldCancelActivation && activation ? await rotateCurrentActivation( replaceReason || 'replace_number', getPhoneReplacementReleaseAction(replaceReason || 'replace_number') ) : { handled: false, nextActivation: null }; if (shouldRetireFreeReusableActivationOnFailure(await getState(), activation)) { await retireFreeReusableActivation( `自动白嫖复用号码 ${activation.phoneNumber} 在失败后被更换。` ); } if (isPhoneNumberUsedFailureReason(replaceReason)) { await discardPhoneActivationFromReuse( `目标站拒绝该号码(${replaceReason})。`, activation, await getState() ); } if (!rotated.nextActivation) { if (!rotated.handled && shouldCancelActivation && activation) { await cancelPhoneActivation(state, activation); } await clearCurrentActivation(); activation = null; shouldCancelActivation = false; } addPhoneReentryWithSameActivation = 0; let returnResult = null; try { returnResult = await returnToAddPhone(tabId); } catch (returnError) { await addLog(`步骤 9:更换号码前返回添加手机号页面失败。${returnError.message}`, 'warn'); } if (!returnResult?.addPhonePage) { try { const stateSnapshot = await readPhonePageState(tabId, 12000); if (stateSnapshot?.addPhonePage) { returnResult = { ...(returnResult || {}), ...stateSnapshot, addPhonePage: true, phoneVerificationPage: false, }; } } catch (_) { // Best effort: keep fallback state for compatibility with tests and older flows. } } const verifiedAddPhoneState = await ensureAddPhonePageBeforeSubmit( 'after replace-number rotation', { allowDirectNavigation: true } ); returnResult = { ...(returnResult || {}), ...verifiedAddPhoneState, addPhonePage: true, phoneVerificationPage: false, }; await addLog( `步骤 9:正在更换号码并在步骤 9 内重试(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`, 'warn' ); pageState = { ...pageState, ...returnResult, addPhonePage: true, phoneVerificationPage: false, }; } } catch (error) { const errorMessage = String(error?.message || error || ''); if ( errorMessage.startsWith(PHONE_MANUAL_FREE_REUSE_ERROR_PREFIX) || errorMessage.startsWith(PHONE_AUTO_FREE_REUSE_PREPARE_ERROR_PREFIX) ) { throw error; } if (shouldRetireFreeReusableActivationOnFailure(await getState(), activation)) { await retireFreeReusableActivation( `自动白嫖复用号码 ${activation.phoneNumber} 执行失败:${errorMessage || 'unknown error'}。` ); } if (shouldCancelActivation && activation) { await cancelPhoneActivation(await getState(), activation); } await clearCurrentActivation(); throw sanitizePhoneRestartStep7Error(sanitizePhoneCodeTimeoutError(error)); } finally { activePhoneVerificationLogStep = previousLogStep; activePhoneVerificationLogStepKey = previousLogStepKey; } } return { cancelSignupPhoneActivation, completeLoginPhoneVerificationFlow, completePhoneVerificationFlow, completeSignupPhoneVerificationFlow, finalizeLoginPhoneActivationAfterSuccess, finalizeSignupPhoneActivationAfterSuccess, isPhoneResendBannedNumberError, normalizeActivation, pollPhoneActivationCode, prepareLoginPhoneActivation, prepareSignupPhoneActivation, reactivatePhoneActivation, requestPhoneActivation, waitForLoginPhoneCode, waitForSignupPhoneCode, }; } return { createPhoneVerificationHelpers, }; });