Files
FlowPilot/background/phone-verification-flow.js
T
2026-05-28 12:41:57 +08:00

7315 lines
293 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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_BASE_URL = 'https://hero-sms.com/stubs/handler_api.php',
DEFAULT_FIVE_SIM_BASE_URL = 'https://5sim.net/v1',
DEFAULT_FIVE_SIM_PRODUCT = 'openai',
DEFAULT_FIVE_SIM_OPERATOR = 'any',
DEFAULT_HERO_SMS_OPERATOR = 'any',
DEFAULT_FIVE_SIM_COUNTRY_ORDER = ['thailand'],
DEFAULT_NEX_SMS_BASE_URL = 'https://api.nexsms.net',
DEFAULT_NEX_SMS_COUNTRY_ORDER = [1],
DEFAULT_NEX_SMS_SERVICE_CODE = 'ot',
DEFAULT_HERO_SMS_REUSE_ENABLED = true,
createFiveSimProvider = null,
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 HERO_SMS_LAST_PRICE_TIERS_KEY = 'heroSmsLastPriceTiers';
const HERO_SMS_LAST_PRICE_COUNTRY_ID_KEY = 'heroSmsLastPriceCountryId';
const HERO_SMS_LAST_PRICE_COUNTRY_LABEL_KEY = 'heroSmsLastPriceCountryLabel';
const HERO_SMS_LAST_PRICE_USER_LIMIT_KEY = 'heroSmsLastPriceUserLimit';
const HERO_SMS_LAST_PRICE_AT_KEY = 'heroSmsLastPriceAt';
const FIVE_SIM_RATE_LIMIT_ERROR_PREFIX = 'FIVE_SIM_RATE_LIMIT::';
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 DEFAULT_PHONE_PRICE_LOOKUP_ATTEMPTS = 3;
const MAX_PHONE_PRICE_CANDIDATES = 8;
const DEFAULT_PHONE_ACTIVATION_RETRY_ROUNDS = 3;
const PHONE_ACTIVATION_RETRY_ROUNDS_MIN = 1;
const PHONE_ACTIVATION_RETRY_ROUNDS_MAX = 10;
const DEFAULT_PHONE_ACTIVATION_RETRY_DELAY_MS = 2000;
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 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,
]);
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, label: 'Vietnam' },
{ prefix: '66', id: 52, label: 'Thailand' },
{ prefix: '62', id: 6, label: 'Indonesia' },
{ prefix: '44', id: 16, label: 'United Kingdom' },
{ prefix: '81', id: 151, label: 'Japan' },
{ prefix: '49', id: 43, label: 'Germany' },
{ prefix: '33', id: 73, label: 'France' },
{ prefix: '1', id: 187, label: 'USA' },
]);
const activationPriceHintsByKey = 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 normalizeUrl(value, fallback = DEFAULT_HERO_SMS_BASE_URL) {
const trimmed = String(value || '').trim();
if (!trimmed) {
return fallback;
}
try {
return new URL(trimmed).toString();
} catch {
return fallback;
}
}
function normalizeApiKey(value) {
return String(value || '').trim();
}
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 === PHONE_SMS_PROVIDER_NEXSMS) {
return PHONE_SMS_PROVIDER_NEXSMS;
}
return PHONE_SMS_PROVIDER_HERO;
}
function isFiveSimProvider(state = {}) {
return normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER) === PHONE_SMS_PROVIDER_5SIM;
}
function normalizeNexSmsCountryId(value, fallback = 0) {
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 normalizeNexSmsCountryOrder(value = []) {
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 id = normalizeNexSmsCountryId(
entry && typeof entry === 'object' && !Array.isArray(entry)
? (entry.id || entry.countryId || entry.country || '')
: entry,
-1
);
if (id < 0 || seen.has(id)) {
return;
}
seen.add(id);
normalized.push(id);
});
return normalized.slice(0, 10);
}
function resolveNexSmsCountryCandidates(state = {}) {
const ids = normalizeNexSmsCountryOrder(state?.nexSmsCountryOrder);
return ids.map((id) => ({
id,
label: `Country #${id}`,
}));
}
function normalizeNexSmsServiceCode(value = '', fallback = DEFAULT_NEX_SMS_SERVICE_CODE) {
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 || 'ot';
}
function normalizeFiveSimCountryCode(value = '', fallback = 'thailand') {
const normalized = String(value || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9_-]/g, '');
return normalized || fallback;
}
function normalizeFiveSimCountryOrder(value = []) {
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 code = normalizeFiveSimCountryCode(
entry && typeof entry === 'object' && !Array.isArray(entry)
? (entry.code || entry.country || entry.id || '')
: entry,
''
);
if (!code || seen.has(code)) {
return;
}
seen.add(code);
normalized.push(code);
});
return normalized.slice(0, 10);
}
function resolveFiveSimCountryCandidates(state = {}) {
let codes = normalizeFiveSimCountryOrder(state?.fiveSimCountryOrder);
if (!codes.length) {
const legacyPrimary = normalizeFiveSimCountryCode(state?.fiveSimCountryId, '');
const legacyFallback = normalizeFiveSimCountryOrder(state?.fiveSimCountryFallback);
codes = normalizeFiveSimCountryOrder([
...(legacyPrimary ? [legacyPrimary] : []),
...legacyFallback,
]);
}
return codes.map((code) => ({
code,
id: code,
label: (
code === normalizeFiveSimCountryCode(state?.fiveSimCountryId, '')
? normalizeCountryLabel(state?.fiveSimCountryLabel, code)
: code
),
}));
}
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 normalizePhoneActivationRetryRounds(value) {
const parsed = Math.floor(Number(value));
if (!Number.isFinite(parsed) || parsed <= 0) {
return DEFAULT_PHONE_ACTIVATION_RETRY_ROUNDS;
}
return Math.max(PHONE_ACTIVATION_RETRY_ROUNDS_MIN, Math.min(PHONE_ACTIVATION_RETRY_ROUNDS_MAX, parsed));
}
function normalizePhoneActivationRetryDelayMs(value) {
const parsed = Math.floor(Number(value));
if (!Number.isFinite(parsed) || parsed <= 0) {
return DEFAULT_PHONE_ACTIVATION_RETRY_DELAY_MS;
}
return Math.max(500, Math.min(30000, parsed));
}
function assertFiveSimMaxPriceCompatibleWithOperator(operator, maxPriceLimit) {
const normalizedOperator = normalizeFiveSimCountryCode(operator, DEFAULT_FIVE_SIM_OPERATOR);
if (maxPriceLimit !== null && maxPriceLimit !== undefined && normalizedOperator !== DEFAULT_FIVE_SIM_OPERATOR) {
throw new Error('5sim 价格上限仅支持运营商为 "any" 时使用;请清空价格上限,或先把运营商切换为 any。');
}
}
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 resolvePhonePriceRange(state = {}, provider = DEFAULT_PHONE_SMS_PROVIDER) {
const normalizedProvider = normalizePhoneSmsProvider(provider);
const minPriceLimit = normalizedProvider === PHONE_SMS_PROVIDER_5SIM
? normalizeHeroSmsPriceLimit(state?.fiveSimMinPrice)
: normalizeHeroSmsPriceLimit(state?.heroSmsMinPrice);
const maxPriceLimit = normalizedProvider === PHONE_SMS_PROVIDER_5SIM
? normalizeHeroSmsPriceLimit(state?.fiveSimMaxPrice)
: normalizeHeroSmsPriceLimit(state?.heroSmsMaxPrice);
return {
provider: normalizedProvider,
minPriceLimit,
maxPriceLimit,
hasMinPriceLimit: minPriceLimit !== null,
hasMaxPriceLimit: maxPriceLimit !== null,
invalidRange: minPriceLimit !== null && maxPriceLimit !== null && minPriceLimit > maxPriceLimit,
};
}
function isPriceWithinRange(price, minPriceLimit = null, maxPriceLimit = null) {
const numeric = Number(price);
if (!Number.isFinite(numeric) || numeric <= 0) {
return false;
}
const normalized = Math.round(numeric * 10000) / 10000;
if (minPriceLimit !== null && normalized < minPriceLimit) {
return false;
}
if (maxPriceLimit !== null && normalized > maxPriceLimit) {
return false;
}
return true;
}
function filterPriceCandidatesWithinRange(prices = [], minPriceLimit = null, maxPriceLimit = null) {
return (Array.isArray(prices) ? prices : []).filter((price) => (
isPriceWithinRange(price, minPriceLimit, maxPriceLimit)
));
}
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 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),
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, 3);
}
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, 3);
}
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, 3);
}
function reorderPriceCandidates(prices = [], acquirePriority = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY, preferredPrice = null) {
const hasNullTier = Array.isArray(prices)
&& prices.some((value) => value === null || value === undefined || String(value).trim() === '');
const normalized = Array.from(
new Set(
(Array.isArray(prices) ? prices : [])
.map((value) => Number(value))
.filter((value) => Number.isFinite(value) && value > 0)
.map((value) => Math.round(value * 10000) / 10000)
)
).sort((left, right) => left - right);
const ordered = acquirePriority === HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH
? normalized.reverse()
: normalized;
const preferred = Number(preferredPrice);
if (!Number.isFinite(preferred) || preferred <= 0) {
if (ordered.length) {
return ordered;
}
return hasNullTier ? [null] : [];
}
const normalizedPreferred = Math.round(preferred * 10000) / 10000;
const withoutPreferred = ordered.filter((value) => value !== normalizedPreferred);
return [normalizedPreferred, ...withoutPreferred];
}
function filterPriceCandidatesAboveFloor(prices = [], minExclusivePrice = null) {
const floor = normalizeHeroSmsPrice(minExclusivePrice);
if (floor === null || floor <= 0) {
return Array.isArray(prices) ? [...prices] : [];
}
return (Array.isArray(prices) ? prices : []).filter((value) => {
const numeric = Number(value);
if (!Number.isFinite(numeric) || numeric <= 0) {
return false;
}
const normalized = Math.round(numeric * 10000) / 10000;
return normalized > floor;
});
}
function shouldUseHeroSmsExpandedPriceLookup(state = {}) {
if (typeof state?.heroSmsUseExpandedPriceLookup === 'boolean') {
return state.heroSmsUseExpandedPriceLookup;
}
const runningInNode = (
typeof process !== 'undefined'
&& process
&& process.versions
&& process.versions.node
);
// Runtime default: enabled in extension/browser; tests in Node can opt-in explicitly.
return !runningInNode;
}
async function fetchHeroSmsPricePayloads(config, countryConfig, options = {}) {
const payloads = [];
const errors = [];
const actions = Array.isArray(options.actions) && options.actions.length
? options.actions
: (
shouldUseHeroSmsExpandedPriceLookup(options.state || {})
? ['getPricesExtended', 'getPrices']
: ['getPrices']
);
for (const action of actions) {
try {
const query = {
action,
service: HERO_SMS_SERVICE_CODE,
country: countryConfig.id,
};
if (action === 'getPricesExtended') {
query.freePrice = 'true';
}
const payload = await fetchHeroSmsPayload(config, query, `HeroSMS ${action}`);
payloads.push(payload);
} catch (error) {
errors.push({
action,
message: describeHeroSmsPayload(error?.payload || error?.message || ''),
});
}
}
return {
payloads,
errors,
};
}
function collectHeroSmsPriceCandidatesIncludingZeroStock(payload, candidates = []) {
if (Array.isArray(payload)) {
payload.forEach((entry) => collectHeroSmsPriceCandidatesIncludingZeroStock(entry, candidates));
return candidates;
}
if (!payload || typeof payload !== 'object') {
return candidates;
}
const cost = normalizeHeroSmsPrice(payload.cost);
if (cost !== null) {
candidates.push(cost);
}
Object.entries(payload).forEach(([key]) => {
const keyedPrice = normalizeHeroSmsPrice(key);
if (keyedPrice !== null) {
const value = payload[key];
if (value && typeof value === 'object') {
const stockState = resolveHeroSmsStockState(value);
if (stockState.hasStockField) {
candidates.push(keyedPrice);
}
return;
}
const numericCount = Number(value);
if (Number.isFinite(numericCount)) {
candidates.push(keyedPrice);
}
}
});
Object.values(payload).forEach((value) => collectHeroSmsPriceCandidatesIncludingZeroStock(value, candidates));
return candidates;
}
async function resolveHeroSmsPricePlanFromPricePayloads(config, countryConfig, state = {}, payloads = []) {
const userLimit = normalizeHeroSmsPriceLimit(state.heroSmsMaxPrice);
const inStockCandidates = buildSortedUniquePriceCandidates(
(Array.isArray(payloads) ? payloads : [])
.flatMap((payload) => collectHeroSmsPriceCandidates(payload, []))
);
const allCatalogCandidates = buildSortedUniquePriceCandidates(
(Array.isArray(payloads) ? payloads : [])
.flatMap((payload) => collectHeroSmsPriceCandidatesIncludingZeroStock(payload, []))
);
const mergedCandidates = inStockCandidates.length
? buildSortedUniquePriceCandidates([
...inStockCandidates,
...allCatalogCandidates,
])
: [];
const minCatalogPrice = allCatalogCandidates.length
? allCatalogCandidates[0]
: (mergedCandidates.length ? mergedCandidates[0] : null);
if (userLimit !== null) {
const bounded = mergedCandidates.filter((price) => price <= userLimit);
if (bounded.length > 0) {
const boundedPlan = {
prices: bounded,
userLimit,
minCatalogPrice,
syntheticUserLimitProbe: false,
};
await persistHeroSmsPricePlanSnapshot(countryConfig, boundedPlan);
return boundedPlan;
}
const userLimitedPlan = {
prices: [userLimit],
userLimit,
minCatalogPrice,
syntheticUserLimitProbe: true,
};
await persistHeroSmsPricePlanSnapshot(countryConfig, userLimitedPlan);
return userLimitedPlan;
}
if (mergedCandidates.length > 0) {
const plan = {
prices: mergedCandidates,
userLimit: null,
minCatalogPrice,
syntheticUserLimitProbe: false,
};
await persistHeroSmsPricePlanSnapshot(countryConfig, plan);
return plan;
}
const fallbackPlan = {
prices: [null],
userLimit: null,
minCatalogPrice: null,
syntheticUserLimitProbe: false,
};
await persistHeroSmsPricePlanSnapshot(countryConfig, fallbackPlan);
return fallbackPlan;
}
function normalizeCountryPriceFloorMap(rawMap = {}, normalizeCountryKey) {
const normalizedMap = new Map();
if (!rawMap || typeof rawMap !== 'object') {
return normalizedMap;
}
Object.entries(rawMap).forEach(([rawCountryKey, rawPrice]) => {
const countryKey = String(
typeof normalizeCountryKey === 'function'
? normalizeCountryKey(rawCountryKey)
: rawCountryKey
).trim();
if (!countryKey) {
return;
}
const normalizedPrice = normalizeHeroSmsPrice(rawPrice);
if (normalizedPrice === null || normalizedPrice <= 0) {
return;
}
normalizedMap.set(countryKey, Math.round(normalizedPrice * 10000) / 10000);
});
return normalizedMap;
}
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';
}
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 formatPhoneSmsActionLabel(actionLabel = '') {
const text = String(actionLabel || '').trim();
if (!text) {
return '接码平台请求';
}
const normalized = text.toLowerCase();
const heroMatch = text.match(/^HeroSMS\s+(.+)$/i);
if (heroMatch) {
return `HeroSMS ${formatHeroSmsActionName(heroMatch[1])}`;
}
if (normalized === '5sim guest prices') {
return '5sim 查询游客价格';
}
if (normalized === '5sim user prices') {
return '5sim 查询账号价格';
}
if (normalized === '5sim buy activation') {
return '5sim 购买手机号';
}
if (normalized === '5sim check activation') {
return '5sim 查询短信状态';
}
if (normalized === '5sim reuse activation') {
return '5sim 复用手机号';
}
if (normalized === 'nexsms getcountrybyservice') {
return 'NexSMS 查询服务国家';
}
if (normalized === 'nexsms price lookup') {
return 'NexSMS 查询价格';
}
if (normalized === 'nexsms purchase') {
return 'NexSMS 购买手机号';
}
if (normalized === 'nexsms close activation') {
return 'NexSMS 关闭订单';
}
if (normalized === 'nexsms get sms messages') {
return 'NexSMS 查询短信';
}
return text;
}
function createPhoneSmsActionFailureError(actionLabel, reason = '', payload = null, status = 0) {
const message = `${formatPhoneSmsActionLabel(actionLabel)}失败:${formatPhoneSmsApiFailureReason(reason || status)}`;
const error = new Error(message);
if (payload !== null && payload !== undefined) {
error.payload = payload;
}
if (status) {
error.status = status;
}
return error;
}
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 createHeroSmsActionFailureError(action, reason = '') {
const normalizedReason = stripRepeatedHeroSmsFailurePrefix(action, describeHeroSmsPayload(reason));
const error = new Error(`HeroSMS ${formatHeroSmsActionName(action)}失败:${formatPhoneSmsApiFailureReason(normalizedReason)}`);
error.localizedPhoneSmsFailure = true;
return error;
}
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 createResolvedFiveSimProvider() {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
const factory = createFiveSimProvider || rootScope.PhoneSmsFiveSimProvider?.createProvider;
if (typeof factory !== 'function') {
return null;
}
return factory({
addLog,
fetchImpl,
requestTimeoutMs: DEFAULT_PHONE_REQUEST_TIMEOUT_MS,
sleepWithStop,
throwIfStopped,
});
}
function getFiveSimProviderForState(_state = {}) {
return createResolvedFiveSimProvider();
}
function normalizeFiveSimCountryId(value, fallback = 'england') {
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
return normalized || fallback;
}
function normalizeFiveSimCountryLabel(value = '', fallback = '英国 (England)') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryLabel) {
return rootScope.PhoneSmsFiveSimProvider.normalizeFiveSimCountryLabel(value, fallback);
}
if (rootScope.PhoneSmsFiveSimProvider?.formatFiveSimCountryLabel) {
return rootScope.PhoneSmsFiveSimProvider.formatFiveSimCountryLabel('', value, fallback);
}
return String(value || '').trim() || fallback;
}
function normalizeFiveSimCountryFallbackList(value = []) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryFallback) {
return rootScope.PhoneSmsFiveSimProvider.normalizeFiveSimCountryFallback(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 = '';
let label = '';
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
id = normalizeFiveSimCountryId(entry.id ?? entry.countryId ?? entry.slug, '');
label = String((entry.label ?? entry.countryLabel ?? entry.name ?? entry.text_en) || '').trim();
} else {
const text = String(entry || '').trim();
const structured = text.match(/^([a-z0-9_-]+)\s*(?:[:|/-]\s*(.+))?$/i);
id = normalizeFiveSimCountryId(structured?.[1] || text, '');
label = String(structured?.[2] || '').trim();
}
if (!id || seen.has(id)) continue;
seen.add(id);
normalized.push({ id, label: label || normalizeFiveSimCountryLabel('', id) });
}
return normalized;
}
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 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 rawProvider = String(record.provider || '').trim();
const provider = normalizePhoneSmsProvider(rawProvider);
const rawCountryId = record.countryId ?? record.country;
const fallbackCountryId = provider === PHONE_SMS_PROVIDER_FIVE_SIM ? 'england' : HERO_SMS_COUNTRY_ID;
const expiresAt = normalizeTimestampMs(record.expiresAt);
const serviceCode = String(
record.serviceCode
|| (
provider === PHONE_SMS_PROVIDER_FIVE_SIM
? DEFAULT_FIVE_SIM_PRODUCT
: (provider === PHONE_SMS_PROVIDER_NEXSMS ? DEFAULT_NEX_SMS_SERVICE_CODE : HERO_SMS_SERVICE_CODE)
)
).trim();
const countryId = provider === PHONE_SMS_PROVIDER_FIVE_SIM
? normalizeFiveSimCountryId(record.countryCode ?? rawCountryId, fallbackCountryId)
: (
provider === PHONE_SMS_PROVIDER_NEXSMS
? normalizeNexSmsCountryId(rawCountryId, 0)
: normalizeCountryId(rawCountryId, fallbackCountryId)
);
const ignoredPhoneCodeKeys = normalizeStringList(record.ignoredPhoneCodeKeys);
return {
activationId,
phoneNumber,
provider,
serviceCode,
countryId,
...(provider === PHONE_SMS_PROVIDER_FIVE_SIM ? { countryCode: 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 normalizeActivationFallback(record) {
if (!record || typeof record !== 'object' || Array.isArray(record)) {
return null;
}
const fallback = {};
const rawProvider = String(record.provider || '').trim();
const provider = rawProvider ? normalizePhoneSmsProvider(rawProvider) : '';
const serviceCode = String(record.serviceCode || '').trim();
const rawCountryId = record.countryId ?? record.country;
const countryId = provider === PHONE_SMS_PROVIDER_FIVE_SIM
? normalizeFiveSimCountryId(rawCountryId, '')
: Math.floor(Number(rawCountryId));
const countryLabel = String(record.countryLabel || '').trim();
const statusAction = String(record.statusAction || '').trim();
if (provider) {
fallback.provider = provider;
}
if (serviceCode) {
fallback.serviceCode = serviceCode;
}
if (provider === PHONE_SMS_PROVIDER_FIVE_SIM) {
if (countryId) {
fallback.countryId = countryId;
}
} else if (Number.isFinite(countryId) && countryId > 0) {
fallback.countryId = countryId;
if (provider === PHONE_SMS_PROVIDER_5SIM) {
fallback.countryCode = countryId;
}
}
if (countryLabel) {
fallback.countryLabel = countryLabel;
}
if (Object.prototype.hasOwnProperty.call(record, 'successfulUses')) {
fallback.successfulUses = normalizeUseCount(record.successfulUses);
}
if (Object.prototype.hasOwnProperty.call(record, 'maxUses')) {
fallback.maxUses = Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES));
}
if (statusAction) {
fallback.statusAction = statusAction;
}
return Object.keys(fallback).length ? fallback : null;
}
function describeHeroSmsPayload(raw) {
if (typeof raw === 'string') {
return raw.trim();
}
if (raw && typeof raw === 'object') {
if (raw.title || raw.details) {
const title = String(raw.title || '').trim();
const details = String(raw.details || '').trim();
return details ? `${title}: ${details}` : title;
}
if (raw.status === 'false' && raw.msg) {
return String(raw.msg).trim();
}
try {
return JSON.stringify(raw);
} catch {
return String(raw);
}
}
return String(raw || '').trim();
}
function parseHeroSmsPayload(text) {
const trimmed = String(text || '').trim();
if (!trimmed) {
return '';
}
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
try {
return JSON.parse(trimmed);
} catch {
return trimmed;
}
}
return trimmed;
}
function buildHeroSmsUrl(baseUrl, query = {}) {
const url = new URL(normalizeUrl(baseUrl));
Object.entries(query).forEach(([key, value]) => {
if (value === undefined || value === null || value === '') {
return;
}
url.searchParams.set(key, String(value));
});
return url.toString();
}
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 重新获取新号码。'
);
}
async function fetchHeroSmsPayload(config, query, actionLabel) {
const requestUrl = buildHeroSmsUrl(config.baseUrl, {
api_key: config.apiKey,
...query,
});
const controller = typeof AbortController === 'function' ? new AbortController() : null;
const timeoutId = controller
? setTimeout(() => controller.abort(), DEFAULT_PHONE_REQUEST_TIMEOUT_MS)
: null;
try {
const response = await fetchImpl(requestUrl, {
method: 'GET',
signal: controller?.signal,
});
const text = await response.text();
const payload = parseHeroSmsPayload(text);
if (!response.ok) {
throw createPhoneSmsActionFailureError(
actionLabel,
describeHeroSmsPayload(payload) || response.status,
payload,
response.status
);
}
return payload;
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`${formatPhoneSmsActionLabel(actionLabel)}超时。`);
}
throw error;
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
}
function parseFiveSimPayload(text) {
const trimmed = String(text || '').trim();
if (!trimmed) {
return '';
}
try {
return JSON.parse(trimmed);
} catch {
return trimmed;
}
}
function describeFiveSimPayload(raw) {
if (typeof raw === 'string') {
return raw.trim();
}
if (raw && typeof raw === 'object') {
const message = String(raw.message || raw.error || raw.msg || raw.statusText || '').trim();
if (message) {
return message;
}
try {
return JSON.stringify(raw);
} catch {
return String(raw);
}
}
return String(raw || '').trim();
}
async function fetchFiveSimPayload(config, path, actionLabel, options = {}) {
const controller = typeof AbortController === 'function' ? new AbortController() : null;
const timeoutId = controller
? setTimeout(() => controller.abort(), DEFAULT_PHONE_REQUEST_TIMEOUT_MS)
: null;
try {
const requestUrl = new URL(path.replace(/^\/+/, ''), `${config.baseUrl.replace(/\/+$/, '')}/`);
const query = (options && options.query && typeof options.query === 'object') ? options.query : {};
Object.entries(query).forEach(([key, value]) => {
if (value === undefined || value === null || value === '') {
return;
}
requestUrl.searchParams.set(key, String(value));
});
const response = await fetchImpl(requestUrl.toString(), {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${config.apiKey}`,
},
signal: controller?.signal,
});
const text = await response.text();
const payload = parseFiveSimPayload(text);
if (!response.ok) {
throw createPhoneSmsActionFailureError(
actionLabel,
describeFiveSimPayload(payload) || response.status,
payload,
response.status
);
}
return payload;
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`${formatPhoneSmsActionLabel(actionLabel)}超时。`);
}
throw error;
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
}
function parseNexSmsPayload(text) {
const trimmed = String(text || '').trim();
if (!trimmed) {
return '';
}
try {
return JSON.parse(trimmed);
} catch {
return trimmed;
}
}
function describeNexSmsPayload(raw) {
if (typeof raw === 'string') {
return raw.trim();
}
if (raw && typeof raw === 'object') {
const message = String(raw.message || raw.error || raw.msg || raw.statusText || '').trim();
if (message) {
return message;
}
try {
return JSON.stringify(raw);
} catch {
return String(raw);
}
}
return String(raw || '').trim();
}
function isNexSmsSuccessPayload(payload) {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
return false;
}
return Number(payload.code) === 0;
}
async function fetchNexSmsPayload(config, path, actionLabel, options = {}) {
const controller = typeof AbortController === 'function' ? new AbortController() : null;
const timeoutId = controller
? setTimeout(() => controller.abort(), DEFAULT_PHONE_REQUEST_TIMEOUT_MS)
: null;
try {
const method = String(options.method || 'GET').trim().toUpperCase() || 'GET';
const requestUrl = new URL(path.replace(/^\/+/, ''), `${config.baseUrl.replace(/\/+$/, '')}/`);
requestUrl.searchParams.set('apiKey', config.apiKey);
const query = (options && options.query && typeof options.query === 'object') ? options.query : {};
Object.entries(query).forEach(([key, value]) => {
if (value === undefined || value === null || value === '') {
return;
}
requestUrl.searchParams.set(key, String(value));
});
const headers = {
Accept: 'application/json',
...(options.headers && typeof options.headers === 'object' ? options.headers : {}),
};
const requestInit = {
method,
headers,
signal: controller?.signal,
};
if (method !== 'GET' && method !== 'HEAD' && options.body !== undefined) {
requestInit.body = typeof options.body === 'string'
? options.body
: JSON.stringify(options.body);
if (!requestInit.headers['Content-Type']) {
requestInit.headers['Content-Type'] = 'application/json';
}
}
const response = await fetchImpl(requestUrl.toString(), requestInit);
const text = await response.text();
const payload = parseNexSmsPayload(text);
if (!response.ok) {
throw createPhoneSmsActionFailureError(
actionLabel,
describeNexSmsPayload(payload) || response.status,
payload,
response.status
);
}
return payload;
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`${formatPhoneSmsActionLabel(actionLabel)}超时。`);
}
throw error;
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
}
function resolvePhoneConfig(state = {}) {
const provider = normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER);
if (provider === PHONE_SMS_PROVIDER_5SIM) {
const apiKey = normalizeApiKey(state.fiveSimApiKey || state.heroSmsApiKey);
if (!apiKey) {
throw new Error('5sim API Key 缺失,请先在侧边栏保存接码 API Key。');
}
const configuredMaxPrice = normalizeHeroSmsPriceLimit(state.fiveSimMaxPrice);
const operator = normalizeFiveSimCountryCode(state.fiveSimOperator, DEFAULT_FIVE_SIM_OPERATOR);
const maxPriceLimit = configuredMaxPrice !== null
? configuredMaxPrice
: normalizeHeroSmsPriceLimit(state.heroSmsMaxPrice);
assertFiveSimMaxPriceCompatibleWithOperator(operator, maxPriceLimit);
return {
provider,
apiKey,
baseUrl: normalizeUrl(state.fiveSimBaseUrl, DEFAULT_FIVE_SIM_BASE_URL).replace(/\/+$/, ''),
operator,
product: normalizeFiveSimCountryCode(state.fiveSimProduct, DEFAULT_FIVE_SIM_PRODUCT),
maxPriceLimit,
countryCandidates: resolveFiveSimCountryCandidates(state),
};
}
if (provider === PHONE_SMS_PROVIDER_NEXSMS) {
const apiKey = normalizeApiKey(state.nexSmsApiKey || state.heroSmsApiKey);
if (!apiKey) {
throw new Error('NexSMS API Key 缺失,请先在侧边栏保存接码 API Key。');
}
return {
provider,
apiKey,
baseUrl: normalizeUrl(state.nexSmsBaseUrl, DEFAULT_NEX_SMS_BASE_URL).replace(/\/+$/, ''),
serviceCode: normalizeNexSmsServiceCode(state.nexSmsServiceCode, DEFAULT_NEX_SMS_SERVICE_CODE),
countryCandidates: resolveNexSmsCountryCandidates(state),
};
}
const apiKey = normalizeApiKey(state.heroSmsApiKey);
if (!apiKey) {
throw new Error('HeroSMS API Key 缺失,请先在侧边栏保存接码 API Key。');
}
return {
provider,
apiKey,
baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL),
operator: normalizeHeroSmsOperator(state.heroSmsOperator, DEFAULT_HERO_SMS_OPERATOR),
countryCandidates: resolveCountryCandidates(state),
};
}
function resolveHeroSmsPhoneConfig(state = {}) {
const apiKey = normalizeApiKey(state.heroSmsApiKey);
if (!apiKey) {
throw new Error('HeroSMS API Key 缺失,请先在侧边栏保存接码 API Key。');
}
return {
provider: PHONE_SMS_PROVIDER_HERO,
apiKey,
baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL),
operator: normalizeHeroSmsOperator(state.heroSmsOperator, DEFAULT_HERO_SMS_OPERATOR),
countryCandidates: resolveCountryCandidates(state),
};
}
const LATEST_PHONE_SETTING_KEYS = Object.freeze([
'phoneSmsProvider',
'phoneSmsProviderOrder',
'phoneSmsReuseEnabled',
'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 parseActivationPayload(payload, fallback = null) {
const normalizedFallback = normalizeActivation(fallback) || normalizeActivationFallback(fallback);
const directActivation = normalizeActivation(payload);
if (directActivation) {
const statusAction = normalizedFallback?.statusAction || directActivation.statusAction;
return {
...directActivation,
provider: normalizedFallback?.provider || directActivation.provider,
serviceCode: normalizedFallback?.serviceCode || directActivation.serviceCode,
countryId: normalizedFallback?.countryId || directActivation.countryId,
...(
normalizedFallback?.countryLabel || directActivation.countryLabel
? { countryLabel: normalizedFallback?.countryLabel || directActivation.countryLabel }
: {}
),
successfulUses: normalizedFallback?.successfulUses ?? directActivation.successfulUses,
maxUses: normalizedFallback?.maxUses ?? directActivation.maxUses,
...(statusAction ? { statusAction } : {}),
};
}
const text = describeHeroSmsPayload(payload);
const accessNumberMatch = text.match(/^ACCESS_NUMBER:([^:]+):(.+)$/i);
if (accessNumberMatch) {
return {
activationId: String(accessNumberMatch[1] || '').trim(),
phoneNumber: String(accessNumberMatch[2] || '').trim(),
provider: normalizedFallback?.provider || PHONE_SMS_PROVIDER_HERO,
serviceCode: normalizedFallback?.serviceCode || HERO_SMS_SERVICE_CODE,
countryId: normalizedFallback?.countryId || HERO_SMS_COUNTRY_ID,
...(normalizedFallback?.countryLabel ? { countryLabel: normalizedFallback.countryLabel } : {}),
successfulUses: normalizedFallback?.successfulUses ?? 0,
maxUses: normalizedFallback?.maxUses ?? DEFAULT_PHONE_NUMBER_MAX_USES,
...(normalizedFallback?.statusAction ? { statusAction: normalizedFallback.statusAction } : {}),
};
}
if (/^ACCESS_READY$/i.test(text) && normalizedFallback) {
return normalizedFallback;
}
return null;
}
function resolveActivationStatusAction(activation) {
return activation?.statusAction === 'getStatusV2' ? 'getStatusV2' : 'getStatus';
}
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 resolveHeroSmsStockState(payload = {}) {
const physicalCount = Number(payload.physicalCount);
if (Number.isFinite(physicalCount)) {
return {
hasStockField: true,
stockCount: physicalCount,
};
}
const stockCandidates = [
payload.count,
payload.stock,
payload.available,
payload.quantity,
payload.qty,
payload.left,
payload.free,
]
.map((value) => Number(value))
.filter((value) => Number.isFinite(value));
if (!stockCandidates.length) {
return {
hasStockField: false,
stockCount: 0,
};
}
return {
hasStockField: true,
stockCount: Math.max(...stockCandidates),
};
}
function collectHeroSmsPriceCandidates(payload, candidates = []) {
if (Array.isArray(payload)) {
payload.forEach((entry) => collectHeroSmsPriceCandidates(entry, candidates));
return candidates;
}
if (!payload || typeof payload !== 'object') {
return candidates;
}
const cost = normalizeHeroSmsPrice(payload.cost);
if (cost !== null) {
const stockState = resolveHeroSmsStockState(payload);
if (!stockState.hasStockField || stockState.stockCount > 0) {
candidates.push(cost);
}
}
// Some HeroSMS responses expose price tiers as object keys:
// { "0.05": { count: 0 }, "0.35": { count: 12 } }.
// Parse those keyed tiers so higher-price stock is not missed.
Object.entries(payload).forEach(([key, value]) => {
const keyedPrice = normalizeHeroSmsPrice(key);
if (keyedPrice === null) {
return;
}
if (value && typeof value === 'object') {
const stockState = resolveHeroSmsStockState(value);
// Ignore numeric keys that are actually country/service IDs.
// Keyed price tiers are considered valid only when stock fields exist.
if (stockState.hasStockField && stockState.stockCount > 0) {
candidates.push(keyedPrice);
}
return;
}
const numericCount = Number(value);
if (Number.isFinite(numericCount) && numericCount > 0) {
candidates.push(keyedPrice);
}
});
Object.values(payload).forEach((value) => collectHeroSmsPriceCandidates(value, candidates));
return candidates;
}
function findLowestHeroSmsPrice(payload) {
const candidates = collectHeroSmsPriceCandidates(payload, []);
if (!candidates.length) {
return null;
}
return Math.min(...candidates);
}
function buildSortedUniquePriceCandidates(values = []) {
return Array.from(
new Set(
values
.map((value) => normalizeHeroSmsPrice(value))
.filter((value) => value !== null)
.map((value) => Math.round(value * 10000) / 10000)
)
)
.sort((left, right) => left - right)
.slice(0, MAX_PHONE_PRICE_CANDIDATES);
}
function isHeroSmsNoNumbersPayload(payload) {
return /\bNO_NUMBERS\b/i.test(describeHeroSmsPayload(payload));
}
function extractHeroSmsWrongMaxPrice(payload) {
if (payload && typeof payload === 'object') {
const title = String(payload.title || '').trim();
const minPrice = normalizeHeroSmsPrice(payload.info?.min);
if (/^WRONG_MAX_PRICE$/i.test(title) && minPrice !== null) {
return minPrice;
}
}
const text = describeHeroSmsPayload(payload);
const match = text.match(/\bWRONG_MAX_PRICE:(\d+(?:\.\d+)?)\b/i);
if (!match) {
return null;
}
return normalizeHeroSmsPrice(match[1]);
}
function isNetworkFetchFailure(error) {
const message = String(error?.message || '').trim();
return /failed to fetch|networkerror|load failed/i.test(message);
}
function isHeroSmsTerminalError(payloadOrMessage) {
const text = describeHeroSmsPayload(payloadOrMessage);
return /\bNO_BALANCE\b|\bNOT_ENOUGH_BALANCE\b|\bBAD_KEY\b|\bINVALID_KEY\b|\bBANNED\b|\bACCOUNT_BANNED\b|\bWRONG_KEY\b/i.test(text);
}
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 heroCountryCount = resolveCountryCandidates(state).length;
const fiveSimCountryCount = resolveFiveSimCountryCandidates(state).length;
const nexSmsCountryCount = resolveNexSmsCountryCandidates(state).length;
const activeProvider = normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER);
const priceRange = resolvePhonePriceRange(state, activeProvider);
const minPrice = priceRange.minPriceLimit;
const maxPrice = priceRange.maxPriceLimit;
const acquirePriority = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
return {
order,
heroCountryCount,
fiveSimCountryCount,
nexSmsCountryCount,
minPrice,
maxPrice,
priceRangeInvalid: priceRange.invalidRange,
priceRangeText: formatPhonePriceRangeText(minPrice, maxPrice),
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 resolveCheapestPhoneActivationPrice(config, countryConfig) {
for (let attempt = 1; attempt <= DEFAULT_PHONE_PRICE_LOOKUP_ATTEMPTS; attempt += 1) {
try {
const { payloads } = await fetchHeroSmsPricePayloads(config, countryConfig, { state });
const price = findLowestHeroSmsPrice(
payloads && payloads.length
? payloads
: []
);
const normalizedPrice = Number.isFinite(Number(price)) ? Number(price) : null;
if (normalizedPrice !== null) {
return normalizedPrice;
}
const fallbackCandidates = buildSortedUniquePriceCandidates(
(Array.isArray(payloads) ? payloads : [])
.flatMap((payload) => collectHeroSmsPriceCandidatesIncludingZeroStock(payload, []))
);
if (fallbackCandidates.length > 0) {
return fallbackCandidates[0];
}
} catch (_) {
// Best-effort lookup only.
}
}
return null;
}
async function persistHeroSmsPricePlanSnapshot(countryConfig, pricePlan) {
if (typeof setState !== 'function') {
return;
}
const prices = Array.isArray(pricePlan?.prices)
? pricePlan.prices.filter((price) => Number.isFinite(Number(price)))
: [];
const userLimit = pricePlan?.userLimit === null || pricePlan?.userLimit === undefined
? ''
: String(pricePlan.userLimit);
await setState({
[HERO_SMS_LAST_PRICE_TIERS_KEY]: prices,
[HERO_SMS_LAST_PRICE_COUNTRY_ID_KEY]: normalizeCountryId(countryConfig?.id, 0),
[HERO_SMS_LAST_PRICE_COUNTRY_LABEL_KEY]: normalizeCountryLabel(countryConfig?.label, HERO_SMS_COUNTRY_LABEL),
[HERO_SMS_LAST_PRICE_USER_LIMIT_KEY]: userLimit,
[HERO_SMS_LAST_PRICE_AT_KEY]: Date.now(),
});
}
async function resolvePhoneActivationPricePlan(config, countryConfig, state = {}) {
for (let attempt = 1; attempt <= DEFAULT_PHONE_PRICE_LOOKUP_ATTEMPTS; attempt += 1) {
try {
const { payloads } = await fetchHeroSmsPricePayloads(config, countryConfig, { state });
const plan = await resolveHeroSmsPricePlanFromPricePayloads(
config,
countryConfig,
state,
payloads
);
if (
Array.isArray(plan?.prices)
&& plan.prices.length > 0
&& (
plan.prices.some((price) => Number.isFinite(Number(price)) && Number(price) > 0)
|| plan.syntheticUserLimitProbe
)
) {
return plan;
}
} catch (_) {
// best effort
}
}
const fallbackPlan = {
prices: [null],
userLimit: null,
minCatalogPrice: null,
syntheticUserLimitProbe: false,
};
await persistHeroSmsPricePlanSnapshot(countryConfig, fallbackPlan);
return fallbackPlan;
}
async function fetchPhoneActivationPayload(config, countryConfig, action, options = {}) {
const query = {
action,
service: HERO_SMS_SERVICE_CODE,
country: countryConfig.id,
};
const operator = normalizeHeroSmsOperator(config?.operator, DEFAULT_HERO_SMS_OPERATOR);
if (operator && operator !== DEFAULT_HERO_SMS_OPERATOR) {
query.operator = operator;
}
if (options.maxPrice !== null && options.maxPrice !== undefined) {
query.maxPrice = options.maxPrice;
if (options.fixedPrice !== false) {
query.fixedPrice = 'true';
}
}
return fetchHeroSmsPayload(config, query, `HeroSMS ${action}`);
}
async function requestPhoneActivationWithPrice(config, countryConfig, action, maxPrice, options = {}) {
let nextMaxPrice = maxPrice;
let retriedWithUpdatedPrice = false;
let retriedWithoutPrice = false;
const userLimit = normalizeHeroSmsPriceLimit(options.userLimit);
const userMinLimit = normalizeHeroSmsPriceLimit(options.userMinLimit);
while (true) {
try {
return await fetchPhoneActivationPayload(config, countryConfig, action, {
maxPrice: nextMaxPrice,
fixedPrice: options.fixedPrice,
});
} catch (error) {
const updatedMaxPrice = extractHeroSmsWrongMaxPrice(error?.payload || error?.message);
if (
nextMaxPrice !== null
&& nextMaxPrice !== undefined
&& !retriedWithUpdatedPrice
&& updatedMaxPrice !== null
) {
if (userLimit !== null && updatedMaxPrice > userLimit) {
throw new Error(
`HeroSMS ${formatHeroSmsActionName(action)}失败:价格上限过低,平台要求至少 ${updatedMaxPrice},已超过当前配置的价格上限 ${userLimit}`
);
}
if (userMinLimit !== null && updatedMaxPrice < userMinLimit) {
throw new Error(
`HeroSMS ${formatHeroSmsActionName(action)}失败:平台要求价格 ${updatedMaxPrice} 低于当前配置的最低购买价 ${userMinLimit}`
);
}
nextMaxPrice = updatedMaxPrice;
retriedWithUpdatedPrice = true;
continue;
}
if (
nextMaxPrice !== null
&& nextMaxPrice !== undefined
&& !retriedWithoutPrice
&& isNetworkFetchFailure(error)
) {
nextMaxPrice = null;
retriedWithoutPrice = true;
continue;
}
throw error;
}
}
}
function collectFiveSimPriceCandidates(payload, candidates = []) {
if (Array.isArray(payload)) {
payload.forEach((entry) => collectFiveSimPriceCandidates(entry, candidates));
return candidates;
}
if (!payload || typeof payload !== 'object') {
return candidates;
}
const cost = Number(payload.cost);
const count = Number(payload.count);
if (Number.isFinite(cost) && cost > 0) {
if (!Number.isFinite(count) || count > 0) {
candidates.push(Math.round(cost * 10000) / 10000);
}
}
Object.entries(payload).forEach(([key, value]) => {
const keyedPrice = Number(key);
if (!Number.isFinite(keyedPrice) || keyedPrice <= 0) {
return;
}
if (value && typeof value === 'object') {
const keyedCount = Number(value.count);
if (!Number.isFinite(keyedCount) || keyedCount > 0) {
candidates.push(Math.round(keyedPrice * 10000) / 10000);
}
return;
}
const numericCount = Number(value);
if (!Number.isFinite(numericCount) || numericCount > 0) {
candidates.push(Math.round(keyedPrice * 10000) / 10000);
}
});
Object.values(payload).forEach((entry) => collectFiveSimPriceCandidates(entry, candidates));
return candidates;
}
function collectFiveSimProductPriceCandidates(payload, product = DEFAULT_FIVE_SIM_PRODUCT, candidates = []) {
if (Array.isArray(payload)) {
payload.forEach((entry) => collectFiveSimProductPriceCandidates(entry, product, candidates));
return candidates;
}
if (!payload || typeof payload !== 'object') {
return candidates;
}
const productPayload = payload[product] || payload[String(product || '').toLowerCase()];
if (productPayload && typeof productPayload === 'object') {
const price = Number(productPayload.Price ?? productPayload.price ?? productPayload.cost);
const qty = Number(productPayload.Qty ?? productPayload.qty ?? productPayload.count);
if (Number.isFinite(price) && price > 0 && (!Number.isFinite(qty) || qty > 0)) {
candidates.push(Math.round(price * 10000) / 10000);
}
}
Object.values(payload).forEach((entry) => collectFiveSimProductPriceCandidates(entry, product, candidates));
return candidates;
}
function findLowestFiveSimPrice(payload, product = DEFAULT_FIVE_SIM_PRODUCT, countryCode = '', priceRange = {}) {
const normalizedProduct = normalizeFiveSimCountryCode(product, DEFAULT_FIVE_SIM_PRODUCT);
const normalizedCountryCode = normalizeFiveSimCountryCode(countryCode, '');
const root = payload && typeof payload === 'object'
? (payload[normalizedProduct] || payload)
: {};
const countryPayload = (
normalizedCountryCode
? (root?.[normalizedCountryCode] || root)
: root
);
const candidates = filterPriceCandidatesWithinRange(
buildSortedUniquePriceCandidates(collectFiveSimPriceCandidates(countryPayload, [])),
priceRange?.minPriceLimit ?? null,
priceRange?.maxPriceLimit ?? null
);
if (!candidates.length) {
return null;
}
return Math.min(...candidates);
}
function isFiveSimNoNumbersError(payloadOrMessage) {
const text = describeFiveSimPayload(payloadOrMessage);
return /no\s+free\s+phones|no\s+phones\s+available|no\s+numbers\s+available/i.test(text);
}
function isFiveSimRateLimitError(payloadOrMessage, status = 0) {
if (Number(status) === 429) {
return true;
}
const text = describeFiveSimPayload(payloadOrMessage);
return /rate\s*limit|too\s*many\s*requests|request\s*limit|429/i.test(text);
}
function buildFiveSimRateLimitError(details = []) {
const suffix = Array.isArray(details) && details.length
? `${details.join(' | ')}`
: '。';
return new Error(`${FIVE_SIM_RATE_LIMIT_ERROR_PREFIX}5sim 购买接口触发限流,请稍后再试${suffix}`);
}
function isFiveSimTerminalError(payloadOrMessage, status = 0) {
if (Number(status) === 401 || Number(status) === 403) {
return true;
}
const text = describeFiveSimPayload(payloadOrMessage);
return /not\s+enough\s+balance|no\s+balance|unauthorized|invalid\s+token|forbidden|bad\s+key|wrong\s+key|banned/i.test(text);
}
async function resolveFiveSimLowestPrice(config, countryCode, priceRange = {}) {
try {
const payload = await fetchFiveSimPayload(
config,
'/guest/prices',
'5sim guest prices',
{
query: {
country: countryCode,
product: config.product,
},
}
);
return findLowestFiveSimPrice(payload, config.product, countryCode, priceRange);
} catch {
return null;
}
}
function parseFiveSimActivationPayload(payload, fallback = {}) {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
return null;
}
const activationId = String(payload.id || payload.activationId || '').trim();
const phoneNumber = String(payload.phone || payload.number || '').trim();
if (!activationId || !phoneNumber) {
return null;
}
const fallbackCountryCode = normalizeFiveSimCountryCode(
fallback.countryCode || fallback.countryId || '',
'thailand'
);
const countryCode = normalizeFiveSimCountryCode(
payload.country || payload.country_name || payload.countryCode || payload.countryId || fallbackCountryCode,
fallbackCountryCode
);
const countryLabel = String(
payload.country_name
|| payload.countryName
|| payload.country
|| fallback.countryLabel
|| countryCode
).trim();
return {
activationId,
phoneNumber,
provider: PHONE_SMS_PROVIDER_5SIM,
serviceCode: normalizeFiveSimCountryCode(payload.product || fallback.serviceCode || DEFAULT_FIVE_SIM_PRODUCT, DEFAULT_FIVE_SIM_PRODUCT),
countryId: countryCode,
countryCode,
countryLabel: countryLabel || countryCode,
successfulUses: normalizeUseCount(payload.successfulUses ?? fallback.successfulUses ?? 0),
maxUses: Math.max(1, Math.floor(Number(payload.maxUses ?? fallback.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)),
...(() => {
const expiresAt = normalizeTimestampMs(
payload.expiresAt
?? payload.expires
?? payload.expireAt
?? payload.expired_at
?? payload.expiredAt
?? fallback.expiresAt
);
return expiresAt > 0 ? { expiresAt } : {};
})(),
};
}
async function requestFiveSimActivation(state = {}, options = {}) {
const config = resolvePhoneConfig(state);
const allCountryCandidates = Array.isArray(config.countryCandidates) && config.countryCandidates.length
? config.countryCandidates
: [];
if (!allCountryCandidates.length) {
throw new Error(`步骤 ${getActivePhoneVerificationVisibleStep()}:5sim 未选择国家,请先在接码设置中至少选择 1 个国家。`);
}
const blockedCountryIds = new Set(
(Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : [])
.map((value) => normalizeFiveSimCountryCode(value, ''))
.filter(Boolean)
);
let countryCandidates = allCountryCandidates.filter(
(entry) => !blockedCountryIds.has(normalizeFiveSimCountryCode(entry.code || entry.id || '', ''))
);
if (!countryCandidates.length) {
countryCandidates = allCountryCandidates;
if (blockedCountryIds.size) {
await addLog(
'步骤 9:已选国家均达到临时收码失败跳过阈值,本轮解除跳过并重新尝试。',
'warn'
);
}
}
const priceRange = resolvePhonePriceRange(state, PHONE_SMS_PROVIDER_5SIM);
if (priceRange.invalidRange) {
throw new Error(
`5sim 价格区间无效:最低购买价 ${priceRange.minPriceLimit} 高于价格上限 ${priceRange.maxPriceLimit}`
);
}
const maxPriceLimit = priceRange.maxPriceLimit;
const minPriceLimit = priceRange.minPriceLimit;
const hasPriceBounds = priceRange.hasMinPriceLimit || priceRange.hasMaxPriceLimit;
const acquirePriority = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
const preferredPriceTier = normalizeHeroSmsPriceLimit(state?.heroSmsPreferredPrice);
const countryPriceFloorByCountryCode = normalizeCountryPriceFloorMap(
options?.countryPriceFloorByCountryId,
(value) => normalizeFiveSimCountryCode(value, '')
);
const configuredAcquireRounds = normalizePhoneActivationRetryRounds(state?.heroSmsActivationRetryRounds);
const maxAcquireRounds = Math.max(2, configuredAcquireRounds);
const retryDelayMs = normalizePhoneActivationRetryDelayMs(state?.heroSmsActivationRetryDelayMs);
let finalNoNumbersByCountry = [];
let finalRateLimitByCountry = [];
let finalLastError = null;
for (let round = 1; round <= maxAcquireRounds; round += 1) {
if (maxAcquireRounds > 1) {
await addLog(
`步骤 9:5sim 正在获取手机号(第 ${round}/${maxAcquireRounds} 轮)...`,
'info'
);
}
const noNumbersByCountry = [];
const rateLimitByCountry = [];
const retryableNoNumberCountries = [];
let lastError = null;
let orderedCountryCandidates = [...countryCandidates];
if (
(acquirePriority === HERO_SMS_ACQUIRE_PRIORITY_PRICE || acquirePriority === HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH)
&& countryCandidates.length > 1
) {
const rankedCandidates = [];
for (const [index, countryConfig] of countryCandidates.entries()) {
const countryCode = normalizeFiveSimCountryCode(countryConfig.code || countryConfig.id || '', 'thailand');
const lowestPrice = await resolveFiveSimLowestPrice(config, countryCode, priceRange);
rankedCandidates.push({
index,
countryConfig,
lowestPrice: Number.isFinite(Number(lowestPrice)) ? Number(lowestPrice) : null,
});
}
rankedCandidates.sort((left, right) => {
const leftPrice = left.lowestPrice;
const rightPrice = right.lowestPrice;
const leftHasPrice = Number.isFinite(leftPrice);
const rightHasPrice = Number.isFinite(rightPrice);
if (leftHasPrice && rightHasPrice && leftPrice !== rightPrice) {
return acquirePriority === HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH
? (rightPrice - leftPrice)
: (leftPrice - rightPrice);
}
if (leftHasPrice !== rightHasPrice) {
return leftHasPrice ? -1 : 1;
}
return left.index - right.index;
});
orderedCountryCandidates = rankedCandidates.map((entry) => entry.countryConfig);
const rankedSummary = rankedCandidates
.map((entry) => {
const countryCode = normalizeFiveSimCountryCode(entry.countryConfig.code || entry.countryConfig.id || '', 'thailand');
const countryLabel = String(entry.countryConfig.label || countryCode).trim() || countryCode;
return Number.isFinite(entry.lowestPrice)
? `${countryLabel}:${entry.lowestPrice}`
: `${countryLabel}:无`;
})
.join(' | ');
await addLog(`步骤 95sim 价格优先排序:${rankedSummary}`, 'info');
}
for (const countryConfig of orderedCountryCandidates) {
const countryCode = normalizeFiveSimCountryCode(countryConfig.code || countryConfig.id || '', 'thailand');
const countryLabel = String(countryConfig.label || countryCode).trim() || countryCode;
const countryPriceFloor = countryPriceFloorByCountryCode.get(countryCode) ?? null;
try {
const explicitFiveSimMaxPriceLimit = normalizeHeroSmsPriceLimit(state.fiveSimMaxPrice);
let guestPricesPayload = null;
let productPricesPayload = null;
if (explicitFiveSimMaxPriceLimit !== null) {
try {
productPricesPayload = await fetchFiveSimPayload(
config,
`/guest/products/${countryCode}/${config.operator}`,
'5sim guest products'
);
} catch (_) {
productPricesPayload = null;
}
}
try {
guestPricesPayload = await fetchFiveSimPayload(
config,
'/guest/prices',
'5sim guest prices',
{
query: {
country: countryCode,
product: config.product,
},
}
);
} catch (_) {
guestPricesPayload = null;
}
const rawPriceCandidates = buildSortedUniquePriceCandidates(
[
...(
normalizeHeroSmsPriceLimit(state.fiveSimMaxPrice) !== null
? collectFiveSimProductPriceCandidates(productPricesPayload, config.product, [])
: []
),
...collectFiveSimPriceCandidates(
(
guestPricesPayload
&& typeof guestPricesPayload === 'object'
&& !Array.isArray(guestPricesPayload)
? (guestPricesPayload?.[config.product]?.[countryCode] || guestPricesPayload?.[countryCode] || guestPricesPayload)
: guestPricesPayload
),
[]
),
]
);
const rangeFilteredPriceCandidates = filterPriceCandidatesWithinRange(
rawPriceCandidates,
minPriceLimit,
maxPriceLimit
);
const orderedPricesFromCatalog = reorderPriceCandidates(
rangeFilteredPriceCandidates,
acquirePriority,
preferredPriceTier
);
const orderedPrices = orderedPricesFromCatalog.length
? (
explicitFiveSimMaxPriceLimit !== null
? [
explicitFiveSimMaxPriceLimit,
...orderedPricesFromCatalog.filter((price) => Number(price) !== Number(explicitFiveSimMaxPriceLimit)),
]
: orderedPricesFromCatalog
)
: (
minPriceLimit !== null
? (maxPriceLimit !== null ? [maxPriceLimit] : [])
: (maxPriceLimit !== null ? [maxPriceLimit] : [null])
);
const rangeCheckedPrices = filterPriceCandidatesWithinRange(orderedPrices, minPriceLimit, maxPriceLimit);
const candidatePrices = rangeCheckedPrices.length
? rangeCheckedPrices
: (hasPriceBounds ? [] : orderedPrices);
const floorFilteredPrices = filterPriceCandidatesAboveFloor(candidatePrices, countryPriceFloor);
const hasCountryPriceFloor = (
countryPriceFloor !== null
&& Number.isFinite(Number(countryPriceFloor))
&& Number(countryPriceFloor) > 0
);
const hasAlternativeCountries = orderedCountryCandidates.some((entry) => (
normalizeFiveSimCountryCode(entry.code || entry.id || '', '')
!== normalizeFiveSimCountryCode(countryConfig.code || countryConfig.id || '', '')
));
// When a floor is set (e.g. timeout on current tier), do NOT fall back to the
// original lowest-tier list; that would retry the same tier forever and block
// country/provider fallback.
const pricesToTry = hasCountryPriceFloor
? (
floorFilteredPrices.length
? floorFilteredPrices
: (hasAlternativeCountries ? [] : candidatePrices.slice(0, 1))
)
: (floorFilteredPrices.length ? floorFilteredPrices : candidatePrices);
if (!pricesToTry.length) {
const lowestCatalog = rawPriceCandidates.length ? rawPriceCandidates[0] : null;
if (
minPriceLimit !== null
&& !rangeFilteredPriceCandidates.length
&& rawPriceCandidates.length
) {
noNumbersByCountry.push(
`${countryLabel}: 价格区间 ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)} 内暂无可用号码;可见档位=${rawPriceCandidates.join(', ')}`
);
} else if (
maxPriceLimit !== null
&& lowestCatalog !== null
&& Number(lowestCatalog) > Number(maxPriceLimit)
) {
noNumbersByCountry.push(
`${countryLabel}: 价格上限 ${maxPriceLimit} 内暂无可用号码;平台最低价=${lowestCatalog}`
);
} else if (countryPriceFloor !== null && rangeFilteredPriceCandidates.length) {
noNumbersByCountry.push(
`${countryLabel}: 当前回退尝试没有高于 ${countryPriceFloor} 的价格档位`
);
} else if (rawPriceCandidates.length) {
const tierText = rawPriceCandidates.join(', ');
noNumbersByCountry.push(`${countryLabel}: 可见价格档位均不可用(${tierText}`);
retryableNoNumberCountries.push(countryLabel);
} else {
noNumbersByCountry.push(`${countryLabel}: 暂无可用号码`);
retryableNoNumberCountries.push(countryLabel);
}
continue;
}
let acquiredActivation = null;
let countryNoNumbersText = '';
for (const candidatePrice of pricesToTry) {
try {
const payload = await fetchFiveSimPayload(
config,
`/user/buy/activation/${countryCode}/${config.operator}/${config.product}`,
'5sim buy activation',
{
query: {
...(candidatePrice !== null && candidatePrice !== undefined ? { maxPrice: candidatePrice } : {}),
...(normalizePhoneSmsReuseEnabled(state) ? { reuse: 1 } : {}),
},
}
);
const activation = parseFiveSimActivationPayload(payload, {
countryCode,
countryLabel,
serviceCode: config.product,
});
if (activation) {
const priceValue = Number(candidatePrice);
rememberActivationAcquiredPrice(activation, priceValue);
acquiredActivation = activation;
break;
}
const payloadText = describeFiveSimPayload(payload);
if (isFiveSimRateLimitError(payload)) {
countryNoNumbersText = payloadText || countryNoNumbersText || 'rate limit';
continue;
}
if (isFiveSimNoNumbersError(payload)) {
countryNoNumbersText = payloadText || countryNoNumbersText || '暂无可用号码';
continue;
}
if (isFiveSimTerminalError(payload)) {
throw createPhoneSmsActionFailureError('5sim buy activation', payloadText || 'empty response');
}
lastError = createPhoneSmsActionFailureError('5sim buy activation', payloadText || 'empty response');
} catch (error) {
if (isFiveSimRateLimitError(error?.payload || error?.message, error?.status)) {
countryNoNumbersText = describeFiveSimPayload(error?.payload || error?.message) || countryNoNumbersText || 'rate limit';
continue;
}
if (isFiveSimTerminalError(error?.payload || error?.message, error?.status)) {
throw createPhoneSmsActionFailureError('5sim buy activation', describeFiveSimPayload(error?.payload || error?.message) || 'unknown terminal error', error?.payload, error?.status);
}
if (isFiveSimNoNumbersError(error?.payload || error?.message)) {
countryNoNumbersText = describeFiveSimPayload(error?.payload || error?.message) || countryNoNumbersText || '暂无可用号码';
continue;
}
lastError = error;
}
}
if (acquiredActivation) {
return acquiredActivation;
}
const lowestCatalogPrice = rawPriceCandidates.length
? rawPriceCandidates[0]
: await resolveFiveSimLowestPrice(config, countryCode);
if (
minPriceLimit !== null
&& !rangeFilteredPriceCandidates.length
&& rawPriceCandidates.length
) {
noNumbersByCountry.push(
`${countryLabel}: 价格区间 ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)} 内暂无可用号码;可见档位=${rawPriceCandidates.join(', ')}`
);
} else if (maxPriceLimit !== null && lowestCatalogPrice !== null && Number(lowestCatalogPrice) > Number(maxPriceLimit)) {
noNumbersByCountry.push(
`${countryLabel}: 价格上限 ${maxPriceLimit} 内暂无可用号码;平台最低价=${lowestCatalogPrice}`
);
} else if (isFiveSimRateLimitError(countryNoNumbersText)) {
rateLimitByCountry.push(`${countryLabel}: ${countryNoNumbersText || 'rate limit'}`);
} else {
noNumbersByCountry.push(`${countryLabel}: ${countryNoNumbersText || '暂无可用号码'}`);
retryableNoNumberCountries.push(countryLabel);
}
continue;
} catch (error) {
if (isFiveSimRateLimitError(error?.payload || error?.message, error?.status)) {
rateLimitByCountry.push(`${countryLabel}: ${describeFiveSimPayload(error?.payload || error?.message) || 'rate limit'}`);
continue;
}
if (isFiveSimTerminalError(error?.payload || error?.message, error?.status)) {
throw createPhoneSmsActionFailureError('5sim buy activation', describeFiveSimPayload(error?.payload || error?.message) || 'unknown terminal error', error?.payload, error?.status);
}
if (isFiveSimNoNumbersError(error?.payload || error?.message)) {
const lowestRangePrice = await resolveFiveSimLowestPrice(config, countryCode, priceRange);
const lowestCatalogPrice = lowestRangePrice === null
? await resolveFiveSimLowestPrice(config, countryCode)
: lowestRangePrice;
if (minPriceLimit !== null && lowestRangePrice === null) {
noNumbersByCountry.push(
`${countryLabel}: 价格区间 ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)} 内暂无可用号码`
);
} else if (maxPriceLimit !== null && lowestCatalogPrice !== null && lowestCatalogPrice > maxPriceLimit) {
noNumbersByCountry.push(
`${countryLabel}: 价格上限 ${maxPriceLimit} 内暂无可用号码;平台最低价=${lowestCatalogPrice}`
);
} else {
noNumbersByCountry.push(`${countryLabel}: ${describeFiveSimPayload(error?.payload || error?.message) || '暂无可用号码'}`);
retryableNoNumberCountries.push(countryLabel);
}
continue;
}
lastError = error;
}
}
finalNoNumbersByCountry = noNumbersByCountry;
finalRateLimitByCountry = rateLimitByCountry;
finalLastError = lastError;
if (rateLimitByCountry.length) {
throw buildFiveSimRateLimitError(rateLimitByCountry);
}
if (
noNumbersByCountry.length
&& round < maxAcquireRounds
&& retryableNoNumberCountries.length > 0
) {
await addLog(
`步骤 95sim 暂无可用号码(第 ${round}/${maxAcquireRounds} 轮);${Math.ceil(retryDelayMs / 1000)} 秒后重试。国家:${retryableNoNumberCountries.join(', ')}`,
'warn'
);
await sleepWithStop(retryDelayMs);
continue;
}
break;
}
if (finalNoNumbersByCountry.length) {
throw new Error(
`5sim 已尝试 ${countryCandidates.length} 个候选国家,均无可用号码:${finalNoNumbersByCountry.join(' | ')}`
);
}
if (finalRateLimitByCountry.length) {
throw buildFiveSimRateLimitError(finalRateLimitByCountry);
}
if (finalLastError) {
throw finalLastError;
}
throw new Error('5sim 获取手机号失败。');
}
function isNexSmsNoNumbersError(payloadOrMessage) {
const text = describeNexSmsPayload(payloadOrMessage);
return /numbers?\s+not\s+found|暂无可用|no\s+numbers|no\s+stock|库存.*0|not\s+available/i.test(text);
}
function isNexSmsPendingMessage(payloadOrMessage) {
const text = describeNexSmsPayload(payloadOrMessage);
return /no\s+sms|暂无短信|waiting|not\s+arrived|empty|未收到|短信为空|no\s+records/i.test(text);
}
function isNexSmsTerminalError(payloadOrMessage, status = 0) {
if (Number(status) === 401 || Number(status) === 403) {
return true;
}
const text = describeNexSmsPayload(payloadOrMessage);
return /invalid\s*api\s*key|bad[_\s-]*key|wrong[_\s-]*key|unauthorized|forbidden|no\s*balance|insufficient\s*balance|余额不足|账号.*封禁|banned/i.test(text);
}
function collectNexSmsPriceCandidates(countryData = {}) {
const candidates = [];
const pushCandidate = (value) => {
const numeric = Number(value);
if (Number.isFinite(numeric) && numeric > 0) {
candidates.push(Math.round(numeric * 10000) / 10000);
}
};
pushCandidate(countryData.minPrice);
pushCandidate(countryData.medianPrice);
pushCandidate(countryData.maxPrice);
if (countryData.priceMap && typeof countryData.priceMap === 'object') {
Object.entries(countryData.priceMap).forEach(([priceKey, count]) => {
const availableCount = Number(count);
if (!Number.isFinite(availableCount) || availableCount <= 0) {
return;
}
pushCandidate(priceKey);
});
}
return buildSortedUniquePriceCandidates(candidates);
}
async function resolveNexSmsCountryPricePlan(config, countryConfig, state = {}) {
const countryId = normalizeNexSmsCountryId(countryConfig?.id, -1);
if (countryId < 0) {
throw new Error(`NexSMS 国家 ID 无效:${countryConfig?.id}`);
}
const payload = await fetchNexSmsPayload(
config,
'/api/getCountryByService',
'NexSMS getCountryByService',
{
query: {
serviceCode: config.serviceCode,
countryId,
},
}
);
if (!isNexSmsSuccessPayload(payload)) {
throw createPhoneSmsActionFailureError('NexSMS getCountryByService', describeNexSmsPayload(payload) || 'empty response');
}
const countryData = (payload && typeof payload === 'object' && !Array.isArray(payload))
? (payload.data || {})
: {};
const countryLabel = normalizeCountryLabel(
countryData.countryName || countryConfig?.label,
`Country #${countryId}`
);
const prices = collectNexSmsPriceCandidates(countryData);
const minCatalogPrice = prices.length
? prices[0]
: (() => {
const minPrice = Number(countryData.minPrice);
return Number.isFinite(minPrice) && minPrice > 0
? Math.round(minPrice * 10000) / 10000
: null;
})();
const userLimit = normalizeHeroSmsPriceLimit(state?.heroSmsMaxPrice);
const filteredPrices = userLimit === null
? prices
: prices.filter((price) => price <= userLimit);
return {
countryId,
countryLabel,
prices: filteredPrices,
userLimit,
minCatalogPrice,
rawPayload: payload,
};
}
function parseNexSmsActivationPayload(payload, fallback = {}) {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
return null;
}
if (!isNexSmsSuccessPayload(payload)) {
return null;
}
const data = payload.data || {};
const phoneCandidates = Array.isArray(data.phoneNumbers)
? data.phoneNumbers
: (Array.isArray(data.numbers) ? data.numbers : []);
const phoneNumber = String(
data.phoneNumber
|| data.phone
|| phoneCandidates[0]
|| fallback.phoneNumber
|| ''
).trim();
if (!phoneNumber) {
return null;
}
const countryId = normalizeNexSmsCountryId(
data.countryId ?? fallback.countryId,
0
);
const countryLabel = normalizeCountryLabel(
data.countryName || fallback.countryLabel,
`Country #${countryId}`
);
const serviceCode = normalizeNexSmsServiceCode(
data.serviceCode || fallback.serviceCode || DEFAULT_NEX_SMS_SERVICE_CODE,
DEFAULT_NEX_SMS_SERVICE_CODE
);
return {
activationId: phoneNumber,
phoneNumber,
provider: PHONE_SMS_PROVIDER_NEXSMS,
serviceCode,
countryId,
countryLabel,
successfulUses: normalizeUseCount(fallback.successfulUses ?? 0),
maxUses: 1,
};
}
async function requestNexSmsActivation(state = {}, options = {}) {
const config = resolvePhoneConfig(state);
const allCountryCandidates = Array.isArray(config.countryCandidates) && config.countryCandidates.length
? config.countryCandidates
: resolveNexSmsCountryCandidates(state);
if (!allCountryCandidates.length) {
throw new Error(`步骤 ${getActivePhoneVerificationVisibleStep()}:NexSMS 未选择国家,请先在接码设置中至少选择 1 个国家。`);
}
const blockedCountryIds = new Set(
(Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : [])
.map((value) => normalizeNexSmsCountryId(value, -1))
.filter((id) => id >= 0)
);
let countryCandidates = allCountryCandidates.filter((entry) => {
const id = normalizeNexSmsCountryId(entry.id, -1);
return id >= 0 && !blockedCountryIds.has(id);
});
if (!countryCandidates.length) {
countryCandidates = allCountryCandidates;
if (blockedCountryIds.size) {
await addLog(
'步骤 9:已选国家均达到临时收码失败跳过阈值,本轮解除跳过并重新尝试。',
'warn'
);
}
}
const acquirePriority = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
const priceRange = resolvePhonePriceRange(state, PHONE_SMS_PROVIDER_NEXSMS);
if (priceRange.invalidRange) {
throw new Error(
`NexSMS 价格区间无效:最低购买价 ${priceRange.minPriceLimit} 高于价格上限 ${priceRange.maxPriceLimit}`
);
}
const minPriceLimit = priceRange.minPriceLimit;
const maxPriceLimit = priceRange.maxPriceLimit;
const hasPriceBounds = priceRange.hasMinPriceLimit || priceRange.hasMaxPriceLimit;
const preferredPriceTier = normalizeHeroSmsPriceLimit(state?.heroSmsPreferredPrice);
const countryPriceFloorByCountryId = normalizeCountryPriceFloorMap(
options?.countryPriceFloorByCountryId,
(value) => String(normalizeNexSmsCountryId(value, -1))
);
const configuredAcquireRounds = normalizePhoneActivationRetryRounds(state?.heroSmsActivationRetryRounds);
const maxAcquireRounds = Math.max(2, configuredAcquireRounds);
const retryDelayMs = normalizePhoneActivationRetryDelayMs(state?.heroSmsActivationRetryDelayMs);
let finalNoNumbersByCountry = [];
let finalLastError = null;
for (let round = 1; round <= maxAcquireRounds; round += 1) {
if (maxAcquireRounds > 1) {
await addLog(
`步骤 9:NexSMS 正在获取手机号(第 ${round}/${maxAcquireRounds} 轮)...`,
'info'
);
}
const candidateAttempts = [];
for (const [index, countryConfig] of countryCandidates.entries()) {
candidateAttempts.push({
index,
countryConfig,
pricePlan: null,
orderingPrice: Number.POSITIVE_INFINITY,
});
}
if (
(acquirePriority === HERO_SMS_ACQUIRE_PRIORITY_PRICE || acquirePriority === HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH)
&& candidateAttempts.length > 1
) {
for (const attempt of candidateAttempts) {
try {
const pricePlan = await resolveNexSmsCountryPricePlan(config, attempt.countryConfig, state);
attempt.pricePlan = pricePlan;
const orderedForRanking = reorderPriceCandidates(pricePlan.prices, acquirePriority, preferredPriceTier);
const rangeFilteredForRanking = filterPriceCandidatesWithinRange(
orderedForRanking,
minPriceLimit,
maxPriceLimit
);
const rankingPrices = rangeFilteredForRanking.length
? rangeFilteredForRanking
: (hasPriceBounds ? [] : orderedForRanking);
attempt.orderingPrice = Array.isArray(rankingPrices) && rankingPrices.length
? Number(rankingPrices[0])
: Number.POSITIVE_INFINITY;
} catch (error) {
attempt.pricePlan = null;
attempt.orderingPrice = Number.POSITIVE_INFINITY;
attempt.lookupError = error;
}
}
candidateAttempts.sort((left, right) => {
if (left.orderingPrice !== right.orderingPrice) {
return acquirePriority === HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH
? (right.orderingPrice - left.orderingPrice)
: (left.orderingPrice - right.orderingPrice);
}
return left.index - right.index;
});
const rankingSummary = candidateAttempts.map((attempt) => {
const id = normalizeNexSmsCountryId(attempt.countryConfig.id, -1);
const label = String(attempt.countryConfig.label || `Country #${id}`).trim() || `Country #${id}`;
return Number.isFinite(attempt.orderingPrice)
? `${label}:${attempt.orderingPrice}`
: `${label}:无`;
}).join(' | ');
await addLog(`步骤 9NexSMS 价格优先排序:${rankingSummary}`, 'info');
}
const noNumbersByCountry = [];
const retryableNoNumberCountries = [];
let lastError = null;
for (const attempt of candidateAttempts) {
const countryId = normalizeNexSmsCountryId(attempt.countryConfig.id, -1);
const countryLabel = normalizeCountryLabel(attempt.countryConfig.label, `Country #${countryId}`);
const countryPriceFloor = countryPriceFloorByCountryId.get(String(countryId)) ?? null;
let pricePlan = attempt.pricePlan;
if (!pricePlan) {
try {
pricePlan = await resolveNexSmsCountryPricePlan(config, attempt.countryConfig, state);
} catch (error) {
if (isNexSmsTerminalError(error?.payload || error?.message, error?.status)) {
throw createPhoneSmsActionFailureError('NexSMS price lookup', describeNexSmsPayload(error?.payload || error?.message) || 'unknown terminal error', error?.payload, error?.status);
}
lastError = error;
continue;
}
}
if (!Array.isArray(pricePlan.prices) || !pricePlan.prices.length) {
if (
pricePlan.userLimit !== null
&& pricePlan.minCatalogPrice !== null
&& pricePlan.minCatalogPrice > pricePlan.userLimit
) {
noNumbersByCountry.push(
`${countryLabel}: 价格上限 ${pricePlan.userLimit} 内暂无可用号码;平台最低价=${pricePlan.minCatalogPrice}`
);
} else {
const reason = describeNexSmsPayload(pricePlan.rawPayload) || '无可用价格档位';
noNumbersByCountry.push(`${countryLabel}: ${reason}`);
retryableNoNumberCountries.push(countryLabel);
}
continue;
}
const orderedPrices = reorderPriceCandidates(pricePlan.prices, acquirePriority, preferredPriceTier);
const rangeFilteredPrices = filterPriceCandidatesWithinRange(
orderedPrices,
minPriceLimit,
maxPriceLimit
);
const candidatePrices = rangeFilteredPrices.length
? rangeFilteredPrices
: (hasPriceBounds ? [] : orderedPrices);
const floorFilteredPrices = filterPriceCandidatesAboveFloor(candidatePrices, countryPriceFloor);
const hasCountryPriceFloor = (
countryPriceFloor !== null
&& Number.isFinite(Number(countryPriceFloor))
&& Number(countryPriceFloor) > 0
);
const hasAlternativeCountries = candidateAttempts.some((entry) => (
normalizeNexSmsCountryId(entry?.countryConfig?.id, -1)
!== normalizeNexSmsCountryId(attempt?.countryConfig?.id, -1)
));
// With an explicit floor, only try higher tiers. If none exist, we should
// move to country/provider fallback instead of retrying the same tier again.
const pricesToTry = hasCountryPriceFloor
? (
floorFilteredPrices.length
? floorFilteredPrices
: (hasAlternativeCountries ? [] : candidatePrices.slice(0, 1))
)
: (floorFilteredPrices.length ? floorFilteredPrices : candidatePrices);
if (!pricesToTry.length) {
if (priceRange.hasMinPriceLimit && !rangeFilteredPrices.length) {
noNumbersByCountry.push(
`${countryLabel}: 价格区间 ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)} 内暂无可用号码`
);
continue;
}
if (
countryPriceFloor !== null
&& Array.isArray(pricePlan.prices)
&& pricePlan.prices.length > 0
) {
noNumbersByCountry.push(
`${countryLabel}: 当前回退尝试没有高于 ${countryPriceFloor} 的价格档位`
);
} else {
noNumbersByCountry.push(`${countryLabel}: ${describeNexSmsPayload(pricePlan.rawPayload) || '暂无可用号码'}`);
retryableNoNumberCountries.push(countryLabel);
}
continue;
}
for (const price of pricesToTry) {
try {
const payload = await fetchNexSmsPayload(
config,
'/api/order/purchase',
'NexSMS purchase',
{
method: 'POST',
body: {
serviceCode: config.serviceCode,
countryId,
quantity: 1,
price,
},
}
);
if (!isNexSmsSuccessPayload(payload)) {
if (isNexSmsNoNumbersError(payload)) {
continue;
}
if (isNexSmsTerminalError(payload)) {
throw createPhoneSmsActionFailureError('NexSMS purchase', describeNexSmsPayload(payload) || 'empty response');
}
lastError = createPhoneSmsActionFailureError('NexSMS purchase', describeNexSmsPayload(payload) || 'empty response');
continue;
}
const activation = parseNexSmsActivationPayload(payload, {
countryId,
countryLabel,
serviceCode: config.serviceCode,
});
if (!activation) {
lastError = new Error('NexSMS 购买成功,但未返回手机号。');
continue;
}
const numericPrice = Number(price);
rememberActivationAcquiredPrice(activation, numericPrice);
return activation;
} catch (error) {
if (isNexSmsTerminalError(error?.payload || error?.message, error?.status)) {
throw createPhoneSmsActionFailureError('NexSMS purchase', describeNexSmsPayload(error?.payload || error?.message) || 'unknown terminal error', error?.payload, error?.status);
}
if (isNexSmsNoNumbersError(error?.payload || error?.message)) {
continue;
}
lastError = error;
}
}
const fallbackReason = describeNexSmsPayload(pricePlan.rawPayload) || '暂无可用号码';
noNumbersByCountry.push(`${countryLabel}: ${fallbackReason}`);
retryableNoNumberCountries.push(countryLabel);
}
finalNoNumbersByCountry = noNumbersByCountry;
finalLastError = lastError;
if (
noNumbersByCountry.length
&& round < maxAcquireRounds
&& retryableNoNumberCountries.length > 0
) {
await addLog(
`步骤 9:NexSMS 暂无可用号码(第 ${round}/${maxAcquireRounds} 轮);${Math.ceil(retryDelayMs / 1000)} 秒后重试。国家:${retryableNoNumberCountries.join(', ')}`,
'warn'
);
await sleepWithStop(retryDelayMs);
continue;
}
break;
}
if (finalNoNumbersByCountry.length) {
throw new Error(
`NexSMS 已尝试 ${countryCandidates.length} 个候选国家,均无可用号码:${finalNoNumbersByCountry.join(' | ')}`
);
}
if (finalLastError) {
throw finalLastError;
}
throw new Error('NexSMS 获取手机号失败。');
}
async function requestPhoneActivation(state = {}, options = {}) {
state = await mergeLatestPhoneSettingsState(state, {
preservePhoneSmsProvider: Boolean(options?.preservePhoneSmsProvider),
});
if (normalizePhoneSmsProvider(state?.phoneSmsProvider) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
return provider.requestActivation(state, options);
}
}
const config = resolvePhoneConfig(state);
if (config.provider === PHONE_SMS_PROVIDER_5SIM) {
return requestFiveSimActivation(state, options);
}
if (config.provider === PHONE_SMS_PROVIDER_NEXSMS) {
return requestNexSmsActivation(state, options);
}
const allCountryCandidates = Array.isArray(config.countryCandidates) && config.countryCandidates.length
? config.countryCandidates
: resolveCountryCandidates(state);
if (!allCountryCandidates.length) {
throw new Error(`步骤 ${getActivePhoneVerificationVisibleStep()}:HeroSMS 未选择国家,请先在接码设置中至少选择 1 个国家。`);
}
const blockedCountryIds = new Set(
(Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : [])
.map((value) => normalizeCountryId(value, 0))
.filter((id) => id > 0)
);
let countryCandidates = allCountryCandidates.filter(
(entry) => !blockedCountryIds.has(normalizeCountryId(entry.id, 0))
);
if (!countryCandidates.length) {
countryCandidates = allCountryCandidates;
if (blockedCountryIds.size) {
await addLog(
'步骤 9:已选国家均达到临时收码失败跳过阈值,本轮解除跳过并重新尝试。',
'warn'
);
}
}
const acquirePriority = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
const priceRange = resolvePhonePriceRange(state, PHONE_SMS_PROVIDER_HERO);
if (priceRange.invalidRange) {
throw new Error(
`HeroSMS 价格区间无效:最低购买价 ${priceRange.minPriceLimit} 高于价格上限 ${priceRange.maxPriceLimit}`
);
}
const minPriceLimit = priceRange.minPriceLimit;
const maxPriceLimit = priceRange.maxPriceLimit;
const hasPriceBounds = priceRange.hasMinPriceLimit || priceRange.hasMaxPriceLimit;
const preferredPriceTier = normalizeHeroSmsPriceLimit(state?.heroSmsPreferredPrice);
const countryPriceFloorByCountryId = normalizeCountryPriceFloorMap(
options?.countryPriceFloorByCountryId,
(value) => String(normalizeCountryId(value, 0))
);
const requestActions = ['getNumber', 'getNumberV2'];
const configuredAcquireRounds = normalizePhoneActivationRetryRounds(
state?.heroSmsActivationRetryRounds
);
const maxAcquireRounds = Math.max(2, configuredAcquireRounds);
const retryDelayMs = normalizePhoneActivationRetryDelayMs(
state?.heroSmsActivationRetryDelayMs
);
let finalNoNumbersByCountry = [];
let finalLastError = null;
let finalLastFailureText = '';
for (let round = 1; round <= maxAcquireRounds; round += 1) {
if (maxAcquireRounds > 1) {
await addLog(
`步骤 9:HeroSMS 正在获取手机号(第 ${round}/${maxAcquireRounds} 轮)...`,
'info'
);
}
const countryAttempts = countryCandidates.map((countryConfig, index) => ({
index,
countryConfig,
pricePlan: null,
orderingPrice: Number.POSITIVE_INFINITY,
}));
if (
acquirePriority === HERO_SMS_ACQUIRE_PRIORITY_PRICE
|| acquirePriority === HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH
) {
for (const attempt of countryAttempts) {
const pricePlan = await resolvePhoneActivationPricePlan(config, attempt.countryConfig, state);
const orderedPrices = reorderPriceCandidates(pricePlan?.prices, acquirePriority, preferredPriceTier);
const rangeFilteredForRanking = filterPriceCandidatesWithinRange(
orderedPrices,
minPriceLimit,
maxPriceLimit
);
const rankingPrices = rangeFilteredForRanking.length
? rangeFilteredForRanking
: (hasPriceBounds ? [] : orderedPrices);
const numericPrices = Array.isArray(orderedPrices)
? rankingPrices
.map((value) => Number(value))
.filter((value) => Number.isFinite(value) && value > 0)
: [];
const candidateOrderingPrice = numericPrices.length ? numericPrices[0] : null;
const cappedByUserLimit = (
pricePlan?.userLimit !== null
&& pricePlan?.userLimit !== undefined
&& pricePlan?.minCatalogPrice !== null
&& pricePlan?.minCatalogPrice !== undefined
&& Number(pricePlan.minCatalogPrice) > Number(pricePlan.userLimit)
);
attempt.pricePlan = pricePlan;
attempt.orderingPrice = cappedByUserLimit
? Number.POSITIVE_INFINITY
: (candidateOrderingPrice !== null ? candidateOrderingPrice : Number.POSITIVE_INFINITY);
}
}
if (
(acquirePriority === HERO_SMS_ACQUIRE_PRIORITY_PRICE || acquirePriority === HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH)
&& countryAttempts.length > 1
) {
countryAttempts.sort((left, right) => {
if (left.orderingPrice !== right.orderingPrice) {
return acquirePriority === HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH
? (right.orderingPrice - left.orderingPrice)
: (left.orderingPrice - right.orderingPrice);
}
return left.index - right.index;
});
}
const noNumbersByCountry = [];
const retryableNoNumberCountries = [];
let lastError = null;
let lastFailureText = '';
for (const attempt of countryAttempts) {
const countryConfig = attempt.countryConfig;
const countryIdKey = String(normalizeCountryId(countryConfig?.id, 0));
const countryPriceFloor = countryPriceFloorByCountryId.get(countryIdKey) ?? null;
const buildFallbackActivation = (requestAction) => ({
countryId: countryConfig.id,
...(requestAction === 'getNumberV2' ? { statusAction: 'getStatusV2' } : {}),
});
const pricePlan = attempt.pricePlan || await resolvePhoneActivationPricePlan(config, countryConfig, state);
let noNumbersObservedInCountry = false;
const orderedPrices = reorderPriceCandidates(pricePlan.prices, acquirePriority, preferredPriceTier);
const rangeFilteredPrices = filterPriceCandidatesWithinRange(
orderedPrices,
minPriceLimit,
maxPriceLimit
);
const candidatePrices = rangeFilteredPrices.length
? rangeFilteredPrices
: (hasPriceBounds ? [] : orderedPrices);
const floorFilteredPrices = filterPriceCandidatesAboveFloor(candidatePrices, countryPriceFloor);
const hasCountryPriceFloor = (
countryPriceFloor !== null
&& Number.isFinite(Number(countryPriceFloor))
&& Number(countryPriceFloor) > 0
);
const hasAlternativeCountries = countryAttempts.some((entry) => (
String(normalizeCountryId(entry?.countryConfig?.id, 0))
!== String(normalizeCountryId(countryConfig?.id, 0))
));
// Same rule as 5sim/NexSMS: once floor is set, never re-try lower/equal tiers.
// Keep a probe fallback only for HeroSMS (single-country/no-tier environments),
// so replacement-limit behavior remains stable while still allowing country fallback.
const pricesToTry = hasCountryPriceFloor
? (
floorFilteredPrices.length
? floorFilteredPrices
: (hasAlternativeCountries ? [] : candidatePrices.slice(0, 1))
)
: (floorFilteredPrices.length ? floorFilteredPrices : candidatePrices);
const rawTierText = Array.isArray(pricePlan?.prices) && pricePlan.prices.length
? pricePlan.prices
.map((value) => (value === null || value === undefined ? '自动' : String(value)))
.join(', ')
: '无';
await addLog(
`步骤 9HeroSMS ${countryConfig.label} 价格方案:档位=[${rawTierText}],用户上限=${pricePlan?.userLimit ?? '未设置'},目录最低价=${pricePlan?.minCatalogPrice ?? '未知'}`,
'info'
);
if (pricesToTry.length > 1 || countryPriceFloor !== null) {
const tierText = pricesToTry
.map((value) => (value === null || value === undefined ? '自动' : String(value)))
.join(', ');
await addLog(
`步骤 9HeroSMS ${countryConfig.label} 本轮候选价格:${tierText}${countryPriceFloor !== null ? `(高于 ${countryPriceFloor}` : ''}`,
'info'
);
}
if (!pricesToTry.length) {
if (priceRange.hasMinPriceLimit && !rangeFilteredPrices.length) {
noNumbersByCountry.push(
`${countryConfig.label}: 价格区间 ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)} 内暂无可用号码`
);
continue;
}
if (
countryPriceFloor !== null
&& Array.isArray(pricePlan.prices)
&& pricePlan.prices.length > 0
) {
noNumbersByCountry.push(
`${countryConfig.label}: 当前回退尝试没有高于 ${countryPriceFloor} 的价格档位`
);
continue;
}
if (
pricePlan.userLimit !== null
&& pricePlan.minCatalogPrice !== null
&& pricePlan.minCatalogPrice > pricePlan.userLimit
) {
noNumbersByCountry.push(
`${countryConfig.label}: 价格上限 ${pricePlan.userLimit} 内暂无可用号码;平台最低价=${pricePlan.minCatalogPrice}`
);
} else {
noNumbersByCountry.push(
`${countryConfig.label}: ${lastFailureText || 'NO_NUMBERS'}`
);
retryableNoNumberCountries.push(countryConfig.label);
}
continue;
}
for (const maxPrice of pricesToTry) {
for (const requestAction of requestActions) {
try {
const fixedPrice = !Boolean(pricePlan.syntheticUserLimitProbe);
await addLog(
`步骤 9HeroSMS ${countryConfig.label} 正在尝试${formatHeroSmsActionName(requestAction)},价格档位 ${maxPrice === null || maxPrice === undefined ? '自动' : maxPrice}`,
'info'
);
const payload = await requestPhoneActivationWithPrice(
config,
countryConfig,
requestAction,
maxPrice,
{
userLimit: pricePlan.userLimit,
userMinLimit: minPriceLimit,
fixedPrice,
}
);
const activation = parseActivationPayload(payload, buildFallbackActivation(requestAction));
if (activation) {
const numericPrice = Number(maxPrice);
rememberActivationAcquiredPrice(activation, numericPrice);
return {
...activation,
countryId: countryConfig.id,
};
}
const payloadText = describeHeroSmsPayload(payload);
if (isHeroSmsNoNumbersPayload(payload)) {
noNumbersObservedInCountry = true;
lastFailureText = payloadText || lastFailureText;
continue;
}
if (isHeroSmsTerminalError(payload)) {
throw createHeroSmsActionFailureError(requestAction, payloadText || 'empty response');
}
lastFailureText = payloadText || lastFailureText;
lastError = createHeroSmsActionFailureError(requestAction, payloadText || 'empty response');
} catch (error) {
if (error?.localizedPhoneSmsFailure) {
throw error;
}
const payloadOrMessage = error?.payload || error?.message;
if (isHeroSmsTerminalError(payloadOrMessage)) {
throw createHeroSmsActionFailureError(requestAction, payloadOrMessage || 'empty response');
}
if (isHeroSmsNoNumbersPayload(payloadOrMessage)) {
noNumbersObservedInCountry = true;
lastFailureText = describeHeroSmsPayload(payloadOrMessage) || lastFailureText;
continue;
}
lastFailureText = describeHeroSmsPayload(payloadOrMessage) || lastFailureText;
lastError = error;
}
}
}
if (noNumbersObservedInCountry) {
const tiersTriedText = pricesToTry
.map((value) => (value === null || value === undefined ? '自动' : String(value)))
.join(', ');
if (
pricePlan.userLimit !== null
&& pricePlan.minCatalogPrice !== null
&& pricePlan.minCatalogPrice > pricePlan.userLimit
) {
noNumbersByCountry.push(
`${countryConfig.label}: 价格上限 ${pricePlan.userLimit} 内暂无可用号码;平台最低价=${pricePlan.minCatalogPrice}`
);
} else {
noNumbersByCountry.push(
`${countryConfig.label}: ${lastFailureText || 'NO_NUMBERS'}${tiersTriedText ? `(已尝试档位:${tiersTriedText}` : ''}`
);
retryableNoNumberCountries.push(countryConfig.label);
}
continue;
}
}
finalNoNumbersByCountry = noNumbersByCountry;
finalLastError = lastError;
finalLastFailureText = lastFailureText;
if (
noNumbersByCountry.length
&& round < maxAcquireRounds
&& retryableNoNumberCountries.length > 0
) {
await addLog(
`步骤 9:HeroSMS 暂无可用号码(第 ${round}/${maxAcquireRounds} 轮);${Math.ceil(retryDelayMs / 1000)} 秒后重试。国家:${retryableNoNumberCountries.join(', ')}`,
'warn'
);
await addLog(
`步骤 9:HeroSMS 暂无可用号码(第 ${round}/${maxAcquireRounds} 轮),${Math.ceil(retryDelayMs / 1000)} 秒后重试。国家:${retryableNoNumberCountries.join(', ')}`,
'warn'
);
await sleepWithStop(retryDelayMs);
continue;
}
break;
}
if (finalNoNumbersByCountry.length) {
throw new Error(
`HeroSMS 已尝试 ${countryCandidates.length} 个候选国家,均无可用号码:${finalNoNumbersByCountry.join(' | ')}`
);
}
if (finalLastError) {
throw finalLastError;
}
throw new Error(`HeroSMS 获取手机号失败,最后状态:${finalLastFailureText || '未知'}`);
}
async function reactivatePhoneActivation(state = {}, activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
throw new Error('缺少可复用的手机号接码订单。');
}
if (getActivationProviderId(normalizedActivation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
return provider.reuseActivation(scopeStateToActivationProvider(state, normalizedActivation), normalizedActivation);
}
}
const scopedState = scopeStateToActivationProvider(state, normalizedActivation);
const config = resolvePhoneConfig(scopedState);
if (config.provider === PHONE_SMS_PROVIDER_5SIM) {
const payload = await fetchFiveSimPayload(
config,
`/user/check/${encodeURIComponent(normalizedActivation.activationId)}`,
'5sim reuse activation baseline'
);
return {
...normalizedActivation,
source: '5sim-retained-reuse',
ignoredPhoneCodeKeys: collectPhoneSmsCodeKeys(payload),
};
}
if (config.provider === PHONE_SMS_PROVIDER_NEXSMS) {
throw new Error('NexSMS 当前流程不支持复用手机号订单。');
}
const payload = await fetchHeroSmsPayload(config, {
action: 'reactivate',
id: normalizedActivation.activationId,
}, 'HeroSMS reactivate');
const nextActivation = parseActivationPayload(payload, normalizedActivation);
if (!nextActivation) {
const text = describeHeroSmsPayload(payload);
throw new Error(`HeroSMS 复用手机号失败:${text || '空响应'}`);
}
return nextActivation;
}
async function setPhoneActivationStatus(state = {}, activation, status, actionLabel) {
const normalizedActivation = normalizeActivation(activation);
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(scopeStateToActivationProvider(state, normalizedActivation));
if (config.provider === PHONE_SMS_PROVIDER_5SIM) {
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 (normalizedStatus === 6) {
return 'NexSMS complete skipped';
}
const payload = await fetchNexSmsPayload(
config,
'/api/close/activation',
actionLabel || 'NexSMS close activation',
{
method: 'POST',
body: {
phoneNumber: normalizedActivation.phoneNumber,
},
}
);
if (!isNexSmsSuccessPayload(payload)) {
throw createPhoneSmsActionFailureError('NexSMS close activation', describeNexSmsPayload(payload) || 'empty response');
}
return describeNexSmsPayload(payload);
}
const payload = await fetchHeroSmsPayload(config, {
action: 'setStatus',
id: normalizedActivation.activationId,
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) {
await provider.finishActivation(scopeStateToActivationProvider(state, activation), activation);
return;
}
}
await setPhoneActivationStatus(state, activation, 6, 'HeroSMS setStatus(6)');
}
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) {
await provider.cancelActivation(scopeStateToActivationProvider(state, activation), activation);
return;
}
}
await setPhoneActivationStatus(state, activation, 8, 'HeroSMS setStatus(8)');
} 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 isFiveSimRetainedReuseActivation(activation) {
const normalizedActivation = normalizeActivation(activation);
return Boolean(
normalizedActivation
&& normalizedActivation.provider === PHONE_SMS_PROVIDER_5SIM
&& normalizedActivation.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;
}
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
await provider.banActivation(scopeStateToActivationProvider(state, activation), activation);
return;
}
}
await setPhoneActivationStatus(state, activation, 8, 'HeroSMS setStatus(8)');
} catch (_) {
// Best-effort cleanup.
}
}
async function requestAdditionalPhoneSms(state = {}, activation) {
const config = resolvePhoneConfig(scopeStateToActivationProvider(state, activation));
if (config.provider !== PHONE_SMS_PROVIDER_HERO) {
return;
}
try {
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
// 5sim does not expose a HeroSMS-style setStatus(3) resend primitive.
return;
}
await setPhoneActivationStatus(state, activation, 3, 'HeroSMS setStatus(3)');
} catch (_) {
// Best-effort request only.
}
}
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: '免费复用手机号激活记录缺失。',
};
}
if (normalizedActivation.provider === PHONE_SMS_PROVIDER_5SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
let retainedActivation = null;
try {
retainedActivation = await provider.reuseActivation(state, normalizedActivation);
} catch (error) {
return {
ok: false,
reason: 'five_sim_reuse_check_failed',
message: error.message || '5sim 复用手机号基线检查失败。',
};
}
return {
ok: true,
activation: {
...retainedActivation,
source: 'free-auto-reuse',
},
};
}
return {
ok: true,
activation: {
...normalizedActivation,
source: 'free-auto-reuse',
},
};
}
if (!String(normalizedActivation.activationId || '').trim()) {
return {
ok: false,
reason: 'missing_activation_id',
message: '已保存的免费复用手机号缺少 HeroSMS 激活 ID,无法自动重新激活。',
};
}
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 自动复用设置订单状态'
);
} catch (error) {
return {
ok: false,
reason: 'set_status_failed',
message: error.message || 'HeroSMS 更新订单状态失败。',
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}`);
const statusText = describeHeroSmsPayload(payload);
lastStatus = statusText;
await addLog(
`步骤 9:自动白嫖复用号码 ${normalizedActivation.phoneNumber} 状态:${statusText || '空响应'}${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 订单在自动白嫖复用前已被取消。',
lastStatus,
prepareRound,
};
}
} catch (error) {
return {
ok: false,
reason: 'get_status_failed',
message: error.message || 'HeroSMS 查询短信状态失败。',
lastStatus,
prepareRound,
};
}
}
return {
ok: false,
reason: 'prepare_timeout',
message: `等待已保存手机号进入短信等待状态超时。最后状态:${lastStatus || '未知'}`,
lastStatus,
prepareRound,
};
}
async function pollPhoneActivationCode(state = {}, activation, options = {}) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
throw new Error('缺少手机号接码订单。');
}
if (getActivationProviderId(normalizedActivation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
return provider.pollActivationCode(scopeStateToActivationProvider(state, normalizedActivation), normalizedActivation, options);
}
}
const statusAction = resolveActivationStatusAction(normalizedActivation);
const scopedState = scopeStateToActivationProvider(state, normalizedActivation);
const config = resolvePhoneConfig(scopedState);
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 intervalMs = Math.max(1000, Number(options.intervalMs) || DEFAULT_PHONE_POLL_INTERVAL_MS);
const maxRoundsRaw = Math.floor(Number(options.maxRounds));
const maxRounds = Number.isFinite(maxRoundsRaw) && maxRoundsRaw > 0 ? maxRoundsRaw : 0;
const start = Date.now();
let lastResponse = '';
let pollCount = 0;
const extractVerificationCode = (rawCode) => {
const trimmed = String(rawCode || '').trim();
if (!trimmed) {
return '';
}
const digitMatch = trimmed.match(/\b(\d{4,8})\b/);
return digitMatch?.[1] || '';
};
const emitWaitingForCode = async (statusText) => {
if (typeof options.onWaitingForCode === 'function') {
await options.onWaitingForCode({
activation: normalizedActivation,
elapsedMs: Date.now() - start,
pollCount,
statusText,
timeoutMs,
});
}
};
if (config.provider === PHONE_SMS_PROVIDER_5SIM) {
while (Date.now() - start < timeoutMs) {
if (maxRounds > 0 && pollCount >= maxRounds) {
break;
}
throwIfStopped();
const payload = await fetchFiveSimPayload(
config,
`/user/check/${normalizedActivation.activationId}`,
'5sim check activation'
);
const text = describeFiveSimPayload(payload);
lastResponse = text;
pollCount += 1;
const smsList = Array.isArray(payload?.sms) ? payload.sms : [];
const ignoredPhoneCodeKeys = new Set(normalizeStringList(normalizedActivation.ignoredPhoneCodeKeys));
const directCode = extractVerificationCode(payload?.code || payload?.sms_code);
let smsCode = '';
for (let index = smsList.length - 1; index >= 0; index -= 1) {
const smsItem = smsList[index] || {};
if (ignoredPhoneCodeKeys.has(buildPhoneSmsCodeKey(smsItem))) {
continue;
}
smsCode = extractVerificationCode(smsItem?.code || smsItem?.text || smsItem?.message || '');
if (smsCode) {
break;
}
}
smsCode = directCode || smsCode;
if (smsCode) {
return smsCode;
}
const statusText = String(payload?.status || '').trim().toUpperCase();
if (/^(RECEIVED|PENDING|RETRY|PREPARE|WAITING)$/i.test(statusText) || !statusText) {
const waitingStatusText = statusText || text || 'PENDING';
if (typeof options.onStatus === 'function') {
await options.onStatus({
activation: normalizedActivation,
elapsedMs: Date.now() - start,
pollCount,
statusText: waitingStatusText,
timeoutMs,
});
}
await emitWaitingForCode(waitingStatusText);
await sleepWithStop(intervalMs);
continue;
}
if (/^(CANCELED|CANCELLED|BANNED|FINISHED|EXPIRED|TIMEOUT)$/i.test(statusText)) {
throw new Error(`5sim 订单在收到短信前已结束:${statusText}`);
}
throw createPhoneSmsActionFailureError('5sim check activation', text || statusText || 'empty response');
}
throw buildPhoneCodeTimeoutError(lastResponse);
}
if (config.provider === PHONE_SMS_PROVIDER_NEXSMS) {
while (Date.now() - start < timeoutMs) {
if (maxRounds > 0 && pollCount >= maxRounds) {
break;
}
throwIfStopped();
const payload = await fetchNexSmsPayload(
config,
'/api/sms/messages',
'NexSMS get sms messages',
{
query: {
phoneNumber: normalizedActivation.phoneNumber,
format: 'json_latest',
},
}
);
const text = describeNexSmsPayload(payload);
lastResponse = text;
pollCount += 1;
if (typeof options.onStatus === 'function') {
await options.onStatus({
activation: normalizedActivation,
elapsedMs: Date.now() - start,
pollCount,
statusText: text || 'PENDING',
timeoutMs,
});
}
if (isNexSmsSuccessPayload(payload)) {
const directCode = extractVerificationCode(payload?.data?.code || payload?.data?.text || '');
if (directCode) {
return directCode;
}
await emitWaitingForCode(text || 'PENDING');
await sleepWithStop(intervalMs);
continue;
}
if (isNexSmsPendingMessage(payload)) {
await emitWaitingForCode(text || 'PENDING');
await sleepWithStop(intervalMs);
continue;
}
if (isNexSmsTerminalError(payload)) {
throw createPhoneSmsActionFailureError('NexSMS get sms messages', text || 'unknown terminal error');
}
await emitWaitingForCode(text || 'PENDING');
await sleepWithStop(intervalMs);
}
throw buildPhoneCodeTimeoutError(lastResponse);
}
while (Date.now() - start < timeoutMs) {
if (maxRounds > 0 && pollCount >= maxRounds) {
break;
}
throwIfStopped();
const payload = await fetchHeroSmsPayload(config, {
action: statusAction,
id: normalizedActivation.activationId,
}, `HeroSMS ${statusAction}`);
const text = describeHeroSmsPayload(payload);
lastResponse = text;
pollCount += 1;
if (typeof options.onStatus === 'function') {
await options.onStatus({
activation: normalizedActivation,
elapsedMs: Date.now() - start,
pollCount,
statusText: text,
timeoutMs,
});
}
const v2Code = (
payload
&& typeof payload === 'object'
&& !Array.isArray(payload)
&& (
extractVerificationCode(payload.sms?.code)
|| extractVerificationCode(payload.call?.code)
)
);
if (v2Code) {
return v2Code;
}
const okMatch = text.match(/^STATUS_OK:(.+)$/i);
if (okMatch) {
const extractedCode = extractVerificationCode(okMatch[1] || '');
if (extractedCode) {
return extractedCode;
}
await emitWaitingForCode(text || 'STATUS_OK');
await sleepWithStop(intervalMs);
continue;
}
if (/^STATUS_(WAIT_CODE|WAIT_RETRY|WAIT_RESEND)(?::.+)?$/i.test(text)) {
await emitWaitingForCode(text);
await sleepWithStop(intervalMs);
continue;
}
if (statusAction === 'getStatusV2' && payload && typeof payload === 'object' && !Array.isArray(payload)) {
await emitWaitingForCode(text || 'PENDING');
await sleepWithStop(intervalMs);
continue;
}
if (/^STATUS_CANCEL$/i.test(text)) {
throw new Error('HeroSMS 订单在短信到达前已被取消。');
}
throw createHeroSmsActionFailureError(statusAction, text || 'empty response');
}
throw buildPhoneCodeTimeoutError(lastResponse);
}
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)) {
if (normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
return provider.resolveCountryCandidates(state);
}
return resolveFiveSimCountryCandidates(state);
}
if (normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_NEXSMS) {
return resolveNexSmsCountryCandidates(state);
}
return resolveCountryCandidates(state);
}
function resolveCountryConfigFromActivation(activation, fallbackState = {}) {
const providerId = getActivationProviderId(activation, fallbackState);
const candidates = resolveCountryCandidatesForProvider(fallbackState, providerId);
if (activation && typeof activation === 'object') {
if (providerId === PHONE_SMS_PROVIDER_FIVE_SIM) {
const countryId = normalizeFiveSimCountryId(activation.countryId, '');
if (countryId) {
const matched = candidates.find((entry) => String(entry.id) === countryId);
if (matched) return matched;
return {
id: countryId,
label: normalizeFiveSimCountryLabel(activation.countryLabel, countryId),
};
}
} else {
const inferredCountry = inferHeroSmsCountryFromPhoneNumber(activation.phoneNumber);
const rawCountryId = normalizeCountryId(activation.countryId, 0);
const hasExplicitCountry = Object.prototype.hasOwnProperty.call(activation, 'countryId')
&& Number.isFinite(rawCountryId)
&& rawCountryId > 0
&& !(activation.manualOnly && rawCountryId === HERO_SMS_COUNTRY_ID && inferredCountry?.id && inferredCountry.id !== rawCountryId);
const countryId = hasExplicitCountry ? rawCountryId : normalizeCountryId(inferredCountry?.id, rawCountryId);
const countryLabel = hasExplicitCountry
? activation.countryLabel
: (inferredCountry?.label || activation.countryLabel);
if (countryId > 0) {
const matched = candidates.find((entry) => entry.id === countryId);
if (matched) {
return matched;
}
return {
id: countryId,
label: normalizeCountryLabel(countryLabel, `Country #${countryId}`),
};
}
}
}
return candidates[0] || (providerId === PHONE_SMS_PROVIDER_FIVE_SIM
? { id: 'england', label: 'England' }
: 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 = 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 = {
[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 countryCandidates = resolveCountryCandidatesForProvider(state, provider);
if (
(provider === PHONE_SMS_PROVIDER_5SIM || provider === PHONE_SMS_PROVIDER_NEXSMS)
&& !countryCandidates.length
) {
throw new Error(`步骤 ${getActivePhoneVerificationVisibleStep()}${provider === PHONE_SMS_PROVIDER_5SIM ? '5sim' : 'NexSMS'} 未选择国家,请先在接码设置中至少选择 1 个国家。`);
}
const normalizeCountryKey = (value) => (
provider === PHONE_SMS_PROVIDER_5SIM
? normalizeFiveSimCountryCode(value, '')
: (
provider === PHONE_SMS_PROVIDER_NEXSMS
? String(normalizeNexSmsCountryId(value, -1))
: String(normalizeCountryId(value, 0))
)
);
const blockedCountryIds = new Set(
(Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : [])
.map((value) => normalizeCountryKey(value))
.filter((id) => (
provider === PHONE_SMS_PROVIDER_NEXSMS
? (id !== '' && id !== null && id !== undefined)
: Boolean(id && id !== '0')
))
);
const allowedCountryIds = new Set(
countryCandidates
.map((entry) => normalizeCountryKey(entry.id || entry.code))
.filter((id) => (
provider === PHONE_SMS_PROVIDER_NEXSMS
? (id !== '' && id !== null && id !== undefined && !blockedCountryIds.has(id))
: Boolean(id && id !== '0' && !blockedCountryIds.has(id))
))
);
const preferredCountryLabel = countryCandidates[0]?.label || (
provider === PHONE_SMS_PROVIDER_5SIM
? ''
: (
provider === PHONE_SMS_PROVIDER_NEXSMS
? ''
: HERO_SMS_COUNTRY_LABEL
)
);
const resolveCountryLabelById = (countryId) => {
const normalizedCountryKey = normalizeCountryKey(countryId);
return countryCandidates.find((entry) => normalizeCountryKey(entry.id || entry.code) === normalizedCountryKey)?.label
|| 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
&& (provider === PHONE_SMS_PROVIDER_HERO || provider === PHONE_SMS_PROVIDER_5SIM)
&& preferredActivation.provider === provider
&& !blockedCountryIds.has(normalizeCountryKey(preferredActivation.countryId))
&& allowedCountryIds.has(normalizeCountryKey(preferredActivation.countryId))
&& 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 && (provider === PHONE_SMS_PROVIDER_HERO || provider === PHONE_SMS_PROVIDER_5SIM)) {
for (const candidateActivation of reusableCandidates) {
if (candidateActivation.provider !== provider) {
continue;
}
if (isSameActivation(candidateActivation, failedPreferredActivation)) {
continue;
}
if (candidateActivation.successfulUses >= candidateActivation.maxUses) {
continue;
}
if (blockedCountryIds.has(normalizeCountryKey(candidateActivation.countryId))) {
continue;
}
if (!allowedCountryIds.has(normalizeCountryKey(candidateActivation.countryId))) {
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 reusableProvider = normalizedActivation.provider;
const canPersistReusableActivation = reusableProvider === PHONE_SMS_PROVIDER_HERO
|| reusableProvider === PHONE_SMS_PROVIDER_5SIM;
if (!canPersistReusableActivation) {
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);
return Boolean(
normalizedActivation
&& (
(
normalizedActivation.provider === PHONE_SMS_PROVIDER_HERO
&& normalizedActivation.source === 'hero-sms-new'
)
|| normalizedActivation.provider === PHONE_SMS_PROVIDER_5SIM
)
&& normalizedActivation.phoneCodeReceived
);
}
function shouldSkipTerminalStatusForFreeReuse(state, activation) {
if (isPhoneSignupIdentityState(state)) {
return false;
}
const normalizedActivation = normalizeActivation(activation);
if (
!normalizedActivation
|| (
normalizedActivation.provider !== PHONE_SMS_PROVIDER_HERO
&& normalizedActivation.provider !== PHONE_SMS_PROVIDER_5SIM
)
) {
return false;
}
if (isFreeAutoReuseActivation(normalizedActivation)) {
return true;
}
if (isFiveSimRetainedReuseActivation(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);
if (
!normalizedActivation
|| (
normalizedActivation.provider !== PHONE_SMS_PROVIDER_HERO
&& normalizedActivation.provider !== PHONE_SMS_PROVIDER_5SIM
)
|| !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() : {}),
};
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);
if (
!normalizedActivation
|| (
normalizedActivation.provider !== PHONE_SMS_PROVIDER_HERO
&& normalizedActivation.provider !== PHONE_SMS_PROVIDER_5SIM
)
|| 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 providerLabel = normalizedActivation.provider === PHONE_SMS_PROVIDER_5SIM
? '5sim'
: (normalizedActivation.provider === PHONE_SMS_PROVIDER_NEXSMS ? 'NexSMS' : 'HeroSMS');
const usePageResend = normalizedActivation.provider !== PHONE_SMS_PROVIDER_5SIM;
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(
`步骤 9OpenAI 无法向号码 ${normalizedActivation.phoneNumber} 发送短信,立即更换号码。${error.message}`,
'warn'
);
await clearPhoneRuntimeCountdown();
return {
code: '',
replaceNumber: true,
reason: 'resend_phone_banned',
};
}
if (isPhoneMaxUsageExceededFlowError(error)) {
await addLog(
`步骤 9OpenAI 提示号码 ${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(
`步骤 9OpenAI 无法向号码 ${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 || '');
if (normalizedProvider === PHONE_SMS_PROVIDER_5SIM) {
const normalizedCountryCode = normalizeFiveSimCountryCode(countryId, '');
return normalizedCountryCode ? `${normalizedProvider}:${normalizedCountryCode}` : '';
}
if (normalizedProvider === PHONE_SMS_PROVIDER_NEXSMS) {
const normalizedCountryId = normalizeNexSmsCountryId(countryId, -1);
return normalizedCountryId >= 0 ? `${normalizedProvider}:${normalizedCountryId}` : '';
}
const normalizedCountryId = normalizeCountryId(countryId, 0);
return normalizedCountryId > 0 ? `${normalizedProvider}:${normalizedCountryId}` : '';
};
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';
}
if (normalizedProvider === PHONE_SMS_PROVIDER_5SIM) {
const matched = resolveFiveSimCountryCandidates(state)
.find((entry) => String(entry.id || entry.code || '') === normalizedCountryKey);
return matched?.label || normalizedCountryKey || 'Unknown country';
}
if (normalizedProvider === PHONE_SMS_PROVIDER_NEXSMS) {
const normalizedCountryId = normalizeNexSmsCountryId(normalizedCountryKey, -1);
const matched = resolveNexSmsCountryCandidates(state)
.find((entry) => normalizeNexSmsCountryId(entry.id, -1) === normalizedCountryId);
return matched?.label || `Country #${normalizedCountryId}`;
}
const normalizedCountryId = normalizeCountryId(normalizedCountryKey, 0);
const matched = resolveCountryCandidates(state)
.find((entry) => normalizeCountryId(entry.id, 0) === normalizedCountryId);
return matched?.label || `Country #${normalizedCountryId}`;
};
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 getCountryFailureKey = (countryId, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) => (
normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_FIVE_SIM
? normalizeFiveSimCountryId(countryId, '')
: String(normalizeCountryId(countryId, 0) || '')
);
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 countryKey = normalizeCountryFailureKey(
normalizedActivation.countryId,
normalizedActivation.provider
);
if (!countryKey) {
return;
}
const floorPrice = normalizeHeroSmsPrice(
normalizedActivation.price
?? normalizedActivation.maxPrice
?? normalizedActivation.selectedPrice
?? 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 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'
);
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} 反复返回添加手机号页。`
);
}
if (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} 被目标站拒绝。`
);
}
if (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');
}
if (shouldCancelActivation && activation) {
await cancelPhoneActivation(state, activation);
}
if (shouldRetireFreeReusableActivationOnFailure(await getState(), activation)) {
await retireFreeReusableActivation(
`自动白嫖复用号码 ${activation.phoneNumber} 在失败后被更换。`
);
}
if (isPhoneNumberUsedError(replaceReason)) {
await discardPhoneActivationFromReuse(
`目标站拒绝该号码(${replaceReason})。`,
activation,
await getState()
);
}
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,
};
});