diff --git a/background.js b/background.js index 4e3c3df..cb414ab 100644 --- a/background.js +++ b/background.js @@ -242,6 +242,7 @@ const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts'; const DEFAULT_GPC_HELPER_API_URL = 'https://gopay.hwork.pro'; const DEFAULT_SUB2API_GROUP_NAME = 'codex'; const DEFAULT_SUB2API_PROXY_NAME = ''; +const DEFAULT_SUB2API_ACCOUNT_PRIORITY = 1; const CONTRIBUTION_SOURCE_CPA = 'cpa'; const CONTRIBUTION_SOURCE_SUB2API = 'sub2api'; const CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME = 'codex号池'; @@ -574,6 +575,7 @@ const PERSISTED_SETTING_DEFAULTS = { sub2apiPassword: '', sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME, sub2apiGroupNames: DEFAULT_SUB2API_GROUP_NAMES, + sub2apiAccountPriority: DEFAULT_SUB2API_ACCOUNT_PRIORITY, sub2apiDefaultProxyName: DEFAULT_SUB2API_PROXY_NAME, ipProxyEnabled: false, ipProxyService: DEFAULT_IP_PROXY_SERVICE, @@ -632,6 +634,8 @@ const PERSISTED_SETTING_DEFAULTS = { autoRunDelayMinutes: 30, autoStepDelaySeconds: null, phoneVerificationEnabled: false, + freePhoneReuseEnabled: true, + freePhoneReuseAutoEnabled: true, signupMethod: DEFAULT_SIGNUP_METHOD, phoneSmsProvider: DEFAULT_PHONE_SMS_PROVIDER, phoneSmsProviderOrder: [], @@ -798,6 +802,7 @@ const DEFAULT_STATE = { currentPhoneVerificationCountdownWindowIndex: 0, currentPhoneVerificationCountdownWindowTotal: 0, reusablePhoneActivation: null, + freeReusablePhoneActivation: null, phoneReusableActivationPool: [], signupPhoneNumber: '', signupPhoneActivation: null, @@ -2105,6 +2110,18 @@ function normalizeSub2ApiGroupNames(value = '') { return names; } +function normalizeSub2ApiAccountPriority(value, fallback = DEFAULT_SUB2API_ACCOUNT_PRIORITY) { + const rawValue = String(value ?? '').trim(); + const numeric = Number(rawValue); + if (!rawValue || !Number.isSafeInteger(numeric) || numeric < 1) { + const fallbackNumber = Number(fallback); + return Number.isSafeInteger(fallbackNumber) && fallbackNumber >= 1 + ? fallbackNumber + : DEFAULT_SUB2API_ACCOUNT_PRIORITY; + } + return numeric; +} + function normalizePersistentSettingValue(key, value) { switch (key) { case 'panelMode': @@ -2125,6 +2142,8 @@ function normalizePersistentSettingValue(key, value) { return String(value || '').trim(); case 'sub2apiGroupNames': return normalizeSub2ApiGroupNames(value); + case 'sub2apiAccountPriority': + return normalizeSub2ApiAccountPriority(value); case 'sub2apiDefaultProxyName': return String(value || '').trim(); case 'ipProxyEnabled': @@ -2286,6 +2305,8 @@ function normalizePersistentSettingValue(key, value) { case 'gopayHelperLocalSmsHelperEnabled': case 'autoRunDelayEnabled': case 'phoneVerificationEnabled': + case 'freePhoneReuseEnabled': + case 'freePhoneReuseAutoEnabled': case 'plusModeEnabled': return Boolean(value); case 'phoneSmsProvider': @@ -3093,6 +3114,7 @@ async function resetState() { 'tabRegistry', 'sourceLastUrls', 'reusablePhoneActivation', + 'freeReusablePhoneActivation', 'phoneReusableActivationPool', 'luckmailApiKey', 'luckmailBaseUrl', @@ -3132,6 +3154,19 @@ async function resetState() { .map((entry) => normalizePhonePreferredActivation(entry)) .filter(Boolean) : []; + const freeReusablePhoneActivation = ( + prev.freeReusablePhoneActivation + && typeof prev.freeReusablePhoneActivation === 'object' + && !Array.isArray(prev.freeReusablePhoneActivation) + && String( + prev.freeReusablePhoneActivation.phoneNumber + ?? prev.freeReusablePhoneActivation.number + ?? prev.freeReusablePhoneActivation.phone + ?? '' + ).trim() + ) + ? prev.freeReusablePhoneActivation + : null; await chrome.storage.session.clear(); await chrome.storage.session.set({ ...DEFAULT_STATE, @@ -3154,6 +3189,8 @@ async function resetState() { currentLuckmailMailCursor: null, // Keep reusable phone activation across round resets so the same number can be reactivated up to maxUses. reusablePhoneActivation, + // Keep free reuse phone activation until the user clears or the flow retires it. + freeReusablePhoneActivation, phoneReusableActivationPool, preferredIcloudHost: prev.preferredIcloudHost || '', }); @@ -5448,7 +5485,6 @@ async function withIcloudLoginHelp(actionLabel, action) { if (shouldEmitIcloudTransientLog(`${safeActionLabel}:final`)) { await addLog(`iCloud:${safeActionLabel}受网络/上下文波动影响:${getErrorMessage(err)}`, 'warn'); } - const safeActionLabel = String(actionLabel || '操作').trim() || '操作'; const transientError = new Error(`iCloud:${safeActionLabel}受网络/上下文波动影响,请稍后重试。`); transientError.code = 'ICLOUD_TRANSIENT_CONTEXT'; transientError.actionLabel = safeActionLabel; @@ -6613,6 +6649,53 @@ async function finalizePhoneActivationAfterSuccessfulFlow(state) { return phoneVerificationHelpers.finalizePendingPhoneActivationConfirmation(state); } +async function clearFreeReusablePhoneActivation() { + await setState({ freeReusablePhoneActivation: null }); + broadcastDataUpdate({ freeReusablePhoneActivation: null }); + await addLog('已清除白嫖复用手机号记录。', 'ok'); + return { ok: true, freeReusablePhoneActivation: null }; +} + +async function setFreeReusablePhoneActivation(record = {}) { + const phoneNumber = String(record.phoneNumber || record.number || record.phone || '').trim(); + if (!phoneNumber) { + throw new Error('请先填写白嫖复用手机号。'); + } + const state = await getState(); + const activationId = String(record.activationId || record.id || record.activation || '').trim(); + const countryId = Math.max(1, Math.floor(Number(record.countryId) || Number(state.heroSmsCountryId) || HERO_SMS_COUNTRY_ID)); + const stateCountryLabel = Math.floor(Number(state.heroSmsCountryId) || 0) === countryId + ? String(state.heroSmsCountryLabel || '').trim() + : ''; + const countryLabel = String( + record.countryLabel + || stateCountryLabel + || (countryId === HERO_SMS_COUNTRY_ID ? HERO_SMS_COUNTRY_LABEL : `Country #${countryId}`) + ).trim(); + const activation = { + ...(activationId ? { activationId } : {}), + phoneNumber, + provider: PHONE_SMS_PROVIDER_HERO, + serviceCode: HERO_SMS_SERVICE_CODE, + countryId, + ...(countryLabel ? { countryLabel } : {}), + successfulUses: Math.max(0, Math.floor(Number(record.successfulUses) || 0)), + maxUses: Math.max(1, Math.floor(Number(record.maxUses) || 3)), + source: 'free-manual-reuse', + recordedAt: Date.now(), + manualOnly: !activationId, + }; + await setState({ freeReusablePhoneActivation: activation }); + broadcastDataUpdate({ freeReusablePhoneActivation: activation }); + await addLog( + activationId + ? `已手动记录白嫖复用手机号 ${phoneNumber}(#${activationId})。` + : `已手动记录白嫖复用手机号 ${phoneNumber}。未填写 HeroSMS 激活 ID,仅支持手动填号复用。`, + 'ok' + ); + return { ok: true, freeReusablePhoneActivation: activation }; +} + // ============================================================ // Tab Registry // ============================================================ @@ -10689,6 +10772,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter clearAccountRunHistory: (...args) => clearAndBroadcastAccountRunHistory(...args), deleteAccountRunHistoryRecords: (...args) => deleteAndBroadcastAccountRunHistoryRecords(...args), clearAutoRunTimerAlarm, + clearFreeReusablePhoneActivation, clearLuckmailRuntimeState, clearStopRequest, closeLocalhostCallbackTabs, @@ -10777,6 +10861,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter setContributionMode, setEmailState, setEmailStateSilently, + setFreeReusablePhoneActivation, setSignupPhoneState, setSignupPhoneStateSilently, setIcloudAliasPreservedState, diff --git a/background/message-router.js b/background/message-router.js index 7121e48..9960bde 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -16,6 +16,7 @@ clearAccountRunHistory, deleteAccountRunHistoryRecords, clearAutoRunTimerAlarm, + clearFreeReusablePhoneActivation, clearLuckmailRuntimeState, clearStopRequest, closeLocalhostCallbackTabs, @@ -103,6 +104,7 @@ setContributionMode, setEmailState, setEmailStateSilently, + setFreeReusablePhoneActivation, setSignupPhoneState, setSignupPhoneStateSilently, setIcloudAliasPreservedState, @@ -684,6 +686,20 @@ return { ok: true }; } + case 'CLEAR_FREE_REUSABLE_PHONE': { + if (typeof clearFreeReusablePhoneActivation !== 'function') { + throw new Error('白嫖复用手机号清除能力未接入。'); + } + return await clearFreeReusablePhoneActivation(); + } + + case 'SET_FREE_REUSABLE_PHONE': { + if (typeof setFreeReusablePhoneActivation !== 'function') { + throw new Error('白嫖复用手机号记录能力未接入。'); + } + return await setFreeReusablePhoneActivation(message.payload || {}); + } + case 'SET_CONTRIBUTION_MODE': { const enabled = Boolean(message.payload?.enabled); const state = await ensureManualInteractionAllowed(enabled ? '进入贡献模式' : '退出贡献模式'); @@ -991,6 +1007,9 @@ if (Boolean(currentState?.contributionMode) && typeof setContributionMode === 'function') { await setContributionMode(true); } + if (Object.keys(stateUpdates).length > 0 && typeof broadcastDataUpdate === 'function') { + broadcastDataUpdate(stateUpdates); + } if (modeChanged) { const selectedPlusPaymentMethod = getPlusPaymentMethodLabel( stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal' diff --git a/background/panel-bridge.js b/background/panel-bridge.js index 2b0f077..a4dff4c 100644 --- a/background/panel-bridge.js +++ b/background/panel-bridge.js @@ -293,6 +293,7 @@ sub2apiPassword: state.sub2apiPassword, sub2apiGroupName: groupName, sub2apiDefaultProxyName: state.sub2apiDefaultProxyName, + sub2apiAccountPriority: state.sub2apiAccountPriority, logStep: 7, }, }, { diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index f497774..429e990 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -10,6 +10,7 @@ generateRandomName, getOAuthFlowStepTimeoutMs, getState, + requestStop = null, sendToContentScript, sendToContentScriptResilient, setState, @@ -40,6 +41,7 @@ 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'; @@ -89,7 +91,13 @@ const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::'; 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_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 activationPriceHintsByKey = new Map(); @@ -292,6 +300,24 @@ return Math.max(0, Math.floor(Number(value) || 0)); } + 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) { @@ -435,6 +461,15 @@ return Boolean(value); } + 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) { @@ -983,6 +1018,66 @@ 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) } : {}), + }; + } + + 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 countryLabel = String(record.countryLabel || '').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: normalizeCountryId(record.countryId, HERO_SMS_COUNTRY_ID), + ...(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); + return { + ...normalized, + provider: PHONE_SMS_PROVIDER_HERO, + source: 'free-manual-reuse', + ...(recordedAt ? { recordedAt } : {}), + }; + } + + function markActivationPhoneCodeReceived(activation) { + const normalizedActivation = normalizeActivation(activation); + if (!normalizedActivation) { + return null; + } + return { + ...normalizedActivation, + phoneCodeReceived: true, + phoneCodeReceivedAt: normalizedActivation.phoneCodeReceivedAt || Date.now(), }; } @@ -1067,6 +1162,18 @@ } } + 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 normalizeActivationFallback(record) { if (!record || typeof record !== 'object' || Array.isArray(record)) { return null; @@ -1183,6 +1290,34 @@ 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 shouldTreatResendThrottledAsBanned(state = {}) { + return Boolean(state?.phoneResendThrottledAsBannedEnabled); + } + + function buildHighRiskResendThrottledError(message = '') { + return new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${message || 'OpenAI resend is throttled and configured as high-probability banned phone.'}`); + } + + function buildPhoneMaxUsageExceededError(message = '') { + return new Error(`PHONE_MAX_USAGE_EXCEEDED::${message || 'OpenAI reported phone_max_usage_exceeded for this phone number.'}`); + } + + 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) { @@ -1505,6 +1640,19 @@ }; } + function resolveHeroSmsPhoneConfig(state = {}) { + const apiKey = normalizeApiKey(state.heroSmsApiKey); + if (!apiKey) { + throw new Error('HeroSMS API key is missing. Save it in the side panel before running the phone flow.'); + } + return { + provider: PHONE_SMS_PROVIDER_HERO, + apiKey, + baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL), + countryCandidates: resolveCountryCandidates(state), + }; + } + function parseActivationPayload(payload, fallback = null) { const normalizedFallback = normalizeActivation(fallback) || normalizeActivationFallback(fallback); const directActivation = normalizeActivation(payload); @@ -3220,16 +3368,28 @@ if (!normalizedActivation) { return ''; } + const normalizedStatus = Math.floor(Number(status) || 0); + if ( + (normalizedStatus === 6 || normalizedStatus === 8) + && shouldSkipTerminalStatusForFreeReuse(state, normalizedActivation) + ) { + const identifier = normalizedActivation.phoneNumber || normalizedActivation.activationId || 'current activation'; + await addLog( + `步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的 setStatus(${normalizedStatus})。`, + 'info' + ); + return `free reuse setStatus(${normalizedStatus}) skipped`; + } const config = resolvePhoneConfig(state); if (config.provider === PHONE_SMS_PROVIDER_5SIM) { - const endpoint = status === 6 + const endpoint = normalizedStatus === 6 ? `/user/finish/${normalizedActivation.activationId}` : `/user/cancel/${normalizedActivation.activationId}`; const payload = await fetchFiveSimPayload(config, endpoint, actionLabel || '5sim set status'); return describeFiveSimPayload(payload); } if (config.provider === PHONE_SMS_PROVIDER_NEXSMS) { - if (status === 6) { + if (normalizedStatus === 6) { return 'NexSMS complete skipped'; } const payload = await fetchNexSmsPayload( @@ -3251,12 +3411,21 @@ const payload = await fetchHeroSmsPayload(config, { action: 'setStatus', id: normalizedActivation.activationId, - status, + status: normalizedStatus, }, actionLabel); return describeHeroSmsPayload(payload); } 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; + } if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) { const provider = getFiveSimProviderForState(state); if (provider) { @@ -3269,6 +3438,15 @@ 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 (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) { const provider = getFiveSimProviderForState(state); if (provider) { @@ -3282,8 +3460,62 @@ } } + 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'; + } + 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; + } if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) { const provider = getFiveSimProviderForState(state); if (provider) { @@ -3313,6 +3545,134 @@ } } + function isHeroSmsWaitingStatusText(text) { + return /^STATUS_(WAIT_CODE|WAIT_RETRY|WAIT_RESEND)(?::.+)?$/i.test(String(text || '').trim()); + } + + function isHeroSmsReadyForFreshSmsText(text) { + return /^STATUS_WAIT_CODE(?::.+)?$/i.test(String(text || '').trim()); + } + + function isHeroSmsCancelledStatusText(text) { + return /^STATUS_CANCEL$/i.test(String(text || '').trim()); + } + + async function prepareFreeReusablePhoneActivation(state = {}, activation) { + const normalizedActivation = normalizeFreeReusablePhoneActivation(activation); + if (!normalizedActivation) { + return { + ok: false, + reason: 'missing_free_reusable_activation', + message: 'Free reusable phone activation is missing.', + }; + } + if (!String(normalizedActivation.activationId || '').trim()) { + return { + ok: false, + reason: 'missing_activation_id', + message: 'Saved free reusable phone has no HeroSMS activation ID; automatic free reuse cannot reactivate it.', + }; + } + + const statusAction = resolveActivationStatusAction(normalizedActivation); + const config = resolveHeroSmsPhoneConfig(state); + const start = Date.now(); + let lastStatus = ''; + let prepareRound = 0; + + while ( + Date.now() - start < FREE_PHONE_REUSE_PREPARE_TIMEOUT_MS + && prepareRound < FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS + ) { + throwIfStopped(); + prepareRound += 1; + + try { + await setPhoneActivationStatus( + { ...state, phoneSmsProvider: PHONE_SMS_PROVIDER_HERO }, + normalizedActivation, + 3, + 'HeroSMS setStatus(3) for automatic free reuse' + ); + } catch (error) { + return { + ok: false, + reason: 'set_status_failed', + message: error.message || 'HeroSMS setStatus(3) failed.', + lastStatus, + prepareRound, + }; + } + + await addLog( + `步骤 9:自动白嫖复用已刷新 ${normalizedActivation.phoneNumber},${Math.ceil(FREE_PHONE_REUSE_PREPARE_INTERVAL_MS / 1000)} 秒后检查等待状态(${prepareRound}/${FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS})。`, + 'info' + ); + await sleepWithStop(FREE_PHONE_REUSE_PREPARE_INTERVAL_MS); + + try { + const payload = await fetchHeroSmsPayload(config, { + action: statusAction, + id: normalizedActivation.activationId, + }, `HeroSMS ${statusAction} for automatic free reuse`); + const statusText = describeHeroSmsPayload(payload); + lastStatus = statusText; + await addLog( + `步骤 9:自动白嫖复用号码 ${normalizedActivation.phoneNumber} 状态:${statusText || 'empty response'}(${prepareRound}/${FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS})。`, + 'info' + ); + + const v2Waiting = statusAction === 'getStatusV2' + && payload + && typeof payload === 'object' + && !Array.isArray(payload) + && !payload.sms?.code + && !payload.call?.code; + if (isHeroSmsReadyForFreshSmsText(statusText) || isHeroSmsWaitingStatusText(statusText) || v2Waiting) { + return { + ok: true, + activation: { + ...normalizedActivation, + source: 'free-auto-reuse', + }, + }; + } + if (/^STATUS_OK:/i.test(statusText)) { + await addLog( + `步骤 9:自动白嫖复用仍看到旧验证码,将再次刷新等待短信状态。`, + 'warn' + ); + continue; + } + if (isHeroSmsCancelledStatusText(statusText)) { + return { + ok: false, + reason: 'activation_cancelled', + message: 'HeroSMS activation was cancelled before automatic free reuse.', + lastStatus, + prepareRound, + }; + } + } catch (error) { + return { + ok: false, + reason: 'get_status_failed', + message: error.message || 'HeroSMS getStatus failed.', + lastStatus, + prepareRound, + }; + } + } + + return { + ok: false, + reason: 'prepare_timeout', + message: `Timed out waiting for saved phone to enter SMS waiting state. Last status: ${lastStatus || 'unknown'}.`, + lastStatus, + prepareRound, + }; + } + async function pollPhoneActivationCode(state = {}, activation, options = {}) { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { @@ -3777,6 +4137,71 @@ 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('signup-page', { + 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 (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 = normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER); + return provider === PHONE_SMS_PROVIDER_HERO || provider === PHONE_SMS_PROVIDER_NEXSMS || provider === PHONE_SMS_PROVIDER_5SIM; + } + async function persistCurrentActivation(activation) { const normalizedActivation = normalizeActivation(activation); const updates = { @@ -3843,6 +4268,67 @@ await persistReusableActivation(null); } + async function handoffFreeReusablePhone(tabId, state = {}) { + 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; + } + + if (normalizeFreePhoneReuseAutoEnabled(state)) { + 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) { @@ -4144,6 +4630,8 @@ ...normalizedActivation, successfulUses, }; + delete nextReusableActivation.phoneCodeReceived; + delete nextReusableActivation.phoneCodeReceivedAt; await upsertReusableActivationPool(nextReusableActivation, { state }); if (!normalizeHeroSmsReuseEnabled(state?.heroSmsReuseEnabled)) { await clearReusableActivation(); @@ -4158,6 +4646,119 @@ await persistReusableActivation(nextReusableActivation); } + function shouldPreserveActivationForFreeReuse(state, activation) { + if (!normalizeFreePhoneReuseEnabled(state?.freePhoneReuseEnabled)) { + return false; + } + const normalizedActivation = normalizeActivation(activation); + return Boolean( + normalizedActivation + && normalizedActivation.provider === PHONE_SMS_PROVIDER_HERO + && normalizedActivation.source === 'hero-sms-new' + && normalizedActivation.phoneCodeReceived + ); + } + + function shouldSkipTerminalStatusForFreeReuse(state, activation) { + const normalizedActivation = normalizeActivation(activation); + if (!normalizedActivation || normalizedActivation.provider !== PHONE_SMS_PROVIDER_HERO) { + return false; + } + if (isFreeAutoReuseActivation(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 (!normalizeFreePhoneReuseEnabled(latestState?.freePhoneReuseEnabled)) { + return; + } + if (normalizeFreeReusablePhoneActivation(latestState[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY])) { + return; + } + const normalizedActivation = normalizeActivation(activation); + if ( + !normalizedActivation + || normalizedActivation.provider !== PHONE_SMS_PROVIDER_HERO + || !normalizedActivation.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() : {}), + }; + 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 waitForPhoneCodeOrRotateNumber(tabId, state, activation) { const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation) { @@ -4191,6 +4792,24 @@ 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 could not send SMS to this phone number.'}`); + } + if (pageError?.reason === 'phone_max_usage_exceeded') { + throw buildPhoneMaxUsageExceededError(pageError.message); + } + if (pageError?.reason === 'resend_throttled') { + if (shouldTreatResendThrottledAsBanned(state)) { + throw buildHighRiskResendThrottledError(pageError.message); + } + await addLog( + `步骤 9:检测到号码 ${normalizedActivation.phoneNumber} 重发限流,但未启用“按疑似封禁处理”,继续等待短信。${pageError.message || ''}`.trim(), + 'warn' + ); + } + } const shouldLog = ( pollCount === 1 || statusText !== lastLoggedStatus @@ -4214,6 +4833,50 @@ }; } 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 (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( `Step 9: ${providerLabel} activation for ${normalizedActivation.phoneNumber} became invalid (${error.message || error}), replacing number immediately.`, @@ -4257,6 +4920,18 @@ 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}`, @@ -4266,7 +4941,9 @@ return { code: '', replaceNumber: true, - reason: 'resend_throttled', + reason: shouldTreatResendThrottledAsBanned(state) + ? 'resend_throttled_high_risk_banned' + : 'resend_throttled', }; } await addLog(`步骤 9:点击手机验证码页面重发按钮失败。${resendError.message}`, 'warn'); @@ -5101,13 +5778,18 @@ ); } if (!activation) { - activation = await acquirePhoneActivation(state, { - blockedCountryIds: getBlockedCountryIds(), - countryPriceFloorByCountryId: getCountryPriceFloorById(), - skipPreferredActivation: preferredActivationExhausted, - }); - shouldCancelActivation = true; - await persistCurrentActivation(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; @@ -5120,6 +5802,11 @@ `步骤 9:当前号码 ${activation.phoneNumber} 反复返回添加手机号页,正在更换号码(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`, 'warn' ); + if (isFreeAutoReuseActivation(activation)) { + await retireFreeReusableActivation( + `自动白嫖复用号码 ${activation.phoneNumber} 反复返回添加手机号页。` + ); + } if (shouldCancelActivation && activation) { await cancelPhoneActivation(state, activation); } @@ -5170,6 +5857,16 @@ `步骤 9:添加手机号页面提示 ${activation.phoneNumber} 已被使用(${addPhoneRejectText}),正在更换号码(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`, 'warn' ); + await discardPhoneActivationFromReuse( + `目标站拒绝该号码(${addPhoneRejectText})。`, + activation, + await getState() + ); + if (isFreeAutoReuseActivation(activation)) { + await retireFreeReusableActivation( + `自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。` + ); + } if (shouldCancelActivation && activation) { await banPhoneActivation(state, activation); } @@ -5258,6 +5955,12 @@ 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); @@ -5281,6 +5984,16 @@ if (isPhoneNumberUsedError(invalidErrorText)) { shouldReplaceNumber = true; replaceReason = 'phone_number_used'; + await discardPhoneActivationFromReuse( + `目标站拒绝该号码(${invalidErrorText})。`, + activation, + await getState() + ); + if (isFreeAutoReuseActivation(activation)) { + await retireFreeReusableActivation( + `自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。` + ); + } if (shouldCancelActivation && activation) { await banPhoneActivation(state, activation); shouldCancelActivation = false; @@ -5331,8 +6044,19 @@ continue; } - await completePhoneActivation(state, activation); - await markActivationReusableAfterSuccess(state, activation); + const latestSuccessState = await getState(); + if (shouldSkipTerminalStatusForFreeReuse(latestSuccessState, activation)) { + await addLog( + `步骤 9:已跳过 HeroSMS 完成状态,保留 ${activation.phoneNumber} 供白嫖复用。`, + 'info' + ); + } 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(); @@ -5372,6 +6096,18 @@ if (shouldCancelActivation && activation) { await cancelPhoneActivation(state, activation); } + if (isFreeAutoReuseActivation(activation)) { + await retireFreeReusableActivation( + `自动白嫖复用号码 ${activation.phoneNumber} 在失败后被更换。` + ); + } + if (isPhoneNumberUsedError(replaceReason)) { + await discardPhoneActivationFromReuse( + `目标站拒绝该号码(${replaceReason})。`, + activation, + await getState() + ); + } await clearCurrentActivation(); activation = null; shouldCancelActivation = false; @@ -5430,8 +6166,20 @@ }; } } 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 (isFreeAutoReuseActivation(activation)) { + await retireFreeReusableActivation( + `自动白嫖复用号码 ${activation.phoneNumber} 执行失败:${errorMessage || 'unknown error'}。` + ); + } if (shouldCancelActivation && activation) { - await cancelPhoneActivation(state, activation); + await cancelPhoneActivation(await getState(), activation); } await clearCurrentActivation(); throw sanitizePhoneRestartStep7Error(sanitizePhoneCodeTimeoutError(error)); diff --git a/background/steps/fill-plus-checkout.js b/background/steps/fill-plus-checkout.js index 89133ec..78168f2 100644 --- a/background/steps/fill-plus-checkout.js +++ b/background/steps/fill-plus-checkout.js @@ -423,8 +423,7 @@ } await addLog(`步骤 7:GPC 模式开始 OTP 验证(reference_id: ${referenceId})...`, 'info'); let otp = ''; - const useLocalSmsHelper = Boolean(state?.gopayHelperLocalSmsHelperEnabled) - && normalizeGpcOtpChannel(state?.gopayHelperOtpChannel) === 'sms'; + const useLocalSmsHelper = Boolean(state?.gopayHelperLocalSmsHelperEnabled); if (useLocalSmsHelper) { try { await addLog('步骤 7:正在从本地 SMS Helper 等待 GPC OTP...', 'info'); diff --git a/background/steps/platform-verify.js b/background/steps/platform-verify.js index 8237339..e9fb06c 100644 --- a/background/steps/platform-verify.js +++ b/background/steps/platform-verify.js @@ -380,6 +380,7 @@ sub2apiPassword: state.sub2apiPassword, sub2apiGroupName: state.sub2apiGroupName, sub2apiDefaultProxyName: state.sub2apiDefaultProxyName, + sub2apiAccountPriority: state.sub2apiAccountPriority, sub2apiProxyId: state.sub2apiProxyId, sub2apiSessionId: state.sub2apiSessionId, sub2apiOAuthState: state.sub2apiOAuthState, diff --git a/content/icloud-mail.js b/content/icloud-mail.js index e6befa7..8f434ad 100644 --- a/content/icloud-mail.js +++ b/content/icloud-mail.js @@ -307,9 +307,13 @@ if (shouldHandlePollEmailInCurrentFrame) { const items = collectThreadItems(); const useFallback = (fallbackCarry + attempt) > FALLBACK_AFTER; - for (const item of items) { + for (const [index, item] of items.entries()) { const signature = buildItemSignature(item); - if (!useFallback && existingSignatures.has(signature)) { + const allowInitialStep8VisibleCode = Number(step) === 8 + && attempt === 1 + && index === 0 + && !sessionBaseline.fromCache; + if (!useFallback && existingSignatures.has(signature) && !allowInitialStep8VisibleCode) { continue; } diff --git a/content/phone-auth.js b/content/phone-auth.js index 84804aa..be79514 100644 --- a/content/phone-auth.js +++ b/content/phone-auth.js @@ -19,11 +19,14 @@ waitForElement, } = deps; const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::'; + const PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX = 'PHONE_RESEND_BANNED_NUMBER::'; + const PHONE_MAX_USAGE_EXCEEDED_PATTERN = /phone_max_usage_exceeded/i; const PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX = 'PHONE_ROUTE_405_RECOVERY_FAILED::'; const PHONE_ROUTE_405_RECOVERY_COOLDOWN_MS = 6000; const PHONE_RESEND_ROUTE_405_MAX_RECOVERIES = 2; const PHONE_RESEND_ROUTE_405_MAX_RECOVERY_TOTAL_MS = 12000; const PHONE_RESEND_THROTTLED_PATTERN = /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; + const PHONE_RESEND_BANNED_NUMBER_PATTERN = /无法向此电话号码发送短信|无法向此手机号发送短信|无法发送短信到此电话号码|无法发送短信到此手机号|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; const PHONE_ROUTE_405_PATTERN = /405\s+method\s+not\s+allowed|route\s+error.*405|did\s+not\s+provide\s+an?\s+[`'"]?action|post\s+request\s+to\s+["']?\/phone-verification/i; const PHONE_ROUTE_405_MAX_RECOVERY_CLICKS = 3; const rootScope = typeof self !== 'undefined' ? self : globalThis; @@ -493,6 +496,63 @@ return ''; } + function getPhoneResendBannedNumberText() { + const inlineMatch = getPhoneVerificationInlineMessages() + .find((text) => PHONE_RESEND_BANNED_NUMBER_PATTERN.test(text)); + if (inlineMatch) { + return inlineMatch; + } + const pageSnapshot = String(getPageTextSnapshot?.() || '').replace(/\s+/g, ' ').trim(); + if (pageSnapshot && PHONE_RESEND_BANNED_NUMBER_PATTERN.test(pageSnapshot)) { + const concise = pageSnapshot.match( + /无法向此电话号码发送短信|无法向此手机号发送短信|无法发送短信到此电话号码|无法发送短信到此手机号|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 + ); + return String(concise?.[0] || pageSnapshot).trim(); + } + return ''; + } + + function checkPhoneResendError() { + const maxUsageText = getAddPhoneErrorText(); + if (maxUsageText && PHONE_MAX_USAGE_EXCEEDED_PATTERN.test(maxUsageText)) { + return { + hasError: true, + reason: 'phone_max_usage_exceeded', + message: maxUsageText, + url: location.href, + }; + } + + const bannedNumberText = getPhoneResendBannedNumberText(); + if (bannedNumberText) { + return { + hasError: true, + reason: 'resend_phone_banned', + prefix: PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX, + message: bannedNumberText, + url: location.href, + }; + } + + const throttledText = getPhoneResendThrottleText(); + if (throttledText) { + return { + hasError: true, + reason: 'resend_throttled', + prefix: PHONE_RESEND_THROTTLED_ERROR_PREFIX, + message: throttledText, + url: location.href, + }; + } + + return { + hasError: false, + reason: '', + message: '', + url: location.href, + }; + } + function getAuthRetryButton(options = {}) { const { allowDisabled = false } = options; const direct = document.querySelector('button[data-dd-action-name="Try again"]'); @@ -779,6 +839,10 @@ await recoverRoute405WithinResend(); continue; } + const bannedNumberText = getPhoneResendBannedNumberText(); + if (bannedNumberText) { + throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${bannedNumberText}`); + } const throttledText = getPhoneResendThrottleText(); if (throttledText) { throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${throttledText}`); @@ -792,6 +856,10 @@ await recoverRoute405WithinResend(); continue; } + const afterClickBannedNumberText = getPhoneResendBannedNumberText(); + if (afterClickBannedNumberText) { + throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${afterClickBannedNumberText}`); + } const afterClickThrottleText = getPhoneResendThrottleText(); if (afterClickThrottleText) { throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${afterClickThrottleText}`); @@ -804,6 +872,11 @@ await sleep(250); } + const timeoutBannedNumberText = getPhoneResendBannedNumberText(); + if (timeoutBannedNumberText) { + throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${timeoutBannedNumberText}`); + } + const timeoutThrottleText = getPhoneResendThrottleText(); if (timeoutThrottleText) { throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${timeoutThrottleText}`); @@ -839,6 +912,7 @@ return { getPhoneVerificationDisplayedPhone, + checkPhoneResendError, isPhoneVerificationPageReady, resendPhoneVerificationCode, returnToAddPhone, diff --git a/content/signup-page.js b/content/signup-page.js index 11d395a..797236a 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -25,6 +25,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1' || message.type === 'SUBMIT_PHONE_NUMBER' || message.type === 'SUBMIT_PHONE_VERIFICATION_CODE' || message.type === 'RESEND_PHONE_VERIFICATION_CODE' + || message.type === 'CHECK_PHONE_RESEND_ERROR' || message.type === 'RETURN_TO_ADD_PHONE' || message.type === 'ENSURE_SIGNUP_ENTRY_READY' || message.type === 'ENSURE_SIGNUP_PHONE_ENTRY_READY' @@ -97,6 +98,8 @@ async function handleCommand(message) { return await submitPhoneVerificationCodeWithProfileFallback(message.payload); case 'RESEND_PHONE_VERIFICATION_CODE': return await phoneAuthHelpers.resendPhoneVerificationCode(); + case 'CHECK_PHONE_RESEND_ERROR': + return phoneAuthHelpers.checkPhoneResendError(); case 'RETURN_TO_ADD_PHONE': return await phoneAuthHelpers.returnToAddPhone(); case 'ENSURE_SIGNUP_ENTRY_READY': @@ -2948,6 +2951,7 @@ const phoneAuthHelpers = self.MultiPagePhoneAuth?.createPhoneAuthHelpers?.({ resendPhoneVerificationCode: async () => { throw new Error('Phone auth helpers are unavailable.'); }, + checkPhoneResendError: () => ({ hasError: false, reason: '', message: '', url: location.href }), returnToAddPhone: async () => { throw new Error('Phone auth helpers are unavailable.'); }, diff --git a/content/sub2api-panel.js b/content/sub2api-panel.js index 1bf82b7..8f5c39c 100644 --- a/content/sub2api-panel.js +++ b/content/sub2api-panel.js @@ -252,6 +252,21 @@ function resolveSub2ApiProxyPreference(payload = {}, backgroundState = {}) { return SUB2API_DEFAULT_PROXY_NAME; } +function resolveSub2ApiAccountPriority(payload = {}, backgroundState = {}) { + const candidate = payload.sub2apiAccountPriority !== undefined + ? payload.sub2apiAccountPriority + : backgroundState.sub2apiAccountPriority; + const rawValue = String(candidate ?? '').trim(); + if (!rawValue) { + return SUB2API_DEFAULT_PRIORITY; + } + const numeric = Number(rawValue); + if (!Number.isSafeInteger(numeric) || numeric < 1) { + throw new Error('SUB2API 账号优先级必须是大于等于 1 的整数。'); + } + return numeric; +} + function normalizeProxyId(value) { if (value === undefined || value === null || value === '') { return null; @@ -568,6 +583,7 @@ async function step9_submitOpenAiCallback(payload = {}) { const proxySelector = preferredProxyId || proxyPreference; const proxy = proxySelector ? await resolveSub2ApiProxy(origin, token, proxySelector) : null; const proxyId = normalizeProxyId(proxy?.id); + const accountPriority = resolveSub2ApiAccountPriority(payload, backgroundState); const storedGroupIds = Array.isArray(payload.sub2apiGroupIds) ? payload.sub2apiGroupIds : (Array.isArray(backgroundState.sub2apiGroupIds) ? backgroundState.sub2apiGroupIds : []); @@ -627,7 +643,7 @@ async function step9_submitOpenAiCallback(payload = {}) { type: 'oauth', credentials, concurrency: SUB2API_DEFAULT_CONCURRENCY, - priority: SUB2API_DEFAULT_PRIORITY, + priority: accountPriority, rate_multiplier: SUB2API_DEFAULT_RATE_MULTIPLIER, group_ids: groupIds, auto_pause_on_expired: true, diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 6004558..2c4fc3f 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -2367,6 +2367,20 @@ header { min-height: 33px; } +.hero-sms-free-phone-cell { + flex-wrap: wrap; +} + +.hero-sms-manual-phone-input { + flex: 1 1 150px; + min-width: 120px; +} + +.hero-sms-free-phone-country { + flex: 0 0 auto; + white-space: nowrap; +} + .signup-phone-runtime-inline, .signup-phone-runtime-input { width: 100%; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 1584dd9..afe062d 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -210,6 +210,10 @@ + @@ -1467,6 +1471,28 @@ + + @@ -1487,6 +1513,14 @@ 验证码 未获取 +