refactor: 收敛接码 provider 生命周期边界

This commit is contained in:
Isulew
2026-05-29 15:04:19 +08:00
parent e50b0fbfeb
commit 26b1b15cd6
5 changed files with 2804 additions and 3428 deletions
+739
View File
@@ -9,6 +9,15 @@
const DEFAULT_COUNTRY_ID = 1;
const DEFAULT_COUNTRY_LABEL = 'Country #1';
const DEFAULT_REQUEST_TIMEOUT_MS = 20000;
const DEFAULT_ACQUIRE_RETRY_ROUNDS = 3;
const DEFAULT_ACQUIRE_RETRY_DELAY_MS = 2000;
const DEFAULT_POLL_TIMEOUT_MS = 180000;
const DEFAULT_POLL_INTERVAL_MS = 5000;
const MAX_PRICE_CANDIDATES = 8;
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
const ACQUIRE_PRIORITY_COUNTRY = 'country';
const ACQUIRE_PRIORITY_PRICE = 'price';
const ACQUIRE_PRIORITY_PRICE_HIGH = 'price_high';
function normalizeBaseUrl(value = '') {
const trimmed = String(value || '').trim() || DEFAULT_BASE_URL;
@@ -39,6 +48,11 @@
return normalizeText(value, fallback);
}
function normalizeCountryKey(value) {
const countryId = normalizeNexSmsCountryId(value, -1);
return countryId >= 0 ? String(countryId) : '';
}
function normalizeNexSmsServiceCode(value = '', fallback = DEFAULT_SERVICE_CODE) {
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
if (normalized) {
@@ -48,6 +62,44 @@
return fallbackNormalized || DEFAULT_SERVICE_CODE;
}
function normalizePriceLimit(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 normalizeAcquirePriority(value = '') {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === ACQUIRE_PRIORITY_PRICE) {
return ACQUIRE_PRIORITY_PRICE;
}
if (normalized === ACQUIRE_PRIORITY_PRICE_HIGH) {
return ACQUIRE_PRIORITY_PRICE_HIGH;
}
return ACQUIRE_PRIORITY_COUNTRY;
}
function normalizeActivationRetryRounds(value) {
const parsed = Math.floor(Number(value));
if (!Number.isFinite(parsed) || parsed <= 0) {
return DEFAULT_ACQUIRE_RETRY_ROUNDS;
}
return Math.max(1, Math.min(10, parsed));
}
function normalizeActivationRetryDelayMs(value) {
const parsed = Math.floor(Number(value));
if (!Number.isFinite(parsed) || parsed <= 0) {
return DEFAULT_ACQUIRE_RETRY_DELAY_MS;
}
return Math.max(500, Math.min(30000, parsed));
}
function normalizeNexSmsCountryOrder(value = []) {
const source = Array.isArray(value)
? value
@@ -94,6 +146,13 @@
}];
}
function resolveCountryLabel(state = {}, countryId = DEFAULT_COUNTRY_ID) {
const countryKey = normalizeCountryKey(countryId);
const matched = resolveCountryCandidates(state)
.find((entry) => normalizeCountryKey(entry.id) === countryKey);
return matched?.label || (countryKey ? `Country #${countryKey}` : DEFAULT_COUNTRY_LABEL);
}
function parsePayload(text) {
const trimmed = String(text || '').trim();
if (!trimmed) {
@@ -131,6 +190,24 @@
return Number(payload.code) === 0;
}
function isNoNumbersError(payloadOrMessage) {
const text = describePayload(payloadOrMessage);
return /numbers?\s+not\s+found|暂无可用|no\s+numbers|no\s+stock|库存.*0|not\s+available/i.test(text);
}
function isPendingMessage(payloadOrMessage) {
const text = describePayload(payloadOrMessage);
return /no\s+sms|暂无短信|waiting|not\s+arrived|empty|未收到|短信为空|no\s+records/i.test(text);
}
function isTerminalError(payloadOrMessage, status = 0) {
if (Number(status) === 401 || Number(status) === 403) {
return true;
}
const text = describePayload(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 resolveConfig(state = {}, deps = {}) {
return {
apiKey: normalizeText(state?.nexSmsApiKey),
@@ -226,14 +303,648 @@
});
}
function buildSortedUniquePriceCandidates(values = []) {
return Array.from(
new Set(
values
.map((value) => Number(value))
.filter((value) => Number.isFinite(value) && value > 0)
.map((value) => Math.round(value * 10000) / 10000)
)
)
.sort((left, right) => left - right)
.slice(0, MAX_PRICE_CANDIDATES);
}
function collectPriceCandidates(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);
}
function resolvePriceRange(state = {}) {
const minPriceLimit = normalizePriceLimit(state?.nexSmsMinPrice ?? state?.heroSmsMinPrice);
const maxPriceLimit = normalizePriceLimit(state?.nexSmsMaxPrice ?? state?.heroSmsMaxPrice);
return {
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 filterPriceCandidatesAboveFloor(prices = [], minExclusivePrice = null) {
const floor = normalizePriceLimit(minExclusivePrice);
if (floor === null || floor <= 0) {
return Array.isArray(prices) ? [...prices] : [];
}
return (Array.isArray(prices) ? prices : []).filter((value) => {
const numeric = Number(value);
return Number.isFinite(numeric) && numeric > floor;
});
}
function reorderPriceCandidates(prices = [], acquirePriority = ACQUIRE_PRIORITY_COUNTRY, preferredPrice = null) {
const ordered = buildSortedUniquePriceCandidates(prices);
if (acquirePriority === ACQUIRE_PRIORITY_PRICE_HIGH) {
ordered.reverse();
}
const preferred = normalizePriceLimit(preferredPrice);
if (preferred === null) {
return ordered;
}
return [preferred, ...ordered.filter((price) => price !== preferred)];
}
function normalizePriceFloorMap(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 = normalizePriceLimit(rawPrice);
if (normalizedPrice === null || normalizedPrice <= 0) {
return;
}
normalizedMap.set(countryKey, normalizedPrice);
});
return normalizedMap;
}
function formatPriceRangeText(minPriceLimit = null, maxPriceLimit = null) {
const minPrice = normalizePriceLimit(minPriceLimit);
const maxPrice = normalizePriceLimit(maxPriceLimit);
if (minPrice !== null && maxPrice !== null) {
return `${minPrice}~${maxPrice}`;
}
if (minPrice !== null) {
return `${minPrice}~`;
}
if (maxPrice !== null) {
return `~${maxPrice}`;
}
return 'unbounded';
}
async function resolveCountryPricePlan(state = {}, countryConfig = {}, deps = {}) {
const config = resolveConfig(state, deps);
const countryId = normalizeNexSmsCountryId(countryConfig?.id, -1);
if (countryId < 0) {
throw new Error(`NexSMS 国家 ID 无效:${countryConfig?.id}`);
}
const payload = await fetchPayload(config, '/api/getCountryByService', 'NexSMS getCountryByService', {
query: {
serviceCode: config.serviceCode,
countryId,
},
});
if (!isSuccessPayload(payload)) {
throw new Error(`NexSMS getCountryByService 失败:${describePayload(payload) || 'empty response'}`);
}
const countryData = payload && typeof payload === 'object' && !Array.isArray(payload)
? (payload.data || {})
: {};
const countryLabel = normalizeNexSmsCountryLabel(
countryData.countryName || countryConfig?.label,
`Country #${countryId}`
);
const prices = collectPriceCandidates(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 priceRange = resolvePriceRange(state);
const filteredPrices = filterPriceCandidatesWithinRange(
prices,
priceRange.minPriceLimit,
priceRange.maxPriceLimit
);
return {
countryId,
countryLabel,
prices: filteredPrices,
userLimit: priceRange.maxPriceLimit,
minCatalogPrice,
rawPayload: payload,
};
}
function normalizeActivation(payload, fallback = {}) {
const source = payload && typeof payload === 'object' && !Array.isArray(payload)
? payload
: {};
const data = isSuccessPayload(source) ? (source.data || {}) : source;
const phoneCandidates = Array.isArray(data.phoneNumbers)
? data.phoneNumbers
: (Array.isArray(data.numbers) ? data.numbers : []);
const phoneNumber = normalizeText(
data.phoneNumber
|| data.phone
|| phoneCandidates[0]
|| source.phoneNumber
|| source.phone
|| fallback.phoneNumber
);
if (!phoneNumber) {
return null;
}
const countryId = normalizeNexSmsCountryId(
data.countryId ?? source.countryId ?? fallback.countryId,
DEFAULT_COUNTRY_ID
);
return {
activationId: normalizeText(data.activationId || source.activationId || fallback.activationId, phoneNumber),
phoneNumber,
provider: PROVIDER_ID,
serviceCode: normalizeNexSmsServiceCode(
data.serviceCode || source.serviceCode || fallback.serviceCode || DEFAULT_SERVICE_CODE,
DEFAULT_SERVICE_CODE
),
countryId,
countryLabel: normalizeNexSmsCountryLabel(
data.countryName || source.countryName || data.countryLabel || source.countryLabel || fallback.countryLabel,
`Country #${countryId}`
),
successfulUses: Math.max(0, Math.floor(Number(fallback.successfulUses) || 0)),
maxUses: 1,
...(fallback.selectedPrice !== undefined ? { selectedPrice: Number(fallback.selectedPrice) } : {}),
...(fallback.price !== undefined ? { price: Number(fallback.price) } : {}),
...(fallback.maxPrice !== undefined ? { maxPrice: Number(fallback.maxPrice) } : {}),
};
}
function resolveActivationCountry(activation = {}, state = {}) {
const normalizedActivation = normalizeActivation(activation)
|| (activation && typeof activation === 'object' ? activation : {});
const countryId = normalizeNexSmsCountryId(
normalizedActivation.countryId ?? normalizedActivation.country,
DEFAULT_COUNTRY_ID
);
const matched = resolveCountryCandidates(state)
.find((entry) => normalizeNexSmsCountryId(entry.id, -1) === countryId);
if (matched) {
return matched;
}
return {
id: countryId,
label: normalizeNexSmsCountryLabel(normalizedActivation.countryLabel, `Country #${countryId}`),
};
}
function getActivationCountryKey(activation = {}) {
return normalizeCountryKey(activation?.countryId ?? activation?.country);
}
function getActivationPrice(activation = {}) {
return Number.isFinite(Number(activation?.selectedPrice))
? Math.round(Number(activation.selectedPrice) * 10000) / 10000
: null;
}
async function requestActivation(state = {}, options = {}, deps = {}) {
const config = resolveConfig(state, deps);
const allCountryCandidates = resolveCountryCandidates(state);
if (!allCountryCandidates.length) {
throw new Error('步骤 9: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 && typeof deps.addLog === 'function') {
await deps.addLog('步骤 9:已选国家均达到临时收码失败跳过阈值,本轮解除跳过并重新尝试。', 'warn');
}
}
const acquirePriority = normalizeAcquirePriority(state?.heroSmsAcquirePriority);
const priceRange = resolvePriceRange(state);
if (priceRange.invalidRange) {
throw new Error(`NexSMS 价格区间无效:最低购买价 ${priceRange.minPriceLimit} 高于价格上限 ${priceRange.maxPriceLimit}`);
}
const hasPriceBounds = priceRange.hasMinPriceLimit || priceRange.hasMaxPriceLimit;
const preferredPriceTier = normalizePriceLimit(state?.heroSmsPreferredPrice);
const countryPriceFloorByCountryId = normalizePriceFloorMap(
options?.countryPriceFloorByCountryId,
(value) => String(normalizeNexSmsCountryId(value, -1))
);
const maxAcquireRounds = Math.max(2, normalizeActivationRetryRounds(state?.heroSmsActivationRetryRounds));
const retryDelayMs = normalizeActivationRetryDelayMs(state?.heroSmsActivationRetryDelayMs);
let finalNoNumbersByCountry = [];
let finalLastError = null;
for (let round = 1; round <= maxAcquireRounds; round += 1) {
if (maxAcquireRounds > 1 && typeof deps.addLog === 'function') {
await deps.addLog(`步骤 9:NexSMS 正在获取手机号(第 ${round}/${maxAcquireRounds} 轮)...`, 'info');
}
const candidateAttempts = countryCandidates.map((countryConfig, index) => ({
index,
countryConfig,
pricePlan: null,
orderingPrice: Number.POSITIVE_INFINITY,
}));
if (
(acquirePriority === ACQUIRE_PRIORITY_PRICE || acquirePriority === ACQUIRE_PRIORITY_PRICE_HIGH)
&& candidateAttempts.length > 1
) {
for (const attempt of candidateAttempts) {
try {
const pricePlan = await resolveCountryPricePlan(state, attempt.countryConfig, deps);
attempt.pricePlan = pricePlan;
const orderedForRanking = reorderPriceCandidates(pricePlan.prices, acquirePriority, preferredPriceTier);
const rangeFilteredForRanking = filterPriceCandidatesWithinRange(
orderedForRanking,
priceRange.minPriceLimit,
priceRange.maxPriceLimit
);
const rankingPrices = rangeFilteredForRanking.length
? rangeFilteredForRanking
: (hasPriceBounds ? [] : orderedForRanking);
attempt.orderingPrice = Array.isArray(rankingPrices) && rankingPrices.length
? Number(rankingPrices[0])
: Number.POSITIVE_INFINITY;
} catch (error) {
attempt.lookupError = error;
}
}
candidateAttempts.sort((left, right) => {
if (left.orderingPrice !== right.orderingPrice) {
return acquirePriority === ACQUIRE_PRIORITY_PRICE_HIGH
? (right.orderingPrice - left.orderingPrice)
: (left.orderingPrice - right.orderingPrice);
}
return left.index - right.index;
});
if (typeof deps.addLog === 'function') {
const rankingSummary = candidateAttempts.map((attempt) => {
const id = normalizeNexSmsCountryId(attempt.countryConfig.id, -1);
const label = normalizeText(attempt.countryConfig.label, `Country #${id}`);
return Number.isFinite(attempt.orderingPrice)
? `${label}:${attempt.orderingPrice}`
: `${label}:无`;
}).join(' | ');
await deps.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 = normalizeNexSmsCountryLabel(attempt.countryConfig.label, `Country #${countryId}`);
const countryPriceFloor = countryPriceFloorByCountryId.get(String(countryId)) ?? null;
let pricePlan = attempt.pricePlan;
if (!pricePlan) {
try {
pricePlan = await resolveCountryPricePlan(state, attempt.countryConfig, deps);
} catch (error) {
if (isTerminalError(error?.payload || error?.message, error?.status)) {
throw new Error(`NexSMS price lookup 失败:${describePayload(error?.payload || error?.message) || 'unknown terminal error'}`);
}
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 = describePayload(pricePlan.rawPayload) || '无可用价格档位';
noNumbersByCountry.push(`${countryLabel}: ${reason}`);
retryableNoNumberCountries.push(countryLabel);
}
continue;
}
const orderedPrices = reorderPriceCandidates(pricePlan.prices, acquirePriority, preferredPriceTier);
const rangeFilteredPrices = filterPriceCandidatesWithinRange(
orderedPrices,
priceRange.minPriceLimit,
priceRange.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)
));
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}: 价格区间 ${formatPriceRangeText(priceRange.minPriceLimit, priceRange.maxPriceLimit)} 内暂无可用号码`);
continue;
}
if (
countryPriceFloor !== null
&& Array.isArray(pricePlan.prices)
&& pricePlan.prices.length > 0
) {
noNumbersByCountry.push(`${countryLabel}: 当前回退尝试没有高于 ${countryPriceFloor} 的价格档位`);
} else {
noNumbersByCountry.push(`${countryLabel}: ${describePayload(pricePlan.rawPayload) || '暂无可用号码'}`);
retryableNoNumberCountries.push(countryLabel);
}
continue;
}
let boughtActivation = null;
for (const price of pricesToTry) {
try {
const payload = await fetchPayload(config, '/api/order/purchase', 'NexSMS purchase', {
method: 'POST',
body: {
serviceCode: config.serviceCode,
countryId,
quantity: 1,
price,
},
});
if (!isSuccessPayload(payload)) {
if (isNoNumbersError(payload)) {
continue;
}
if (isTerminalError(payload)) {
throw new Error(`NexSMS purchase 失败:${describePayload(payload) || 'empty response'}`);
}
lastError = new Error(`NexSMS purchase 失败:${describePayload(payload) || 'empty response'}`);
continue;
}
boughtActivation = normalizeActivation(payload, {
countryId,
countryLabel,
serviceCode: config.serviceCode,
selectedPrice: price,
});
if (!boughtActivation) {
lastError = new Error('NexSMS 购买成功,但未返回手机号。');
continue;
}
return boughtActivation;
} catch (error) {
if (isTerminalError(error?.payload || error?.message, error?.status)) {
throw new Error(`NexSMS purchase 失败:${describePayload(error?.payload || error?.message) || 'unknown terminal error'}`);
}
if (isNoNumbersError(error?.payload || error?.message)) {
continue;
}
lastError = error;
}
}
if (!boughtActivation) {
const fallbackReason = describePayload(pricePlan.rawPayload) || '暂无可用号码';
noNumbersByCountry.push(`${countryLabel}: ${fallbackReason}`);
retryableNoNumberCountries.push(countryLabel);
}
}
finalNoNumbersByCountry = noNumbersByCountry;
finalLastError = lastError;
if (
noNumbersByCountry.length
&& round < maxAcquireRounds
&& retryableNoNumberCountries.length > 0
) {
if (typeof deps.addLog === 'function') {
await deps.addLog(
`步骤 9:NexSMS 暂无可用号码(第 ${round}/${maxAcquireRounds} 轮);${Math.ceil(retryDelayMs / 1000)} 秒后重试。国家:${retryableNoNumberCountries.join(', ')}`,
'warn'
);
}
await deps.sleepWithStop?.(retryDelayMs);
continue;
}
break;
}
if (finalNoNumbersByCountry.length) {
throw new Error(`NexSMS 已尝试 ${countryCandidates.length} 个候选国家,均无可用号码:${finalNoNumbersByCountry.join(' | ')}`);
}
if (finalLastError) {
throw finalLastError;
}
throw new Error('NexSMS 获取手机号失败。');
}
function extractVerificationCode(value = '') {
const text = String(value || '').trim();
if (!text) {
return '';
}
return text.match(/\b(\d{4,8})\b/)?.[1] || '';
}
async function pollActivationCode(state = {}, activation, options = {}, deps = {}) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
throw new Error('缺少手机号接码订单。');
}
const config = resolveConfig(state, deps);
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || DEFAULT_POLL_TIMEOUT_MS);
const intervalMs = Math.max(1000, Number(options.intervalMs) || DEFAULT_POLL_INTERVAL_MS);
const maxRoundsRaw = Math.floor(Number(options.maxRounds));
const maxRounds = Number.isFinite(maxRoundsRaw) && maxRoundsRaw > 0 ? maxRoundsRaw : 0;
const start = Date.now();
let pollCount = 0;
let lastResponse = '';
const emitWaitingForCode = async (statusText) => {
if (typeof options.onWaitingForCode === 'function') {
await options.onWaitingForCode({
activation: normalizedActivation,
elapsedMs: Date.now() - start,
pollCount,
statusText,
timeoutMs,
});
}
};
while (Date.now() - start < timeoutMs) {
if (maxRounds > 0 && pollCount >= maxRounds) {
break;
}
deps.throwIfStopped?.();
const payload = await fetchPayload(config, '/api/sms/messages', 'NexSMS get sms messages', {
query: {
phoneNumber: normalizedActivation.phoneNumber,
format: 'json_latest',
},
});
const text = describePayload(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 (isSuccessPayload(payload)) {
const code = extractVerificationCode(payload?.data?.code || payload?.data?.text || payload?.data?.message || '');
if (code) {
return code;
}
await emitWaitingForCode(text || 'PENDING');
await deps.sleepWithStop?.(intervalMs);
continue;
}
if (isPendingMessage(payload)) {
await emitWaitingForCode(text || 'PENDING');
await deps.sleepWithStop?.(intervalMs);
continue;
}
if (isTerminalError(payload)) {
throw new Error(`NexSMS get sms messages 失败:${text || 'unknown terminal error'}`);
}
await emitWaitingForCode(text || 'PENDING');
await deps.sleepWithStop?.(intervalMs);
}
throw new Error(`${PHONE_CODE_TIMEOUT_ERROR_PREFIX}等待手机验证码超时。${lastResponse ? ` NexSMS 最后状态:${lastResponse}` : ''}`);
}
async function closeActivation(state = {}, activation, deps = {}, actionLabel = 'NexSMS close activation') {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
return '';
}
const config = resolveConfig(state, deps);
const payload = await fetchPayload(config, '/api/close/activation', actionLabel, {
method: 'POST',
body: {
phoneNumber: normalizedActivation.phoneNumber,
},
});
if (!isSuccessPayload(payload)) {
throw new Error(`NexSMS close activation 失败:${describePayload(payload) || 'empty response'}`);
}
return describePayload(payload);
}
async function reuseActivation() {
throw new Error('NexSMS 当前流程不支持复用手机号订单。');
}
async function finishActivation() {
return 'NexSMS complete skipped';
}
async function cancelActivation(state = {}, activation, deps = {}) {
return closeActivation(state, activation, deps, 'NexSMS close activation');
}
async function banActivation(state = {}, activation, deps = {}) {
return closeActivation(state, activation, deps, 'NexSMS close activation');
}
async function requestAdditionalSms() {
return '';
}
async function rotateActivation(state = {}, activation, options = {}, deps = {}) {
const releaseAction = String(options?.releaseAction || '').trim().toLowerCase() === 'ban'
? 'ban'
: 'cancel';
if (releaseAction === 'ban') {
await banActivation(state, activation, deps);
} else {
await cancelActivation(state, activation, deps);
}
return {
currentTicketId: String(activation?.activationId || activation?.phoneNumber || ''),
nextActivation: null,
};
}
function createProvider(deps = {}) {
const providerDeps = {
fetchImpl: deps.fetchImpl,
sleepWithStop: deps.sleepWithStop,
throwIfStopped: deps.throwIfStopped,
addLog: deps.addLog,
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
};
const capabilities = Object.freeze({
supportsReusableActivation: false,
supportsAutomaticFreeReuse: false,
supportsFreeReusePreservation: false,
supportsPageResend: true,
supportsPageResendProbe: true,
requiresCountrySelection: true,
});
return {
id: PROVIDER_ID,
label: 'NexSMS',
capabilities,
defaultCountryId: DEFAULT_COUNTRY_ID,
defaultCountryLabel: DEFAULT_COUNTRY_LABEL,
defaultProduct: DEFAULT_SERVICE_LABEL,
@@ -241,12 +952,37 @@
normalizeCountryId: normalizeNexSmsCountryId,
normalizeCountryLabel: normalizeNexSmsCountryLabel,
normalizeCountryOrder: normalizeNexSmsCountryOrder,
normalizeCountryKey,
normalizeServiceCode: normalizeNexSmsServiceCode,
normalizeActivation,
resolveCountryCandidates,
resolveCountryLabel,
resolveActivationCountry,
getActivationCountryKey,
getActivationPrice,
requestActivation: (state, options) => requestActivation(state, options, providerDeps),
reuseActivation: (state, activation) => reuseActivation(state, activation, providerDeps),
finishActivation: (state, activation) => finishActivation(state, activation, providerDeps),
cancelActivation: (state, activation) => cancelActivation(state, activation, providerDeps),
banActivation: (state, activation) => banActivation(state, activation, providerDeps),
requestAdditionalSms: (state, activation) => requestAdditionalSms(state, activation, providerDeps),
rotateActivation: (state, activation, options) => rotateActivation(state, activation, options, providerDeps),
pollActivationCode: (state, activation, options) => pollActivationCode(state, activation, options, providerDeps),
prepareActivationForReuse: reuseActivation,
canPersistReusableActivation: () => false,
canPreserveActivationForFreeReuse: () => false,
shouldUsePageResend: () => true,
shouldProbePageResend: () => true,
fetchBalance: (state) => fetchBalance(state, providerDeps),
fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, providerDeps),
resolveCountryPricePlan: (state, countryConfig) => resolveCountryPricePlan(state, countryConfig, providerDeps),
resolvePriceRange,
formatPriceRangeText,
describePayload,
isSuccessPayload,
isNoNumbersError,
isPendingMessage,
isTerminalError,
};
}
@@ -263,6 +999,9 @@
normalizeNexSmsCountryId,
normalizeNexSmsCountryLabel,
normalizeNexSmsCountryOrder,
normalizeCountryKey,
normalizeNexSmsServiceCode,
normalizeActivation,
resolveActivationCountry,
};
});