修正手机接码价格区间与最低价逻辑
This commit is contained in:
@@ -874,6 +874,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
heroSmsApiKey: '',
|
heroSmsApiKey: '',
|
||||||
heroSmsReuseEnabled: DEFAULT_HERO_SMS_REUSE_ENABLED,
|
heroSmsReuseEnabled: DEFAULT_HERO_SMS_REUSE_ENABLED,
|
||||||
heroSmsAcquirePriority: DEFAULT_HERO_SMS_ACQUIRE_PRIORITY,
|
heroSmsAcquirePriority: DEFAULT_HERO_SMS_ACQUIRE_PRIORITY,
|
||||||
|
heroSmsMinPrice: '',
|
||||||
heroSmsMaxPrice: '',
|
heroSmsMaxPrice: '',
|
||||||
heroSmsPreferredPrice: '',
|
heroSmsPreferredPrice: '',
|
||||||
heroSmsCountryId: HERO_SMS_COUNTRY_ID,
|
heroSmsCountryId: HERO_SMS_COUNTRY_ID,
|
||||||
@@ -885,6 +886,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
fiveSimCountryLabel: FIVE_SIM_COUNTRY_LABEL,
|
fiveSimCountryLabel: FIVE_SIM_COUNTRY_LABEL,
|
||||||
fiveSimCountryFallback: [],
|
fiveSimCountryFallback: [],
|
||||||
fiveSimCountryOrder: [...DEFAULT_FIVE_SIM_COUNTRY_ORDER],
|
fiveSimCountryOrder: [...DEFAULT_FIVE_SIM_COUNTRY_ORDER],
|
||||||
|
fiveSimMinPrice: '',
|
||||||
fiveSimMaxPrice: '',
|
fiveSimMaxPrice: '',
|
||||||
fiveSimOperator: FIVE_SIM_OPERATOR,
|
fiveSimOperator: FIVE_SIM_OPERATOR,
|
||||||
nexSmsApiKey: '',
|
nexSmsApiKey: '',
|
||||||
@@ -2796,6 +2798,7 @@ function normalizePersistentSettingValue(key, value) {
|
|||||||
return Boolean(value);
|
return Boolean(value);
|
||||||
case 'heroSmsAcquirePriority':
|
case 'heroSmsAcquirePriority':
|
||||||
return normalizeHeroSmsAcquirePriority(value);
|
return normalizeHeroSmsAcquirePriority(value);
|
||||||
|
case 'heroSmsMinPrice':
|
||||||
case 'heroSmsMaxPrice':
|
case 'heroSmsMaxPrice':
|
||||||
return normalizeHeroSmsMaxPrice(value);
|
return normalizeHeroSmsMaxPrice(value);
|
||||||
case 'heroSmsPreferredPrice':
|
case 'heroSmsPreferredPrice':
|
||||||
@@ -2823,6 +2826,7 @@ function normalizePersistentSettingValue(key, value) {
|
|||||||
return normalizeFiveSimCountryFallback(value);
|
return normalizeFiveSimCountryFallback(value);
|
||||||
case 'fiveSimCountryOrder':
|
case 'fiveSimCountryOrder':
|
||||||
return normalizeFiveSimCountryOrder(value);
|
return normalizeFiveSimCountryOrder(value);
|
||||||
|
case 'fiveSimMinPrice':
|
||||||
case 'fiveSimMaxPrice':
|
case 'fiveSimMaxPrice':
|
||||||
return normalizeFiveSimMaxPrice(value);
|
return normalizeFiveSimMaxPrice(value);
|
||||||
case 'fiveSimOperator':
|
case 'fiveSimOperator':
|
||||||
|
|||||||
@@ -396,6 +396,60 @@
|
|||||||
return Math.round(parsed * 10000) / 10000;
|
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) {
|
function isPhoneNumberUsedError(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -2030,7 +2084,7 @@
|
|||||||
if (!text) {
|
if (!text) {
|
||||||
return false;
|
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|step\s*9:\s*(?:5sim|nexsms)\s+countries\s+are\s+empty|\bNO_NUMBERS\b/i.test(text);
|
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 = []) {
|
function resolveNoSupplyDiagnosticsContext(state = {}, providerOrder = []) {
|
||||||
@@ -2040,22 +2094,37 @@
|
|||||||
const heroCountryCount = resolveCountryCandidates(state).length;
|
const heroCountryCount = resolveCountryCandidates(state).length;
|
||||||
const fiveSimCountryCount = resolveFiveSimCountryCandidates(state).length;
|
const fiveSimCountryCount = resolveFiveSimCountryCandidates(state).length;
|
||||||
const nexSmsCountryCount = resolveNexSmsCountryCandidates(state).length;
|
const nexSmsCountryCount = resolveNexSmsCountryCandidates(state).length;
|
||||||
const maxPrice = normalizeHeroSmsPriceLimit(state?.heroSmsMaxPrice);
|
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);
|
const acquirePriority = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
|
||||||
return {
|
return {
|
||||||
order,
|
order,
|
||||||
heroCountryCount,
|
heroCountryCount,
|
||||||
fiveSimCountryCount,
|
fiveSimCountryCount,
|
||||||
nexSmsCountryCount,
|
nexSmsCountryCount,
|
||||||
|
minPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
|
priceRangeInvalid: priceRange.invalidRange,
|
||||||
|
priceRangeText: formatPhonePriceRangeText(minPrice, maxPrice),
|
||||||
acquirePriority,
|
acquirePriority,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatNoSupplySuggestion(context = {}) {
|
function formatNoSupplySuggestion(context = {}) {
|
||||||
const suggestions = [];
|
const suggestions = [];
|
||||||
|
const minPrice = Number(context?.minPrice);
|
||||||
const maxPrice = Number(context?.maxPrice);
|
const maxPrice = Number(context?.maxPrice);
|
||||||
if (!Number.isFinite(maxPrice) || maxPrice <= 0) {
|
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)');
|
suggestions.push('先设置价格上限(建议 >= 0.12)');
|
||||||
} else if (maxPrice < 0.12) {
|
} else if (maxPrice < 0.12) {
|
||||||
suggestions.push('先提高价格上限(当前偏低)');
|
suggestions.push('先提高价格上限(当前偏低)');
|
||||||
@@ -2118,11 +2187,13 @@
|
|||||||
latestState && typeof latestState === 'object' ? latestState : state,
|
latestState && typeof latestState === 'object' ? latestState : state,
|
||||||
providerOrder
|
providerOrder
|
||||||
);
|
);
|
||||||
|
const minPriceText = context.minPrice === null ? '未设置' : String(context.minPrice);
|
||||||
const maxPriceText = context.maxPrice === null ? '未设置' : String(context.maxPrice);
|
const maxPriceText = context.maxPrice === null ? '未设置' : String(context.maxPrice);
|
||||||
|
const priceRangeText = context.priceRangeText || formatPhonePriceRangeText(context.minPrice, context.maxPrice);
|
||||||
const providerOrderText = context.order.join(' > ');
|
const providerOrderText = context.order.join(' > ');
|
||||||
const suggestion = formatNoSupplySuggestion(context);
|
const suggestion = formatNoSupplySuggestion(context);
|
||||||
await addLog(
|
await addLog(
|
||||||
`Step 9 diagnostics: 无号连续失败 ${nextStreak} 次;maxPrice=${maxPriceText};providerOrder=${providerOrderText};国家数 HeroSMS=${context.heroCountryCount}, 5sim=${context.fiveSimCountryCount}, NexSMS=${context.nexSmsCountryCount}。建议:${suggestion}。`,
|
`Step 9 diagnostics: 无号连续失败 ${nextStreak} 次;priceRange=${priceRangeText};minPrice=${minPriceText};maxPrice=${maxPriceText};providerOrder=${providerOrderText};国家数 HeroSMS=${context.heroCountryCount}, 5sim=${context.fiveSimCountryCount}, NexSMS=${context.nexSmsCountryCount}。建议:${suggestion}。`,
|
||||||
nextStreak >= 2 ? 'warn' : 'info'
|
nextStreak >= 2 ? 'warn' : 'info'
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
@@ -2229,6 +2300,7 @@
|
|||||||
let retriedWithUpdatedPrice = false;
|
let retriedWithUpdatedPrice = false;
|
||||||
let retriedWithoutPrice = false;
|
let retriedWithoutPrice = false;
|
||||||
const userLimit = normalizeHeroSmsPriceLimit(options.userLimit);
|
const userLimit = normalizeHeroSmsPriceLimit(options.userLimit);
|
||||||
|
const userMinLimit = normalizeHeroSmsPriceLimit(options.userMinLimit);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
@@ -2249,6 +2321,11 @@
|
|||||||
`HeroSMS ${action} failed: WRONG_MAX_PRICE requires ${updatedMaxPrice}, which exceeds configured maxPrice=${userLimit}.`
|
`HeroSMS ${action} failed: WRONG_MAX_PRICE requires ${updatedMaxPrice}, which exceeds configured maxPrice=${userLimit}.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (userMinLimit !== null && updatedMaxPrice < userMinLimit) {
|
||||||
|
throw new Error(
|
||||||
|
`HeroSMS ${action} failed: WRONG_MAX_PRICE requires ${updatedMaxPrice}, which is below configured minPrice=${userMinLimit}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
nextMaxPrice = updatedMaxPrice;
|
nextMaxPrice = updatedMaxPrice;
|
||||||
retriedWithUpdatedPrice = true;
|
retriedWithUpdatedPrice = true;
|
||||||
continue;
|
continue;
|
||||||
@@ -2326,7 +2403,7 @@
|
|||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findLowestFiveSimPrice(payload, product = DEFAULT_FIVE_SIM_PRODUCT, countryCode = '') {
|
function findLowestFiveSimPrice(payload, product = DEFAULT_FIVE_SIM_PRODUCT, countryCode = '', priceRange = {}) {
|
||||||
const normalizedProduct = normalizeFiveSimCountryCode(product, DEFAULT_FIVE_SIM_PRODUCT);
|
const normalizedProduct = normalizeFiveSimCountryCode(product, DEFAULT_FIVE_SIM_PRODUCT);
|
||||||
const normalizedCountryCode = normalizeFiveSimCountryCode(countryCode, '');
|
const normalizedCountryCode = normalizeFiveSimCountryCode(countryCode, '');
|
||||||
const root = payload && typeof payload === 'object'
|
const root = payload && typeof payload === 'object'
|
||||||
@@ -2337,7 +2414,11 @@
|
|||||||
? (root?.[normalizedCountryCode] || root)
|
? (root?.[normalizedCountryCode] || root)
|
||||||
: root
|
: root
|
||||||
);
|
);
|
||||||
const candidates = collectFiveSimPriceCandidates(countryPayload, []);
|
const candidates = filterPriceCandidatesWithinRange(
|
||||||
|
buildSortedUniquePriceCandidates(collectFiveSimPriceCandidates(countryPayload, [])),
|
||||||
|
priceRange?.minPriceLimit ?? null,
|
||||||
|
priceRange?.maxPriceLimit ?? null
|
||||||
|
);
|
||||||
if (!candidates.length) {
|
if (!candidates.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -2372,7 +2453,7 @@
|
|||||||
return /not\s+enough\s+balance|no\s+balance|unauthorized|invalid\s+token|forbidden|bad\s+key|wrong\s+key|banned/i.test(text);
|
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) {
|
async function resolveFiveSimLowestPrice(config, countryCode, priceRange = {}) {
|
||||||
try {
|
try {
|
||||||
const payload = await fetchFiveSimPayload(
|
const payload = await fetchFiveSimPayload(
|
||||||
config,
|
config,
|
||||||
@@ -2385,7 +2466,7 @@
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return findLowestFiveSimPrice(payload, config.product, countryCode);
|
return findLowestFiveSimPrice(payload, config.product, countryCode, priceRange);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -2467,9 +2548,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxPriceLimit = config.maxPriceLimit === undefined
|
const priceRange = resolvePhonePriceRange(state, PHONE_SMS_PROVIDER_5SIM);
|
||||||
? normalizeHeroSmsPriceLimit(state.heroSmsMaxPrice)
|
if (priceRange.invalidRange) {
|
||||||
: config.maxPriceLimit;
|
throw new Error(
|
||||||
|
`5sim price range is invalid: minPrice=${priceRange.minPriceLimit} exceeds maxPrice=${priceRange.maxPriceLimit}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const maxPriceLimit = priceRange.maxPriceLimit;
|
||||||
|
const minPriceLimit = priceRange.minPriceLimit;
|
||||||
|
const hasPriceBounds = priceRange.hasMinPriceLimit || priceRange.hasMaxPriceLimit;
|
||||||
const acquirePriority = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
|
const acquirePriority = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
|
||||||
const preferredPriceTier = normalizeHeroSmsPriceLimit(state?.heroSmsPreferredPrice);
|
const preferredPriceTier = normalizeHeroSmsPriceLimit(state?.heroSmsPreferredPrice);
|
||||||
const countryPriceFloorByCountryCode = normalizeCountryPriceFloorMap(
|
const countryPriceFloorByCountryCode = normalizeCountryPriceFloorMap(
|
||||||
@@ -2504,7 +2591,7 @@
|
|||||||
const rankedCandidates = [];
|
const rankedCandidates = [];
|
||||||
for (const [index, countryConfig] of countryCandidates.entries()) {
|
for (const [index, countryConfig] of countryCandidates.entries()) {
|
||||||
const countryCode = normalizeFiveSimCountryCode(countryConfig.code || countryConfig.id || '', 'thailand');
|
const countryCode = normalizeFiveSimCountryCode(countryConfig.code || countryConfig.id || '', 'thailand');
|
||||||
const lowestPrice = await resolveFiveSimLowestPrice(config, countryCode);
|
const lowestPrice = await resolveFiveSimLowestPrice(config, countryCode, priceRange);
|
||||||
rankedCandidates.push({
|
rankedCandidates.push({
|
||||||
index,
|
index,
|
||||||
countryConfig,
|
countryConfig,
|
||||||
@@ -2593,11 +2680,13 @@
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
const boundedPriceCandidates = maxPriceLimit === null
|
const rangeFilteredPriceCandidates = filterPriceCandidatesWithinRange(
|
||||||
? rawPriceCandidates
|
rawPriceCandidates,
|
||||||
: rawPriceCandidates.filter((price) => Number(price) <= maxPriceLimit);
|
minPriceLimit,
|
||||||
|
maxPriceLimit
|
||||||
|
);
|
||||||
const orderedPricesFromCatalog = reorderPriceCandidates(
|
const orderedPricesFromCatalog = reorderPriceCandidates(
|
||||||
boundedPriceCandidates,
|
rangeFilteredPriceCandidates,
|
||||||
acquirePriority,
|
acquirePriority,
|
||||||
preferredPriceTier
|
preferredPriceTier
|
||||||
);
|
);
|
||||||
@@ -2610,8 +2699,16 @@
|
|||||||
]
|
]
|
||||||
: orderedPricesFromCatalog
|
: orderedPricesFromCatalog
|
||||||
)
|
)
|
||||||
: (maxPriceLimit !== null ? [maxPriceLimit] : [null]);
|
: (
|
||||||
const floorFilteredPrices = filterPriceCandidatesAboveFloor(orderedPrices, countryPriceFloor);
|
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 = (
|
const hasCountryPriceFloor = (
|
||||||
countryPriceFloor !== null
|
countryPriceFloor !== null
|
||||||
&& Number.isFinite(Number(countryPriceFloor))
|
&& Number.isFinite(Number(countryPriceFloor))
|
||||||
@@ -2628,13 +2725,21 @@
|
|||||||
? (
|
? (
|
||||||
floorFilteredPrices.length
|
floorFilteredPrices.length
|
||||||
? floorFilteredPrices
|
? floorFilteredPrices
|
||||||
: (hasAlternativeCountries ? [] : orderedPrices.slice(0, 1))
|
: (hasAlternativeCountries ? [] : candidatePrices.slice(0, 1))
|
||||||
)
|
)
|
||||||
: (floorFilteredPrices.length ? floorFilteredPrices : orderedPrices);
|
: (floorFilteredPrices.length ? floorFilteredPrices : candidatePrices);
|
||||||
|
|
||||||
if (!pricesToTry.length) {
|
if (!pricesToTry.length) {
|
||||||
const lowestCatalog = rawPriceCandidates.length ? rawPriceCandidates[0] : null;
|
const lowestCatalog = rawPriceCandidates.length ? rawPriceCandidates[0] : null;
|
||||||
if (
|
if (
|
||||||
|
minPriceLimit !== null
|
||||||
|
&& !rangeFilteredPriceCandidates.length
|
||||||
|
&& rawPriceCandidates.length
|
||||||
|
) {
|
||||||
|
noNumbersByCountry.push(
|
||||||
|
`${countryLabel}: no numbers within price range ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)}; visible tiers=${rawPriceCandidates.join(', ')}`
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
maxPriceLimit !== null
|
maxPriceLimit !== null
|
||||||
&& lowestCatalog !== null
|
&& lowestCatalog !== null
|
||||||
&& Number(lowestCatalog) > Number(maxPriceLimit)
|
&& Number(lowestCatalog) > Number(maxPriceLimit)
|
||||||
@@ -2642,7 +2747,7 @@
|
|||||||
noNumbersByCountry.push(
|
noNumbersByCountry.push(
|
||||||
`${countryLabel}: no numbers within maxPrice=${maxPriceLimit}; lowest listed=${lowestCatalog}`
|
`${countryLabel}: no numbers within maxPrice=${maxPriceLimit}; lowest listed=${lowestCatalog}`
|
||||||
);
|
);
|
||||||
} else if (countryPriceFloor !== null && boundedPriceCandidates.length) {
|
} else if (countryPriceFloor !== null && rangeFilteredPriceCandidates.length) {
|
||||||
noNumbersByCountry.push(
|
noNumbersByCountry.push(
|
||||||
`${countryLabel}: no higher price tier above ${countryPriceFloor} for current fallback attempt`
|
`${countryLabel}: no higher price tier above ${countryPriceFloor} for current fallback attempt`
|
||||||
);
|
);
|
||||||
@@ -2716,10 +2821,20 @@
|
|||||||
return acquiredActivation;
|
return acquiredActivation;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lowestPrice = rawPriceCandidates.length ? rawPriceCandidates[0] : await resolveFiveSimLowestPrice(config, countryCode);
|
const lowestCatalogPrice = rawPriceCandidates.length
|
||||||
if (maxPriceLimit !== null && lowestPrice !== null && Number(lowestPrice) > Number(maxPriceLimit)) {
|
? rawPriceCandidates[0]
|
||||||
|
: await resolveFiveSimLowestPrice(config, countryCode);
|
||||||
|
if (
|
||||||
|
minPriceLimit !== null
|
||||||
|
&& !rangeFilteredPriceCandidates.length
|
||||||
|
&& rawPriceCandidates.length
|
||||||
|
) {
|
||||||
noNumbersByCountry.push(
|
noNumbersByCountry.push(
|
||||||
`${countryLabel}: no numbers within maxPrice=${maxPriceLimit}; lowest listed=${lowestPrice}`
|
`${countryLabel}: no numbers within price range ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)}; visible tiers=${rawPriceCandidates.join(', ')}`
|
||||||
|
);
|
||||||
|
} else if (maxPriceLimit !== null && lowestCatalogPrice !== null && Number(lowestCatalogPrice) > Number(maxPriceLimit)) {
|
||||||
|
noNumbersByCountry.push(
|
||||||
|
`${countryLabel}: no numbers within maxPrice=${maxPriceLimit}; lowest listed=${lowestCatalogPrice}`
|
||||||
);
|
);
|
||||||
} else if (isFiveSimRateLimitError(countryNoNumbersText)) {
|
} else if (isFiveSimRateLimitError(countryNoNumbersText)) {
|
||||||
rateLimitByCountry.push(`${countryLabel}: ${countryNoNumbersText || 'rate limit'}`);
|
rateLimitByCountry.push(`${countryLabel}: ${countryNoNumbersText || 'rate limit'}`);
|
||||||
@@ -2737,10 +2852,17 @@
|
|||||||
throw new Error(`5sim buy activation failed: ${describeFiveSimPayload(error?.payload || error?.message) || 'unknown terminal error'}`);
|
throw new Error(`5sim buy activation failed: ${describeFiveSimPayload(error?.payload || error?.message) || 'unknown terminal error'}`);
|
||||||
}
|
}
|
||||||
if (isFiveSimNoNumbersError(error?.payload || error?.message)) {
|
if (isFiveSimNoNumbersError(error?.payload || error?.message)) {
|
||||||
const lowestPrice = await resolveFiveSimLowestPrice(config, countryCode);
|
const lowestRangePrice = await resolveFiveSimLowestPrice(config, countryCode, priceRange);
|
||||||
if (maxPriceLimit !== null && lowestPrice !== null && lowestPrice > maxPriceLimit) {
|
const lowestCatalogPrice = lowestRangePrice === null
|
||||||
|
? await resolveFiveSimLowestPrice(config, countryCode)
|
||||||
|
: lowestRangePrice;
|
||||||
|
if (minPriceLimit !== null && lowestRangePrice === null) {
|
||||||
noNumbersByCountry.push(
|
noNumbersByCountry.push(
|
||||||
`${countryLabel}: no numbers within maxPrice=${maxPriceLimit}; lowest listed=${lowestPrice}`
|
`${countryLabel}: no numbers within price range ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)}`
|
||||||
|
);
|
||||||
|
} else if (maxPriceLimit !== null && lowestCatalogPrice !== null && lowestCatalogPrice > maxPriceLimit) {
|
||||||
|
noNumbersByCountry.push(
|
||||||
|
`${countryLabel}: no numbers within maxPrice=${maxPriceLimit}; lowest listed=${lowestCatalogPrice}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
noNumbersByCountry.push(`${countryLabel}: ${describeFiveSimPayload(error?.payload || error?.message) || 'no free phones'}`);
|
noNumbersByCountry.push(`${countryLabel}: ${describeFiveSimPayload(error?.payload || error?.message) || 'no free phones'}`);
|
||||||
@@ -2957,6 +3079,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const acquirePriority = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
|
const acquirePriority = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
|
||||||
|
const priceRange = resolvePhonePriceRange(state, PHONE_SMS_PROVIDER_NEXSMS);
|
||||||
|
if (priceRange.invalidRange) {
|
||||||
|
throw new Error(
|
||||||
|
`NexSMS price range is invalid: minPrice=${priceRange.minPriceLimit} exceeds maxPrice=${priceRange.maxPriceLimit}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const minPriceLimit = priceRange.minPriceLimit;
|
||||||
|
const maxPriceLimit = priceRange.maxPriceLimit;
|
||||||
|
const hasPriceBounds = priceRange.hasMinPriceLimit || priceRange.hasMaxPriceLimit;
|
||||||
const preferredPriceTier = normalizeHeroSmsPriceLimit(state?.heroSmsPreferredPrice);
|
const preferredPriceTier = normalizeHeroSmsPriceLimit(state?.heroSmsPreferredPrice);
|
||||||
const countryPriceFloorByCountryId = normalizeCountryPriceFloorMap(
|
const countryPriceFloorByCountryId = normalizeCountryPriceFloorMap(
|
||||||
options?.countryPriceFloorByCountryId,
|
options?.countryPriceFloorByCountryId,
|
||||||
@@ -2995,8 +3126,16 @@
|
|||||||
const pricePlan = await resolveNexSmsCountryPricePlan(config, attempt.countryConfig, state);
|
const pricePlan = await resolveNexSmsCountryPricePlan(config, attempt.countryConfig, state);
|
||||||
attempt.pricePlan = pricePlan;
|
attempt.pricePlan = pricePlan;
|
||||||
const orderedForRanking = reorderPriceCandidates(pricePlan.prices, acquirePriority, preferredPriceTier);
|
const orderedForRanking = reorderPriceCandidates(pricePlan.prices, acquirePriority, preferredPriceTier);
|
||||||
attempt.orderingPrice = Array.isArray(orderedForRanking) && orderedForRanking.length
|
const rangeFilteredForRanking = filterPriceCandidatesWithinRange(
|
||||||
? Number(orderedForRanking[0])
|
orderedForRanking,
|
||||||
|
minPriceLimit,
|
||||||
|
maxPriceLimit
|
||||||
|
);
|
||||||
|
const rankingPrices = rangeFilteredForRanking.length
|
||||||
|
? rangeFilteredForRanking
|
||||||
|
: (hasPriceBounds ? [] : orderedForRanking);
|
||||||
|
attempt.orderingPrice = Array.isArray(rankingPrices) && rankingPrices.length
|
||||||
|
? Number(rankingPrices[0])
|
||||||
: Number.POSITIVE_INFINITY;
|
: Number.POSITIVE_INFINITY;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
attempt.pricePlan = null;
|
attempt.pricePlan = null;
|
||||||
@@ -3061,7 +3200,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const orderedPrices = reorderPriceCandidates(pricePlan.prices, acquirePriority, preferredPriceTier);
|
const orderedPrices = reorderPriceCandidates(pricePlan.prices, acquirePriority, preferredPriceTier);
|
||||||
const floorFilteredPrices = filterPriceCandidatesAboveFloor(orderedPrices, countryPriceFloor);
|
const rangeFilteredPrices = filterPriceCandidatesWithinRange(
|
||||||
|
orderedPrices,
|
||||||
|
minPriceLimit,
|
||||||
|
maxPriceLimit
|
||||||
|
);
|
||||||
|
const candidatePrices = rangeFilteredPrices.length
|
||||||
|
? rangeFilteredPrices
|
||||||
|
: (hasPriceBounds ? [] : orderedPrices);
|
||||||
|
const floorFilteredPrices = filterPriceCandidatesAboveFloor(candidatePrices, countryPriceFloor);
|
||||||
const hasCountryPriceFloor = (
|
const hasCountryPriceFloor = (
|
||||||
countryPriceFloor !== null
|
countryPriceFloor !== null
|
||||||
&& Number.isFinite(Number(countryPriceFloor))
|
&& Number.isFinite(Number(countryPriceFloor))
|
||||||
@@ -3077,10 +3224,16 @@
|
|||||||
? (
|
? (
|
||||||
floorFilteredPrices.length
|
floorFilteredPrices.length
|
||||||
? floorFilteredPrices
|
? floorFilteredPrices
|
||||||
: (hasAlternativeCountries ? [] : orderedPrices.slice(0, 1))
|
: (hasAlternativeCountries ? [] : candidatePrices.slice(0, 1))
|
||||||
)
|
)
|
||||||
: (floorFilteredPrices.length ? floorFilteredPrices : orderedPrices);
|
: (floorFilteredPrices.length ? floorFilteredPrices : candidatePrices);
|
||||||
if (!pricesToTry.length) {
|
if (!pricesToTry.length) {
|
||||||
|
if (priceRange.hasMinPriceLimit && !rangeFilteredPrices.length) {
|
||||||
|
noNumbersByCountry.push(
|
||||||
|
`${countryLabel}: no numbers within price range ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
countryPriceFloor !== null
|
countryPriceFloor !== null
|
||||||
&& Array.isArray(pricePlan.prices)
|
&& Array.isArray(pricePlan.prices)
|
||||||
@@ -3217,6 +3370,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const acquirePriority = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
|
const acquirePriority = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
|
||||||
|
const priceRange = resolvePhonePriceRange(state, PHONE_SMS_PROVIDER_HERO);
|
||||||
|
if (priceRange.invalidRange) {
|
||||||
|
throw new Error(
|
||||||
|
`HeroSMS price range is invalid: minPrice=${priceRange.minPriceLimit} exceeds maxPrice=${priceRange.maxPriceLimit}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const minPriceLimit = priceRange.minPriceLimit;
|
||||||
|
const maxPriceLimit = priceRange.maxPriceLimit;
|
||||||
|
const hasPriceBounds = priceRange.hasMinPriceLimit || priceRange.hasMaxPriceLimit;
|
||||||
const preferredPriceTier = normalizeHeroSmsPriceLimit(state?.heroSmsPreferredPrice);
|
const preferredPriceTier = normalizeHeroSmsPriceLimit(state?.heroSmsPreferredPrice);
|
||||||
const countryPriceFloorByCountryId = normalizeCountryPriceFloorMap(
|
const countryPriceFloorByCountryId = normalizeCountryPriceFloorMap(
|
||||||
options?.countryPriceFloorByCountryId,
|
options?.countryPriceFloorByCountryId,
|
||||||
@@ -3257,8 +3419,16 @@
|
|||||||
for (const attempt of countryAttempts) {
|
for (const attempt of countryAttempts) {
|
||||||
const pricePlan = await resolvePhoneActivationPricePlan(config, attempt.countryConfig, state);
|
const pricePlan = await resolvePhoneActivationPricePlan(config, attempt.countryConfig, state);
|
||||||
const orderedPrices = reorderPriceCandidates(pricePlan?.prices, acquirePriority, preferredPriceTier);
|
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)
|
const numericPrices = Array.isArray(orderedPrices)
|
||||||
? orderedPrices
|
? rankingPrices
|
||||||
.map((value) => Number(value))
|
.map((value) => Number(value))
|
||||||
.filter((value) => Number.isFinite(value) && value > 0)
|
.filter((value) => Number.isFinite(value) && value > 0)
|
||||||
: [];
|
: [];
|
||||||
@@ -3308,7 +3478,15 @@
|
|||||||
let noNumbersObservedInCountry = false;
|
let noNumbersObservedInCountry = false;
|
||||||
|
|
||||||
const orderedPrices = reorderPriceCandidates(pricePlan.prices, acquirePriority, preferredPriceTier);
|
const orderedPrices = reorderPriceCandidates(pricePlan.prices, acquirePriority, preferredPriceTier);
|
||||||
const floorFilteredPrices = filterPriceCandidatesAboveFloor(orderedPrices, countryPriceFloor);
|
const rangeFilteredPrices = filterPriceCandidatesWithinRange(
|
||||||
|
orderedPrices,
|
||||||
|
minPriceLimit,
|
||||||
|
maxPriceLimit
|
||||||
|
);
|
||||||
|
const candidatePrices = rangeFilteredPrices.length
|
||||||
|
? rangeFilteredPrices
|
||||||
|
: (hasPriceBounds ? [] : orderedPrices);
|
||||||
|
const floorFilteredPrices = filterPriceCandidatesAboveFloor(candidatePrices, countryPriceFloor);
|
||||||
const hasCountryPriceFloor = (
|
const hasCountryPriceFloor = (
|
||||||
countryPriceFloor !== null
|
countryPriceFloor !== null
|
||||||
&& Number.isFinite(Number(countryPriceFloor))
|
&& Number.isFinite(Number(countryPriceFloor))
|
||||||
@@ -3325,9 +3503,9 @@
|
|||||||
? (
|
? (
|
||||||
floorFilteredPrices.length
|
floorFilteredPrices.length
|
||||||
? floorFilteredPrices
|
? floorFilteredPrices
|
||||||
: (hasAlternativeCountries ? [] : orderedPrices.slice(0, 1))
|
: (hasAlternativeCountries ? [] : candidatePrices.slice(0, 1))
|
||||||
)
|
)
|
||||||
: (floorFilteredPrices.length ? floorFilteredPrices : orderedPrices);
|
: (floorFilteredPrices.length ? floorFilteredPrices : candidatePrices);
|
||||||
const rawTierText = Array.isArray(pricePlan?.prices) && pricePlan.prices.length
|
const rawTierText = Array.isArray(pricePlan?.prices) && pricePlan.prices.length
|
||||||
? pricePlan.prices
|
? pricePlan.prices
|
||||||
.map((value) => (value === null || value === undefined ? 'auto' : String(value)))
|
.map((value) => (value === null || value === undefined ? 'auto' : String(value)))
|
||||||
@@ -3347,6 +3525,12 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!pricesToTry.length) {
|
if (!pricesToTry.length) {
|
||||||
|
if (priceRange.hasMinPriceLimit && !rangeFilteredPrices.length) {
|
||||||
|
noNumbersByCountry.push(
|
||||||
|
`${countryConfig.label}: no numbers within price range ${formatPhonePriceRangeText(minPriceLimit, maxPriceLimit)}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
countryPriceFloor !== null
|
countryPriceFloor !== null
|
||||||
&& Array.isArray(pricePlan.prices)
|
&& Array.isArray(pricePlan.prices)
|
||||||
@@ -3388,6 +3572,7 @@
|
|||||||
maxPrice,
|
maxPrice,
|
||||||
{
|
{
|
||||||
userLimit: pricePlan.userLimit,
|
userLimit: pricePlan.userLimit,
|
||||||
|
userMinLimit: minPriceLimit,
|
||||||
fixedPrice,
|
fixedPrice,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# 手机接码价格区间与最低购买价开发方案
|
||||||
|
|
||||||
|
## 1. 需求结论
|
||||||
|
|
||||||
|
本次需求不是把“价格上限”改成一个可解析字符串,而是要在现有接码价格逻辑上新增一个独立的最低购买价边界,让用户能配置:
|
||||||
|
|
||||||
|
- 只设上限
|
||||||
|
- 只设下限
|
||||||
|
- 上下限都设
|
||||||
|
- 仍然保留“指定档位”作为独立偏好
|
||||||
|
|
||||||
|
最终结论:
|
||||||
|
|
||||||
|
1. 不建议把上限文本框直接改成“范围字符串”。
|
||||||
|
2. 建议在当前价格控制区内新增一个独立的“最低购买价”输入。
|
||||||
|
3. 价格区间采用**闭区间**语义,两个边界都可空。
|
||||||
|
4. `指定档位` 不并入区间字段,只在区间内才生效。
|
||||||
|
5. 现有的内部换档下限 `countryPriceFloorByCountryId` 继续保留,不和用户可配最低价混用。
|
||||||
|
|
||||||
|
## 2. 需求符合性分析
|
||||||
|
|
||||||
|
### 2.1 符合的部分
|
||||||
|
|
||||||
|
- 低价号过滤是明确业务诉求,和当前“只看上限”确实不匹配。
|
||||||
|
- 用户希望继续保留指定档位,说明这是“筛选 + 偏好”组合,而不是单纯范围匹配。
|
||||||
|
- 当前价格控制区本来就集中在同一块,适合扩展为区间控制,不需要拆散到别的区域。
|
||||||
|
|
||||||
|
### 2.2 不足和潜在冲突
|
||||||
|
|
||||||
|
1. **直接复用上限做范围解析不稳**
|
||||||
|
- 会把“上限”语义和“范围”语义混在一起。
|
||||||
|
- 还会让 `指定档位` 的优先级变得不清楚。
|
||||||
|
|
||||||
|
2. **最低价不能复用 `heroSmsPreferredPrice`**
|
||||||
|
- `heroSmsPreferredPrice` 是“指定档位”,不是下限。
|
||||||
|
- 两者语义完全不同,混用会造成维护和排障困难。
|
||||||
|
|
||||||
|
3. **用户最低价和内部换档下限不是同一层**
|
||||||
|
- `countryPriceFloorByCountryId` 是 Step 9 内部避让逻辑。
|
||||||
|
- 用户最低购买价是配置项。
|
||||||
|
- 两者必须分开,否则会互相污染。
|
||||||
|
|
||||||
|
4. **区间反转是边界问题**
|
||||||
|
- 如果最低价大于上限,逻辑上是无效区间。
|
||||||
|
- 这个状态必须显式处理,不能默默当成正常配置。
|
||||||
|
|
||||||
|
5. **区间和指定档位的关系要明确**
|
||||||
|
- 指定档位只有在区间内才应参与排序。
|
||||||
|
- 超出区间的指定档位应忽略,不应反向突破边界。
|
||||||
|
|
||||||
|
## 3. 设计方案
|
||||||
|
|
||||||
|
### 3.1 字段模型
|
||||||
|
|
||||||
|
建议保留现有字段结构,只新增最低价字段:
|
||||||
|
|
||||||
|
- `heroSmsMinPrice`
|
||||||
|
- `heroSmsMaxPrice`
|
||||||
|
- `heroSmsPreferredPrice`
|
||||||
|
- `fiveSimMinPrice`
|
||||||
|
- `fiveSimMaxPrice`
|
||||||
|
|
||||||
|
其中:
|
||||||
|
|
||||||
|
- `heroSmsMinPrice` 兼容 HeroSMS / NexSMS
|
||||||
|
- `fiveSimMinPrice` 仅服务 5sim
|
||||||
|
- `heroSmsPreferredPrice` 保持原语义,不改名
|
||||||
|
|
||||||
|
### 3.2 UI 位置
|
||||||
|
|
||||||
|
价格区仍放在当前 `row-hero-sms-max-price` 这一块里,不另起炉灶。
|
||||||
|
|
||||||
|
建议布局为:
|
||||||
|
|
||||||
|
- `最低购买价`
|
||||||
|
- `价格上限`
|
||||||
|
- `指定档位`
|
||||||
|
- `号码复用`
|
||||||
|
|
||||||
|
这样用户一眼就能看出这四项是一组策略,不会散到别的位置。
|
||||||
|
|
||||||
|
### 3.3 逻辑规则
|
||||||
|
|
||||||
|
1. 先按价格区间过滤候选。
|
||||||
|
2. 再应用指定档位排序。
|
||||||
|
3. 再按平台自身规则进行换档、重试、兜底。
|
||||||
|
4. 当区间内没有可用价格时,显示“无可用号源”类提示,而不是继续尝试区间外价格。
|
||||||
|
|
||||||
|
### 3.4 取号语义
|
||||||
|
|
||||||
|
建议统一成如下顺序:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["读取当前 provider 的最低价/上限/指定档位"] --> B["收集平台候选价格"]
|
||||||
|
B --> C["过滤掉区间外价格"]
|
||||||
|
C --> D{"指定档位是否仍在区间内"}
|
||||||
|
D -->|是| E["优先尝试指定档位"]
|
||||||
|
D -->|否| F["忽略指定档位"]
|
||||||
|
E --> G["按平台规则排序并取号"]
|
||||||
|
F --> G
|
||||||
|
G --> H{"是否仍无可用号源"}
|
||||||
|
H -->|是| I["输出范围相关诊断/提示"]
|
||||||
|
H -->|否| J["继续验证码和后续流程"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 开发清单
|
||||||
|
|
||||||
|
### 阶段 1:字段与持久化
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 新增最低价字段默认值
|
||||||
|
- 新增归一化逻辑
|
||||||
|
- 新增保存/回显/导入兼容
|
||||||
|
- 不破坏旧配置
|
||||||
|
|
||||||
|
自检项:
|
||||||
|
|
||||||
|
- 保存后能否正确回显
|
||||||
|
- 旧配置导入后是否保持可用
|
||||||
|
- 新旧字段是否存在命名冲突
|
||||||
|
- 是否有乱码或字段缺失
|
||||||
|
|
||||||
|
### 阶段 2:UI 改造
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 在现有价格区内新增最低购买价输入
|
||||||
|
- 调整文案为“价格区间”
|
||||||
|
- 保持控件布局整齐,不把新输入塞到别的区域
|
||||||
|
|
||||||
|
自检项:
|
||||||
|
|
||||||
|
- 侧边栏宽度下是否挤压、换行错乱
|
||||||
|
- 输入框宽度是否足够
|
||||||
|
- 切换 provider 时回显是否正确
|
||||||
|
- 文案是否统一、无错别字、无乱码
|
||||||
|
|
||||||
|
### 阶段 3:取号与预览逻辑
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- HeroSMS / NexSMS / 5sim 都按区间过滤候选
|
||||||
|
- 指定档位仅在区间内生效
|
||||||
|
- 预览文案同步展示区间后的结果
|
||||||
|
- 无号源提示能反映“区间过窄/区间反转/无可用档位”
|
||||||
|
|
||||||
|
自检项:
|
||||||
|
|
||||||
|
- 只设上限是否仍与现有行为一致
|
||||||
|
- 只设下限是否能正常过滤低价
|
||||||
|
- 上下限同时设置是否只保留区间内候选
|
||||||
|
- 指定档位超出区间时是否被正确忽略
|
||||||
|
- 与 `countryPriceFloorByCountryId` 是否仍然隔离
|
||||||
|
|
||||||
|
### 阶段 4:测试与全量复审
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 补齐单测
|
||||||
|
- 回归 sidepanel、background、preview、保存/切换
|
||||||
|
- 全量检查无遗漏后再提交
|
||||||
|
|
||||||
|
自检项:
|
||||||
|
|
||||||
|
- 运行测试是否通过
|
||||||
|
- 新增字段是否所有路径都覆盖
|
||||||
|
- 是否还有未同步的文案/默认值/回显逻辑
|
||||||
|
- 是否存在边界问题或乱码
|
||||||
|
|
||||||
|
## 5. 完成标准
|
||||||
|
|
||||||
|
本需求只有在以下条件同时满足时才算完成:
|
||||||
|
|
||||||
|
1. 新的最低购买价能正确保存、回显、切换、导入。
|
||||||
|
2. 价格区间在 HeroSMS / NexSMS / 5sim 的候选过滤中生效。
|
||||||
|
3. 指定档位不会突破区间边界。
|
||||||
|
4. 预览与无号源提示和真实取号逻辑一致。
|
||||||
|
5. 代码、测试、文案、注释都没有乱码和明显冲突。
|
||||||
|
6. 全量复审通过后再写中文提交信息并提交。
|
||||||
|
|
||||||
@@ -1464,7 +1464,7 @@
|
|||||||
placeholder="ot" />
|
placeholder="ot" />
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-hero-sms-max-price" style="display:none;">
|
<div class="data-row" id="row-hero-sms-max-price" style="display:none;">
|
||||||
<span class="data-label">价格</span>
|
<span class="data-label">价格区间</span>
|
||||||
<div class="data-inline hero-sms-price-preview-stack">
|
<div class="data-inline hero-sms-price-preview-stack">
|
||||||
<div class="hero-sms-price-preview-head">
|
<div class="hero-sms-price-preview-head">
|
||||||
<button id="btn-hero-sms-price-preview" class="btn btn-outline btn-xs data-inline-btn" type="button">查询价格</button>
|
<button id="btn-hero-sms-price-preview" class="btn btn-outline btn-xs data-inline-btn" type="button">查询价格</button>
|
||||||
@@ -1475,10 +1475,16 @@
|
|||||||
<span id="display-phone-sms-balance" class="data-value mono hero-sms-price-preview-text">余额未获取</span>
|
<span id="display-phone-sms-balance" class="data-value mono hero-sms-price-preview-text">余额未获取</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-sms-price-controls-grid">
|
<div class="hero-sms-price-controls-grid">
|
||||||
|
<div class="hero-sms-price-control">
|
||||||
|
<span class="hero-sms-settings-caption">最低购买价</span>
|
||||||
|
<div class="setting-controls">
|
||||||
|
<input type="number" id="input-hero-sms-min-price" class="data-input auto-delay-input mono hero-sms-max-price-input" placeholder="0.05" min="0" step="0.0001" title="接码最低购买价;可空(空=不设下限)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="hero-sms-price-control">
|
<div class="hero-sms-price-control">
|
||||||
<span class="hero-sms-settings-caption">价格上限</span>
|
<span class="hero-sms-settings-caption">价格上限</span>
|
||||||
<div class="setting-controls">
|
<div class="setting-controls">
|
||||||
<input type="number" id="input-hero-sms-max-price" class="data-input auto-delay-input mono hero-sms-max-price-input" placeholder="0.12" min="0" step="0.0001" title="接码价格上限;可空(空=自动价格)" />
|
<input type="number" id="input-hero-sms-max-price" class="data-input auto-delay-input mono hero-sms-max-price-input" placeholder="0.12" min="0" step="0.0001" title="接码价格上限;可空(空=不设上限)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-sms-price-control">
|
<div class="hero-sms-price-control">
|
||||||
|
|||||||
+209
-51
@@ -433,6 +433,7 @@ const inputFiveSimProduct = document.getElementById('input-five-sim-product');
|
|||||||
const inputNexSmsApiKey = document.getElementById('input-nex-sms-api-key');
|
const inputNexSmsApiKey = document.getElementById('input-nex-sms-api-key');
|
||||||
const btnToggleNexSmsApiKey = document.getElementById('btn-toggle-nex-sms-api-key');
|
const btnToggleNexSmsApiKey = document.getElementById('btn-toggle-nex-sms-api-key');
|
||||||
const inputNexSmsServiceCode = document.getElementById('input-nex-sms-service-code');
|
const inputNexSmsServiceCode = document.getElementById('input-nex-sms-service-code');
|
||||||
|
const inputHeroSmsMinPrice = document.getElementById('input-hero-sms-min-price');
|
||||||
const inputHeroSmsMaxPrice = document.getElementById('input-hero-sms-max-price');
|
const inputHeroSmsMaxPrice = document.getElementById('input-hero-sms-max-price');
|
||||||
const inputHeroSmsPreferredPrice = document.getElementById('input-hero-sms-preferred-price');
|
const inputHeroSmsPreferredPrice = document.getElementById('input-hero-sms-preferred-price');
|
||||||
const inputPhoneReplacementLimit = document.getElementById('input-phone-replacement-limit');
|
const inputPhoneReplacementLimit = document.getElementById('input-phone-replacement-limit');
|
||||||
@@ -3359,12 +3360,21 @@ function collectSettingsPayload() {
|
|||||||
const currentPhoneSmsMaxPriceValue = typeof inputHeroSmsMaxPrice !== 'undefined' && inputHeroSmsMaxPrice
|
const currentPhoneSmsMaxPriceValue = typeof inputHeroSmsMaxPrice !== 'undefined' && inputHeroSmsMaxPrice
|
||||||
? normalizePhoneSmsMaxPriceValue(inputHeroSmsMaxPrice.value, phoneSmsProviderValue)
|
? normalizePhoneSmsMaxPriceValue(inputHeroSmsMaxPrice.value, phoneSmsProviderValue)
|
||||||
: '';
|
: '';
|
||||||
|
const currentPhoneSmsMinPriceValue = typeof inputHeroSmsMinPrice !== 'undefined' && inputHeroSmsMinPrice
|
||||||
|
? normalizePhoneSmsMinPriceValue(inputHeroSmsMinPrice.value, phoneSmsProviderValue)
|
||||||
|
: '';
|
||||||
const heroSmsMaxPriceValue = phoneSmsProviderValue === PHONE_SMS_PROVIDER_HERO_SMS
|
const heroSmsMaxPriceValue = phoneSmsProviderValue === PHONE_SMS_PROVIDER_HERO_SMS
|
||||||
? currentPhoneSmsMaxPriceValue
|
? currentPhoneSmsMaxPriceValue
|
||||||
: normalizeHeroSmsMaxPriceValue(latestState?.heroSmsMaxPrice || '');
|
: normalizeHeroSmsMaxPriceValue(latestState?.heroSmsMaxPrice || '');
|
||||||
const fiveSimMaxPriceValue = phoneSmsProviderValue === PHONE_SMS_PROVIDER_FIVE_SIM
|
const fiveSimMaxPriceValue = phoneSmsProviderValue === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||||
? currentPhoneSmsMaxPriceValue
|
? currentPhoneSmsMaxPriceValue
|
||||||
: normalizeFiveSimMaxPriceValue(latestState?.fiveSimMaxPrice || '');
|
: normalizeFiveSimMaxPriceValue(latestState?.fiveSimMaxPrice || '');
|
||||||
|
const heroSmsMinPriceValue = phoneSmsProviderValue === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||||
|
? normalizePhoneSmsMinPriceValue(latestState?.heroSmsMinPrice || '', PHONE_SMS_PROVIDER_HERO_SMS)
|
||||||
|
: currentPhoneSmsMinPriceValue;
|
||||||
|
const fiveSimMinPriceValue = phoneSmsProviderValue === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||||
|
? currentPhoneSmsMinPriceValue
|
||||||
|
: normalizePhoneSmsMinPriceValue(latestState?.fiveSimMinPrice || '', PHONE_SMS_PROVIDER_FIVE_SIM);
|
||||||
const defaultFiveSimProduct = typeof DEFAULT_FIVE_SIM_PRODUCT !== 'undefined'
|
const defaultFiveSimProduct = typeof DEFAULT_FIVE_SIM_PRODUCT !== 'undefined'
|
||||||
? DEFAULT_FIVE_SIM_PRODUCT
|
? DEFAULT_FIVE_SIM_PRODUCT
|
||||||
: 'openai';
|
: 'openai';
|
||||||
@@ -3791,6 +3801,7 @@ function collectSettingsPayload() {
|
|||||||
freePhoneReuseEnabled: freePhoneReuseEnabledValue,
|
freePhoneReuseEnabled: freePhoneReuseEnabledValue,
|
||||||
freePhoneReuseAutoEnabled: freePhoneReuseAutoEnabledValue,
|
freePhoneReuseAutoEnabled: freePhoneReuseAutoEnabledValue,
|
||||||
heroSmsAcquirePriority: heroSmsAcquirePriorityValue,
|
heroSmsAcquirePriority: heroSmsAcquirePriorityValue,
|
||||||
|
heroSmsMinPrice: heroSmsMinPriceValue,
|
||||||
heroSmsMaxPrice: heroSmsMaxPriceValue,
|
heroSmsMaxPrice: heroSmsMaxPriceValue,
|
||||||
heroSmsPreferredPrice: heroSmsPreferredPriceValue,
|
heroSmsPreferredPrice: heroSmsPreferredPriceValue,
|
||||||
phonePreferredActivation: phonePreferredActivationValue,
|
phonePreferredActivation: phonePreferredActivationValue,
|
||||||
@@ -3806,6 +3817,7 @@ function collectSettingsPayload() {
|
|||||||
fiveSimCountryLabel: fiveSimCountry.label,
|
fiveSimCountryLabel: fiveSimCountry.label,
|
||||||
fiveSimCountryFallback,
|
fiveSimCountryFallback,
|
||||||
fiveSimMaxPrice: fiveSimMaxPriceValue,
|
fiveSimMaxPrice: fiveSimMaxPriceValue,
|
||||||
|
fiveSimMinPrice: fiveSimMinPriceValue,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3927,6 +3939,13 @@ function normalizePhoneSmsMaxPriceValue(value = '', provider = getSelectedPhoneS
|
|||||||
return normalizeHeroSmsMaxPriceValue(value);
|
return normalizeHeroSmsMaxPriceValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePhoneSmsMinPriceValue(value = '', provider = getSelectedPhoneSmsProvider()) {
|
||||||
|
if (normalizePhoneSmsProvider(provider) === PHONE_SMS_PROVIDER_FIVE_SIM) {
|
||||||
|
return normalizeFiveSimMaxPriceValue(value);
|
||||||
|
}
|
||||||
|
return normalizeHeroSmsMaxPriceValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeFiveSimCountryId(value, fallback = DEFAULT_FIVE_SIM_COUNTRY_ID) {
|
function normalizeFiveSimCountryId(value, fallback = DEFAULT_FIVE_SIM_COUNTRY_ID) {
|
||||||
const fallbackSource = fallback === undefined || fallback === null ? DEFAULT_FIVE_SIM_COUNTRY_ID : fallback;
|
const fallbackSource = fallback === undefined || fallback === null ? DEFAULT_FIVE_SIM_COUNTRY_ID : fallback;
|
||||||
const normalizedFallback = String(fallbackSource).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
const normalizedFallback = String(fallbackSource).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||||
@@ -5070,6 +5089,97 @@ function summarizeHeroSmsPreviewError(payload, responseStatus = 0) {
|
|||||||
return text || '未知错误';
|
return text || '未知错误';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePhoneSmsPricePreviewRange(provider = '') {
|
||||||
|
const activeProvider = provider || (
|
||||||
|
typeof getSelectedPhoneSmsProvider === 'function'
|
||||||
|
? getSelectedPhoneSmsProvider()
|
||||||
|
: (typeof DEFAULT_PHONE_SMS_PROVIDER !== 'undefined' ? DEFAULT_PHONE_SMS_PROVIDER : 'hero-sms')
|
||||||
|
);
|
||||||
|
const rawMinPrice = typeof inputHeroSmsMinPrice !== 'undefined' && inputHeroSmsMinPrice
|
||||||
|
? inputHeroSmsMinPrice.value
|
||||||
|
: '';
|
||||||
|
const rawMaxPrice = typeof inputHeroSmsMaxPrice !== 'undefined' && inputHeroSmsMaxPrice
|
||||||
|
? inputHeroSmsMaxPrice.value
|
||||||
|
: '';
|
||||||
|
const minPriceText = typeof normalizePhoneSmsMinPriceValue === 'function'
|
||||||
|
? normalizePhoneSmsMinPriceValue(rawMinPrice, activeProvider)
|
||||||
|
: normalizeHeroSmsMaxPriceValue(rawMinPrice);
|
||||||
|
const maxPriceText = typeof normalizePhoneSmsMaxPriceValue === 'function'
|
||||||
|
? normalizePhoneSmsMaxPriceValue(rawMaxPrice, activeProvider)
|
||||||
|
: normalizeHeroSmsMaxPriceValue(rawMaxPrice);
|
||||||
|
const minPrice = minPriceText ? Number(minPriceText) : null;
|
||||||
|
const maxPrice = maxPriceText ? Number(maxPriceText) : null;
|
||||||
|
return {
|
||||||
|
minPrice,
|
||||||
|
maxPrice,
|
||||||
|
hasMinPrice: Number.isFinite(minPrice) && minPrice > 0,
|
||||||
|
hasMaxPrice: Number.isFinite(maxPrice) && maxPrice > 0,
|
||||||
|
invalid: Number.isFinite(minPrice) && minPrice > 0
|
||||||
|
&& Number.isFinite(maxPrice) && maxPrice > 0
|
||||||
|
&& minPrice > maxPrice,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPhoneSmsPriceWithinPreviewRange(price, range = {}) {
|
||||||
|
const numeric = Number(price);
|
||||||
|
if (!Number.isFinite(numeric) || numeric <= 0 || range?.invalid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const normalized = Math.round(numeric * 10000) / 10000;
|
||||||
|
if (range?.hasMinPrice && normalized < Number(range.minPrice)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (range?.hasMaxPrice && normalized > Number(range.maxPrice)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterPhoneSmsPriceEntriesForPreviewRange(entries = [], range = {}) {
|
||||||
|
if (!range?.hasMinPrice && !range?.hasMaxPrice && !range?.invalid) {
|
||||||
|
return Array.isArray(entries) ? [...entries] : [];
|
||||||
|
}
|
||||||
|
return (Array.isArray(entries) ? entries : []).filter((entry) => (
|
||||||
|
isPhoneSmsPriceWithinPreviewRange(entry?.price ?? entry?.cost, range)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterPhoneSmsPriceValuesForPreviewRange(values = [], range = {}) {
|
||||||
|
if (!range?.hasMinPrice && !range?.hasMaxPrice && !range?.invalid) {
|
||||||
|
return Array.isArray(values) ? [...values] : [];
|
||||||
|
}
|
||||||
|
return (Array.isArray(values) ? values : []).filter((price) => (
|
||||||
|
isPhoneSmsPriceWithinPreviewRange(price, range)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPhoneSmsPriceRangePreviewText(range = {}) {
|
||||||
|
const minText = range?.hasMinPrice
|
||||||
|
? (formatHeroSmsPriceForPreview(range.minPrice) || String(range.minPrice))
|
||||||
|
: '';
|
||||||
|
const maxText = range?.hasMaxPrice
|
||||||
|
? (formatHeroSmsPriceForPreview(range.maxPrice) || String(range.maxPrice))
|
||||||
|
: '';
|
||||||
|
if (minText && maxText) {
|
||||||
|
return `${minText}~${maxText}`;
|
||||||
|
}
|
||||||
|
if (minText) {
|
||||||
|
return `${minText}~`;
|
||||||
|
}
|
||||||
|
if (maxText) {
|
||||||
|
return `~${maxText}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPhoneSmsPriceRangePreviewMessage(range = {}) {
|
||||||
|
const rangeText = formatPhoneSmsPriceRangePreviewText(range);
|
||||||
|
if (range?.invalid) {
|
||||||
|
return `价格区间无效:最低购买价 ${formatHeroSmsPriceForPreview(range.minPrice) || range.minPrice} 高于价格上限 ${formatHeroSmsPriceForPreview(range.maxPrice) || range.maxPrice}`;
|
||||||
|
}
|
||||||
|
return rangeText ? `区间内无可用号源(当前 ${rangeText})` : '暂无可用号源';
|
||||||
|
}
|
||||||
|
|
||||||
function formatPriceTiersForPreview(entries = [], options = {}) {
|
function formatPriceTiersForPreview(entries = [], options = {}) {
|
||||||
const maxPrice = Number(options?.maxPrice);
|
const maxPrice = Number(options?.maxPrice);
|
||||||
const hasMaxPrice = Number.isFinite(maxPrice) && maxPrice > 0;
|
const hasMaxPrice = Number.isFinite(maxPrice) && maxPrice > 0;
|
||||||
@@ -6670,14 +6780,19 @@ async function buildNexSmsPricePreviewLines(options = {}) {
|
|||||||
const serviceCode = normalizeNexSmsServiceCodeValue(
|
const serviceCode = normalizeNexSmsServiceCodeValue(
|
||||||
inputNexSmsServiceCode?.value || latestState?.nexSmsServiceCode || DEFAULT_NEX_SMS_SERVICE_CODE
|
inputNexSmsServiceCode?.value || latestState?.nexSmsServiceCode || DEFAULT_NEX_SMS_SERVICE_CODE
|
||||||
);
|
);
|
||||||
const maxPriceText = normalizeHeroSmsMaxPriceValue(inputHeroSmsMaxPrice?.value || '');
|
const priceRange = resolvePhoneSmsPricePreviewRange(
|
||||||
const maxPrice = maxPriceText ? Number(maxPriceText) : null;
|
typeof PHONE_SMS_PROVIDER_NEXSMS !== 'undefined' ? PHONE_SMS_PROVIDER_NEXSMS : 'nexsms'
|
||||||
|
);
|
||||||
|
const maxPrice = priceRange.maxPrice;
|
||||||
const apiKey = String(inputNexSmsApiKey?.value || '').trim();
|
const apiKey = String(inputNexSmsApiKey?.value || '').trim();
|
||||||
const providerLabel = String(options?.providerLabel || 'NexSMS').trim();
|
const providerLabel = String(options?.providerLabel || 'NexSMS').trim();
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return [`${providerLabel}: 请先填写 NexSMS API Key`];
|
return [`${providerLabel}: 请先填写 NexSMS API Key`];
|
||||||
}
|
}
|
||||||
|
if (priceRange.invalid) {
|
||||||
|
return [`${providerLabel}: ${buildPhoneSmsPriceRangePreviewMessage(priceRange)}`];
|
||||||
|
}
|
||||||
if (!countryIds.length) {
|
if (!countryIds.length) {
|
||||||
return [`${providerLabel}: 请先选择至少 1 个国家`];
|
return [`${providerLabel}: 请先选择至少 1 个国家`];
|
||||||
}
|
}
|
||||||
@@ -6717,19 +6832,22 @@ async function buildNexSmsPricePreviewLines(options = {}) {
|
|||||||
.map((entry) => entry.price);
|
.map((entry) => entry.price);
|
||||||
const uniqueSorted = Array.from(new Set(availablePrices)).sort((left, right) => left - right);
|
const uniqueSorted = Array.from(new Set(availablePrices)).sort((left, right) => left - right);
|
||||||
const fallbackMin = Number(data?.minPrice);
|
const fallbackMin = Number(data?.minPrice);
|
||||||
const lowest = uniqueSorted.length
|
const filteredTierEntries = filterPhoneSmsPriceEntriesForPreviewRange(tierEntries, priceRange);
|
||||||
? uniqueSorted[0]
|
const rangePrices = filterPhoneSmsPriceValuesForPreviewRange(uniqueSorted, priceRange);
|
||||||
: (Number.isFinite(fallbackMin) && fallbackMin > 0 ? Math.round(fallbackMin * 10000) / 10000 : null);
|
const fallbackMinPrice = Number.isFinite(fallbackMin) && fallbackMin > 0
|
||||||
|
? Math.round(fallbackMin * 10000) / 10000
|
||||||
|
: null;
|
||||||
|
const rangeFallbackMin = isPhoneSmsPriceWithinPreviewRange(fallbackMinPrice, priceRange)
|
||||||
|
? fallbackMinPrice
|
||||||
|
: null;
|
||||||
|
const lowest = rangePrices.length ? rangePrices[0] : rangeFallbackMin;
|
||||||
if (!Number.isFinite(lowest)) {
|
if (!Number.isFinite(lowest)) {
|
||||||
previews.push(`${countryLabel}: 暂无可用号源`);
|
previews.push(`${countryLabel}: ${buildPhoneSmsPriceRangePreviewMessage(priceRange)}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const tierText = formatPriceTiersForPreview(tierEntries, { maxPrice });
|
const tierText = formatPriceTiersForPreview(filteredTierEntries, { maxPrice });
|
||||||
if (Number.isFinite(maxPrice) && maxPrice > 0 && lowest > maxPrice) {
|
const lowestLabel = priceRange.hasMinPrice || priceRange.hasMaxPrice ? '区间内最低' : '最低';
|
||||||
previews.push(`${countryLabel}: 最低 ${lowest}(高于上限 ${maxPrice})${tierText ? `;档位:${tierText}` : ''}`);
|
previews.push(`${countryLabel}: ${lowestLabel} ${lowest}${tierText ? `;档位:${tierText}` : ''}`);
|
||||||
} else {
|
|
||||||
previews.push(`${countryLabel}: 最低 ${lowest}${tierText ? `;档位:${tierText}` : ''}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
previews.push(`${countryLabel}: 查询失败(${normalizeHeroSmsFetchErrorMessage(error)})`);
|
previews.push(`${countryLabel}: 查询失败(${normalizeHeroSmsFetchErrorMessage(error)})`);
|
||||||
}
|
}
|
||||||
@@ -6757,13 +6875,19 @@ async function previewFiveSimPriceTiers() {
|
|||||||
const product = normalizeFiveSimProductValue(
|
const product = normalizeFiveSimProductValue(
|
||||||
inputFiveSimProduct?.value || latestState?.fiveSimProduct || DEFAULT_FIVE_SIM_PRODUCT
|
inputFiveSimProduct?.value || latestState?.fiveSimProduct || DEFAULT_FIVE_SIM_PRODUCT
|
||||||
);
|
);
|
||||||
const maxPriceText = normalizeHeroSmsMaxPriceValue(inputHeroSmsMaxPrice?.value || '');
|
const priceRange = resolvePhoneSmsPricePreviewRange(
|
||||||
const maxPrice = maxPriceText ? Number(maxPriceText) : null;
|
typeof PHONE_SMS_PROVIDER_FIVE_SIM !== 'undefined' ? PHONE_SMS_PROVIDER_FIVE_SIM : '5sim'
|
||||||
|
);
|
||||||
|
const maxPrice = priceRange.maxPrice;
|
||||||
|
|
||||||
displayHeroSmsPriceTiers.textContent = '查询中...';
|
displayHeroSmsPriceTiers.textContent = '查询中...';
|
||||||
if (rowHeroSmsPriceTiers) {
|
if (rowHeroSmsPriceTiers) {
|
||||||
rowHeroSmsPriceTiers.style.display = '';
|
rowHeroSmsPriceTiers.style.display = '';
|
||||||
}
|
}
|
||||||
|
if (priceRange.invalid) {
|
||||||
|
displayHeroSmsPriceTiers.textContent = buildPhoneSmsPriceRangePreviewMessage(priceRange);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!countryCodes.length) {
|
if (!countryCodes.length) {
|
||||||
displayHeroSmsPriceTiers.textContent = '请先选择至少 1 个国家,再查询价格';
|
displayHeroSmsPriceTiers.textContent = '请先选择至少 1 个国家,再查询价格';
|
||||||
return;
|
return;
|
||||||
@@ -6833,17 +6957,16 @@ async function previewFiveSimPriceTiers() {
|
|||||||
.filter((entry) => entry.count === null || entry.count > 0)
|
.filter((entry) => entry.count === null || entry.count > 0)
|
||||||
.map((entry) => entry.price);
|
.map((entry) => entry.price);
|
||||||
const uniqueSorted = Array.from(new Set(prices)).sort((left, right) => left - right);
|
const uniqueSorted = Array.from(new Set(prices)).sort((left, right) => left - right);
|
||||||
if (!uniqueSorted.length) {
|
const rangePrices = filterPhoneSmsPriceValuesForPreviewRange(uniqueSorted, priceRange);
|
||||||
previews.push(`${countryLabel}: 暂无可用号源`);
|
const filteredTierEntries = filterPhoneSmsPriceEntriesForPreviewRange(tierEntries, priceRange);
|
||||||
|
if (!rangePrices.length) {
|
||||||
|
previews.push(`${countryLabel}: ${buildPhoneSmsPriceRangePreviewMessage(priceRange)}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const lowest = uniqueSorted[0];
|
const lowest = rangePrices[0];
|
||||||
const tierText = formatPriceTiersForPreview(tierEntries, { maxPrice });
|
const tierText = formatPriceTiersForPreview(filteredTierEntries, { maxPrice });
|
||||||
if (Number.isFinite(maxPrice) && maxPrice > 0 && lowest > maxPrice) {
|
const lowestLabel = priceRange.hasMinPrice || priceRange.hasMaxPrice ? '区间内最低' : '最低';
|
||||||
previews.push(`${countryLabel}: 最低 ${lowest}(高于上限 ${maxPrice})${tierText ? `;档位:${tierText}` : ''}`);
|
previews.push(`${countryLabel}: ${lowestLabel} ${lowest}${tierText ? `;档位:${tierText}` : ''}`);
|
||||||
} else {
|
|
||||||
previews.push(`${countryLabel}: 最低 ${lowest}${tierText ? `;档位:${tierText}` : ''}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
previews.push(`${countryLabel}: 查询失败(${normalizeHeroSmsFetchErrorMessage(error)})`);
|
previews.push(`${countryLabel}: 查询失败(${normalizeHeroSmsFetchErrorMessage(error)})`);
|
||||||
}
|
}
|
||||||
@@ -6864,13 +6987,18 @@ async function buildFiveSimPricePreviewLines(options = {}) {
|
|||||||
const product = normalizeFiveSimProductValue(
|
const product = normalizeFiveSimProductValue(
|
||||||
inputFiveSimProduct?.value || latestState?.fiveSimProduct || DEFAULT_FIVE_SIM_PRODUCT
|
inputFiveSimProduct?.value || latestState?.fiveSimProduct || DEFAULT_FIVE_SIM_PRODUCT
|
||||||
);
|
);
|
||||||
const maxPriceText = normalizeHeroSmsMaxPriceValue(inputHeroSmsMaxPrice?.value || '');
|
const priceRange = resolvePhoneSmsPricePreviewRange(
|
||||||
const maxPrice = maxPriceText ? Number(maxPriceText) : null;
|
typeof PHONE_SMS_PROVIDER_FIVE_SIM !== 'undefined' ? PHONE_SMS_PROVIDER_FIVE_SIM : '5sim'
|
||||||
|
);
|
||||||
|
const maxPrice = priceRange.maxPrice;
|
||||||
const providerLabel = String(options?.providerLabel || '5sim').trim();
|
const providerLabel = String(options?.providerLabel || '5sim').trim();
|
||||||
|
|
||||||
if (!countryCodes.length) {
|
if (!countryCodes.length) {
|
||||||
return [`${providerLabel}: 请先选择至少 1 个国家`];
|
return [`${providerLabel}: 请先选择至少 1 个国家`];
|
||||||
}
|
}
|
||||||
|
if (priceRange.invalid) {
|
||||||
|
return [`${providerLabel}: ${buildPhoneSmsPriceRangePreviewMessage(priceRange)}`];
|
||||||
|
}
|
||||||
|
|
||||||
const collectPriceEntries = (payload, entries = []) => {
|
const collectPriceEntries = (payload, entries = []) => {
|
||||||
if (Array.isArray(payload)) {
|
if (Array.isArray(payload)) {
|
||||||
@@ -6936,17 +7064,16 @@ async function buildFiveSimPricePreviewLines(options = {}) {
|
|||||||
.filter((entry) => entry.count === null || entry.count > 0)
|
.filter((entry) => entry.count === null || entry.count > 0)
|
||||||
.map((entry) => entry.price);
|
.map((entry) => entry.price);
|
||||||
const uniqueSorted = Array.from(new Set(prices)).sort((left, right) => left - right);
|
const uniqueSorted = Array.from(new Set(prices)).sort((left, right) => left - right);
|
||||||
if (!uniqueSorted.length) {
|
const rangePrices = filterPhoneSmsPriceValuesForPreviewRange(uniqueSorted, priceRange);
|
||||||
previews.push(`${countryLabel}: 暂无可用号源`);
|
const filteredTierEntries = filterPhoneSmsPriceEntriesForPreviewRange(tierEntries, priceRange);
|
||||||
|
if (!rangePrices.length) {
|
||||||
|
previews.push(`${countryLabel}: ${buildPhoneSmsPriceRangePreviewMessage(priceRange)}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const lowest = uniqueSorted[0];
|
const lowest = rangePrices[0];
|
||||||
const tierText = formatPriceTiersForPreview(tierEntries, { maxPrice });
|
const tierText = formatPriceTiersForPreview(filteredTierEntries, { maxPrice });
|
||||||
if (Number.isFinite(maxPrice) && maxPrice > 0 && lowest > maxPrice) {
|
const lowestLabel = priceRange.hasMinPrice || priceRange.hasMaxPrice ? '区间内最低' : '最低';
|
||||||
previews.push(`${countryLabel}: 最低 ${lowest}(高于上限 ${maxPrice})${tierText ? `;档位:${tierText}` : ''}`);
|
previews.push(`${countryLabel}: ${lowestLabel} ${lowest}${tierText ? `;档位:${tierText}` : ''}`);
|
||||||
} else {
|
|
||||||
previews.push(`${countryLabel}: 最低 ${lowest}${tierText ? `;档位:${tierText}` : ''}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
previews.push(`${countryLabel}: 查询失败(${normalizeHeroSmsFetchErrorMessage(error)})`);
|
previews.push(`${countryLabel}: 查询失败(${normalizeHeroSmsFetchErrorMessage(error)})`);
|
||||||
}
|
}
|
||||||
@@ -7018,8 +7145,8 @@ async function previewHeroSmsPriceTiers() {
|
|||||||
label: normalizeHeroSmsCountryLabel(country?.label, ''),
|
label: normalizeHeroSmsCountryLabel(country?.label, ''),
|
||||||
}))
|
}))
|
||||||
.filter((country) => country.id > 0);
|
.filter((country) => country.id > 0);
|
||||||
const maxPriceText = normalizeHeroSmsMaxPriceValue(inputHeroSmsMaxPrice?.value || '');
|
const priceRange = resolvePhoneSmsPricePreviewRange(heroProviderValue);
|
||||||
const maxPrice = maxPriceText ? Number(maxPriceText) : null;
|
const maxPrice = priceRange.maxPrice;
|
||||||
const apiKey = String(inputHeroSmsApiKey?.value || '').trim();
|
const apiKey = String(inputHeroSmsApiKey?.value || '').trim();
|
||||||
|
|
||||||
const heroLines = ['HeroSMS:'];
|
const heroLines = ['HeroSMS:'];
|
||||||
@@ -7028,6 +7155,11 @@ async function previewHeroSmsPriceTiers() {
|
|||||||
previews.push(...heroLines, '');
|
previews.push(...heroLines, '');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (priceRange.invalid) {
|
||||||
|
heroLines.push(buildPhoneSmsPriceRangePreviewMessage(priceRange));
|
||||||
|
previews.push(...heroLines, '');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!candidates.length) {
|
if (!candidates.length) {
|
||||||
heroLines.push('请先选择至少 1 个国家');
|
heroLines.push('请先选择至少 1 个国家');
|
||||||
previews.push(...heroLines, '');
|
previews.push(...heroLines, '');
|
||||||
@@ -7152,13 +7284,18 @@ async function previewHeroSmsPriceTiers() {
|
|||||||
price,
|
price,
|
||||||
count: Math.max(0, Number(tierStockByPrice.get(price)) || 0),
|
count: Math.max(0, Number(tierStockByPrice.get(price)) || 0),
|
||||||
}));
|
}));
|
||||||
const withinLimitInStockPrices = Number.isFinite(maxPrice) && maxPrice > 0
|
const rangeAllPrices = filterPhoneSmsPriceValuesForPreviewRange(allPrices, priceRange);
|
||||||
? inStockPrices.filter((price) => price <= maxPrice)
|
const rangeInStockPrices = filterPhoneSmsPriceValuesForPreviewRange(inStockPrices, priceRange);
|
||||||
: inStockPrices;
|
const filteredTierEntries = filterPhoneSmsPriceEntriesForPreviewRange(tierEntries, priceRange);
|
||||||
const tierPreviewText = formatPriceTiersWithZeroStockForPreview(tierEntries, { maxPrice });
|
const tierPreviewText = formatPriceTiersWithZeroStockForPreview(filteredTierEntries, { maxPrice });
|
||||||
if (!inStockPrices.length) {
|
if (!rangeInStockPrices.length) {
|
||||||
if (allPrices.length) {
|
if ((priceRange.hasMinPrice || priceRange.hasMaxPrice) && allPrices.length && !rangeAllPrices.length) {
|
||||||
const lowestKnown = formatHeroSmsPriceForPreview(allPrices[0]) || String(allPrices[0]);
|
heroLines.push(`${countryLabel}: ${buildPhoneSmsPriceRangePreviewMessage(priceRange)}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rangeAllPrices.length || allPrices.length) {
|
||||||
|
const lowestKnownPrice = rangeAllPrices.length ? rangeAllPrices[0] : allPrices[0];
|
||||||
|
const lowestKnown = formatHeroSmsPriceForPreview(lowestKnownPrice) || String(lowestKnownPrice);
|
||||||
heroLines.push(`${countryLabel}: 全档位均无库存(最低标价 ${lowestKnown})${tierPreviewText ? `;档位:${tierPreviewText}` : ''}`);
|
heroLines.push(`${countryLabel}: 全档位均无库存(最低标价 ${lowestKnown})${tierPreviewText ? `;档位:${tierPreviewText}` : ''}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -7169,14 +7306,10 @@ async function previewHeroSmsPriceTiers() {
|
|||||||
heroLines.push(`${countryLabel}: 无可用价格`);
|
heroLines.push(`${countryLabel}: 无可用价格`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (Number.isFinite(maxPrice) && maxPrice > 0 && !withinLimitInStockPrices.length) {
|
const lowestWithinRange = rangeInStockPrices[0];
|
||||||
const lowestInStockText = formatHeroSmsPriceForPreview(inStockPrices[0]) || String(inStockPrices[0]);
|
const lowestText = formatHeroSmsPriceForPreview(lowestWithinRange) || String(lowestWithinRange);
|
||||||
heroLines.push(`${countryLabel}: 上限内无可用号源(上限 ${formatHeroSmsPriceForPreview(maxPrice) || maxPrice},最低可用 ${lowestInStockText})${tierPreviewText ? `;档位:${tierPreviewText}` : ''}`);
|
const lowestLabel = priceRange.hasMinPrice || priceRange.hasMaxPrice ? '区间内最低' : '最低';
|
||||||
} else {
|
heroLines.push(`${countryLabel}: ${lowestLabel} ${lowestText}${tierPreviewText ? `;档位:${tierPreviewText}` : ''}`);
|
||||||
const lowestWithinLimit = withinLimitInStockPrices[0];
|
|
||||||
const lowestText = formatHeroSmsPriceForPreview(lowestWithinLimit) || String(lowestWithinLimit);
|
|
||||||
heroLines.push(`${countryLabel}: 上限内最低 ${lowestText}${tierPreviewText ? `;档位:${tierPreviewText}` : ''}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
heroLines.push(`${countryLabel}: 查询失败(${normalizeHeroSmsFetchErrorMessage(error)})`);
|
heroLines.push(`${countryLabel}: 查询失败(${normalizeHeroSmsFetchErrorMessage(error)})`);
|
||||||
}
|
}
|
||||||
@@ -9016,6 +9149,11 @@ function applySettingsState(state) {
|
|||||||
? normalizeFiveSimMaxPriceValue(state?.fiveSimMaxPrice || '')
|
? normalizeFiveSimMaxPriceValue(state?.fiveSimMaxPrice || '')
|
||||||
: normalizeHeroSmsMaxPriceValue(state?.heroSmsMaxPrice || '');
|
: normalizeHeroSmsMaxPriceValue(state?.heroSmsMaxPrice || '');
|
||||||
}
|
}
|
||||||
|
if (typeof inputHeroSmsMinPrice !== 'undefined' && inputHeroSmsMinPrice) {
|
||||||
|
inputHeroSmsMinPrice.value = restoredPhoneSmsProvider === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||||
|
? normalizePhoneSmsMinPriceValue(state?.fiveSimMinPrice || '', PHONE_SMS_PROVIDER_FIVE_SIM)
|
||||||
|
: normalizePhoneSmsMinPriceValue(state?.heroSmsMinPrice || '', restoredPhoneSmsProvider);
|
||||||
|
}
|
||||||
if (inputFiveSimOperator) {
|
if (inputFiveSimOperator) {
|
||||||
inputFiveSimOperator.value = normalizeFiveSimOperator(state?.fiveSimOperator);
|
inputFiveSimOperator.value = normalizeFiveSimOperator(state?.fiveSimOperator);
|
||||||
}
|
}
|
||||||
@@ -13441,6 +13579,7 @@ async function switchPhoneSmsProvider(nextProvider) {
|
|||||||
|
|
||||||
const currentApiKey = String(inputHeroSmsApiKey?.value || '');
|
const currentApiKey = String(inputHeroSmsApiKey?.value || '');
|
||||||
const currentMaxPrice = normalizePhoneSmsMaxPriceValue(inputHeroSmsMaxPrice?.value || '', previousProvider);
|
const currentMaxPrice = normalizePhoneSmsMaxPriceValue(inputHeroSmsMaxPrice?.value || '', previousProvider);
|
||||||
|
const currentMinPrice = normalizePhoneSmsMinPriceValue(inputHeroSmsMinPrice?.value || '', previousProvider);
|
||||||
const currentSelection = typeof getPhoneSmsCountrySelectionForProvider === 'function'
|
const currentSelection = typeof getPhoneSmsCountrySelectionForProvider === 'function'
|
||||||
? getPhoneSmsCountrySelectionForProvider(previousProvider, { ensureDefault: true })
|
? getPhoneSmsCountrySelectionForProvider(previousProvider, { ensureDefault: true })
|
||||||
: [];
|
: [];
|
||||||
@@ -13453,6 +13592,7 @@ async function switchPhoneSmsProvider(nextProvider) {
|
|||||||
if (previousProvider === PHONE_SMS_PROVIDER_FIVE_SIM) {
|
if (previousProvider === PHONE_SMS_PROVIDER_FIVE_SIM) {
|
||||||
patch.fiveSimApiKey = currentApiKey;
|
patch.fiveSimApiKey = currentApiKey;
|
||||||
patch.fiveSimMaxPrice = currentMaxPrice;
|
patch.fiveSimMaxPrice = currentMaxPrice;
|
||||||
|
patch.fiveSimMinPrice = currentMinPrice;
|
||||||
patch.fiveSimCountryId = currentPrimary.id;
|
patch.fiveSimCountryId = currentPrimary.id;
|
||||||
patch.fiveSimCountryLabel = currentPrimary.label;
|
patch.fiveSimCountryLabel = currentPrimary.label;
|
||||||
patch.fiveSimCountryFallback = currentFallback;
|
patch.fiveSimCountryFallback = currentFallback;
|
||||||
@@ -13463,6 +13603,7 @@ async function switchPhoneSmsProvider(nextProvider) {
|
|||||||
} else {
|
} else {
|
||||||
patch.heroSmsApiKey = currentApiKey;
|
patch.heroSmsApiKey = currentApiKey;
|
||||||
patch.heroSmsMaxPrice = currentMaxPrice;
|
patch.heroSmsMaxPrice = currentMaxPrice;
|
||||||
|
patch.heroSmsMinPrice = currentMinPrice;
|
||||||
patch.heroSmsCountryId = currentPrimary.id;
|
patch.heroSmsCountryId = currentPrimary.id;
|
||||||
patch.heroSmsCountryLabel = currentPrimary.label;
|
patch.heroSmsCountryLabel = currentPrimary.label;
|
||||||
patch.heroSmsCountryFallback = currentFallback;
|
patch.heroSmsCountryFallback = currentFallback;
|
||||||
@@ -13481,6 +13622,11 @@ async function switchPhoneSmsProvider(nextProvider) {
|
|||||||
? normalizeFiveSimMaxPriceValue(latestState?.fiveSimMaxPrice || '')
|
? normalizeFiveSimMaxPriceValue(latestState?.fiveSimMaxPrice || '')
|
||||||
: normalizeHeroSmsMaxPriceValue(latestState?.heroSmsMaxPrice || '');
|
: normalizeHeroSmsMaxPriceValue(latestState?.heroSmsMaxPrice || '');
|
||||||
}
|
}
|
||||||
|
if (typeof inputHeroSmsMinPrice !== 'undefined' && inputHeroSmsMinPrice) {
|
||||||
|
inputHeroSmsMinPrice.value = normalizedNextProvider === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||||
|
? normalizePhoneSmsMinPriceValue(latestState?.fiveSimMinPrice || '', PHONE_SMS_PROVIDER_FIVE_SIM)
|
||||||
|
: normalizePhoneSmsMinPriceValue(latestState?.heroSmsMinPrice || '', normalizedNextProvider);
|
||||||
|
}
|
||||||
if (inputFiveSimOperator) {
|
if (inputFiveSimOperator) {
|
||||||
inputFiveSimOperator.value = normalizeFiveSimOperator(latestState?.fiveSimOperator);
|
inputFiveSimOperator.value = normalizeFiveSimOperator(latestState?.fiveSimOperator);
|
||||||
}
|
}
|
||||||
@@ -13752,6 +13898,13 @@ inputHeroSmsMaxPrice?.addEventListener('blur', () => {
|
|||||||
inputHeroSmsMaxPrice.value = normalizePhoneSmsMaxPriceValue(inputHeroSmsMaxPrice.value, getSelectedPhoneSmsProvider());
|
inputHeroSmsMaxPrice.value = normalizePhoneSmsMaxPriceValue(inputHeroSmsMaxPrice.value, getSelectedPhoneSmsProvider());
|
||||||
saveSettings({ silent: true }).catch(() => { });
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
});
|
});
|
||||||
|
inputHeroSmsMinPrice?.addEventListener('input', () => {
|
||||||
|
markSettingsDirty(true);
|
||||||
|
});
|
||||||
|
inputHeroSmsMinPrice?.addEventListener('blur', () => {
|
||||||
|
inputHeroSmsMinPrice.value = normalizePhoneSmsMinPriceValue(inputHeroSmsMinPrice.value, getSelectedPhoneSmsProvider());
|
||||||
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
|
});
|
||||||
|
|
||||||
inputFiveSimOperator?.addEventListener('input', () => {
|
inputFiveSimOperator?.addEventListener('input', () => {
|
||||||
markSettingsDirty(true);
|
markSettingsDirty(true);
|
||||||
@@ -14625,6 +14778,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|||||||
? normalizeFiveSimMaxPriceValue(message.payload.fiveSimMaxPrice !== undefined ? message.payload.fiveSimMaxPrice : latestState?.fiveSimMaxPrice)
|
? normalizeFiveSimMaxPriceValue(message.payload.fiveSimMaxPrice !== undefined ? message.payload.fiveSimMaxPrice : latestState?.fiveSimMaxPrice)
|
||||||
: normalizeHeroSmsMaxPriceValue(message.payload.heroSmsMaxPrice !== undefined ? message.payload.heroSmsMaxPrice : latestState?.heroSmsMaxPrice);
|
: normalizeHeroSmsMaxPriceValue(message.payload.heroSmsMaxPrice !== undefined ? message.payload.heroSmsMaxPrice : latestState?.heroSmsMaxPrice);
|
||||||
}
|
}
|
||||||
|
if ((message.payload.heroSmsMinPrice !== undefined || message.payload.fiveSimMinPrice !== undefined) && typeof inputHeroSmsMinPrice !== 'undefined' && inputHeroSmsMinPrice) {
|
||||||
|
inputHeroSmsMinPrice.value = getSelectedPhoneSmsProvider() === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||||
|
? normalizePhoneSmsMinPriceValue(message.payload.fiveSimMinPrice !== undefined ? message.payload.fiveSimMinPrice : latestState?.fiveSimMinPrice, PHONE_SMS_PROVIDER_FIVE_SIM)
|
||||||
|
: normalizePhoneSmsMinPriceValue(message.payload.heroSmsMinPrice !== undefined ? message.payload.heroSmsMinPrice : latestState?.heroSmsMinPrice, getSelectedPhoneSmsProvider());
|
||||||
|
}
|
||||||
if (message.payload.fiveSimOperator !== undefined && inputFiveSimOperator) {
|
if (message.payload.fiveSimOperator !== undefined && inputFiveSimOperator) {
|
||||||
inputFiveSimOperator.value = normalizeFiveSimOperator(message.payload.fiveSimOperator);
|
inputFiveSimOperator.value = normalizeFiveSimOperator(message.payload.fiveSimOperator);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,6 +163,8 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
autoStepDelaySeconds: null,
|
autoStepDelaySeconds: null,
|
||||||
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top',
|
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top',
|
||||||
mailProvider: '163',
|
mailProvider: '163',
|
||||||
|
heroSmsMinPrice: '',
|
||||||
|
fiveSimMinPrice: '',
|
||||||
};
|
};
|
||||||
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : (value === 'codex2api' ? 'codex2api' : 'cpa'); }
|
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : (value === 'codex2api' ? 'codex2api' : 'cpa'); }
|
||||||
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
|
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
|
||||||
@@ -241,6 +243,8 @@ return {
|
|||||||
assert.equal(api.normalizePersistentSettingValue('phoneCodeTimeoutWindows', '3'), 3);
|
assert.equal(api.normalizePersistentSettingValue('phoneCodeTimeoutWindows', '3'), 3);
|
||||||
assert.equal(api.normalizePersistentSettingValue('phoneCodePollIntervalSeconds', '6'), 6);
|
assert.equal(api.normalizePersistentSettingValue('phoneCodePollIntervalSeconds', '6'), 6);
|
||||||
assert.equal(api.normalizePersistentSettingValue('phoneCodePollMaxRounds', '18'), 18);
|
assert.equal(api.normalizePersistentSettingValue('phoneCodePollMaxRounds', '18'), 18);
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('heroSmsMinPrice', '0.123456'), '0.1235');
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('heroSmsMinPrice', '0'), '');
|
||||||
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0.123456'), '0.1235');
|
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0.123456'), '0.1235');
|
||||||
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0'), '');
|
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0'), '');
|
||||||
assert.equal(api.normalizePersistentSettingValue('heroSmsPreferredPrice', '0.051234'), '0.0512');
|
assert.equal(api.normalizePersistentSettingValue('heroSmsPreferredPrice', '0.051234'), '0.0512');
|
||||||
@@ -259,6 +263,8 @@ return {
|
|||||||
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryLabel', ''), '越南 (Vietnam)');
|
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryLabel', ''), '越南 (Vietnam)');
|
||||||
assert.equal(api.normalizePersistentSettingValue('fiveSimMaxPrice', '9.87654'), '9.8765');
|
assert.equal(api.normalizePersistentSettingValue('fiveSimMaxPrice', '9.87654'), '9.8765');
|
||||||
assert.equal(api.normalizePersistentSettingValue('fiveSimMaxPrice', '-1'), '');
|
assert.equal(api.normalizePersistentSettingValue('fiveSimMaxPrice', '-1'), '');
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('fiveSimMinPrice', '9.87654'), '9.8765');
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('fiveSimMinPrice', '-1'), '');
|
||||||
assert.equal(api.normalizePersistentSettingValue('fiveSimOperator', ''), 'any');
|
assert.equal(api.normalizePersistentSettingValue('fiveSimOperator', ''), 'any');
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
api.normalizePersistentSettingValue('fiveSimCountryFallback', [{ id: 'usa', label: 'USA' }, 'thailand:Thailand']),
|
api.normalizePersistentSettingValue('fiveSimCountryFallback', [{ id: 'usa', label: 'USA' }, 'thailand:Thailand']),
|
||||||
@@ -326,6 +332,12 @@ return {
|
|||||||
[1, 6]
|
[1, 6]
|
||||||
);
|
);
|
||||||
assert.equal(api.normalizePersistentSettingValue('nexSmsServiceCode', ' OT! '), 'ot');
|
assert.equal(api.normalizePersistentSettingValue('nexSmsServiceCode', ' OT! '), 'ot');
|
||||||
|
const rangePayload = api.buildPersistentSettingsPayload({
|
||||||
|
heroSmsMinPrice: '0.023456',
|
||||||
|
fiveSimMinPrice: '0.0789',
|
||||||
|
});
|
||||||
|
assert.equal(rangePayload.heroSmsMinPrice, '0.0235');
|
||||||
|
assert.equal(rangePayload.fiveSimMinPrice, '0.0789');
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
api.normalizePersistentSettingValue('phonePreferredActivation', {
|
api.normalizePersistentSettingValue('phonePreferredActivation', {
|
||||||
provider: 'nexsms',
|
provider: 'nexsms',
|
||||||
|
|||||||
@@ -1747,6 +1747,129 @@ test('phone verification helper climbs price tiers when NO_NUMBERS is returned a
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('phone verification helper filters HeroSMS tiers by minimum price and ignores out-of-range preferred tier', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
requests.push(parsedUrl);
|
||||||
|
const action = parsedUrl.searchParams.get('action');
|
||||||
|
const maxPrice = parsedUrl.searchParams.get('maxPrice');
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
52: {
|
||||||
|
dr: {
|
||||||
|
low: { cost: 0.04, count: 100 },
|
||||||
|
high: { cost: 0.09, count: 100 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'getNumber' && maxPrice === '0.09') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => 'ACCESS_NUMBER:989899:66951112223',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action} @ ${maxPrice || 'no-price'}`);
|
||||||
|
},
|
||||||
|
getState: async () => ({ heroSmsApiKey: 'demo-key' }),
|
||||||
|
sendToContentScriptResilient: async () => ({}),
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const activation = await helpers.requestPhoneActivation({
|
||||||
|
heroSmsApiKey: 'demo-key',
|
||||||
|
heroSmsMinPrice: '0.06',
|
||||||
|
heroSmsPreferredPrice: '0.04',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(activation.activationId, '989899');
|
||||||
|
const actions = requests.map((requestUrl) => `${requestUrl.searchParams.get('action')}:${requestUrl.searchParams.get('maxPrice') || ''}`);
|
||||||
|
assert.deepStrictEqual(actions, [
|
||||||
|
'getPrices:',
|
||||||
|
'getNumber:0.09',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper rejects HeroSMS WRONG_MAX_PRICE below configured minimum price', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
requests.push(parsedUrl);
|
||||||
|
const action = parsedUrl.searchParams.get('action');
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => buildHeroSmsPricesPayload({ cost: 0.08 }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'getNumber' || action === 'getNumberV2') {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
text: async () => 'WRONG_MAX_PRICE:0.05',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}`);
|
||||||
|
},
|
||||||
|
getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsMinPrice: '0.07' }),
|
||||||
|
sendToContentScriptResilient: async () => ({}),
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key', heroSmsMinPrice: '0.07' }),
|
||||||
|
/below configured minPrice=0\.07/i
|
||||||
|
);
|
||||||
|
|
||||||
|
const actions = requests.map((requestUrl) => `${requestUrl.searchParams.get('action')}:${requestUrl.searchParams.get('maxPrice') || ''}`);
|
||||||
|
assert.deepStrictEqual(actions, [
|
||||||
|
'getPrices:',
|
||||||
|
'getNumber:0.08',
|
||||||
|
'getNumberV2:0.08',
|
||||||
|
]);
|
||||||
|
assert.equal(actions.some((entry) => entry.endsWith(':0.05')), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper rejects reversed price range before fetching prices', async () => {
|
||||||
|
let fetchCalled = false;
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async () => {
|
||||||
|
fetchCalled = true;
|
||||||
|
throw new Error('fetch should not run for an invalid range');
|
||||||
|
},
|
||||||
|
getState: async () => ({ heroSmsApiKey: 'demo-key' }),
|
||||||
|
sendToContentScriptResilient: async () => ({}),
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
helpers.requestPhoneActivation({
|
||||||
|
heroSmsApiKey: 'demo-key',
|
||||||
|
heroSmsMinPrice: '0.2',
|
||||||
|
heroSmsMaxPrice: '0.1',
|
||||||
|
}),
|
||||||
|
/price range is invalid/i
|
||||||
|
);
|
||||||
|
assert.equal(fetchCalled, false);
|
||||||
|
});
|
||||||
|
|
||||||
test('phone verification helper stops when WRONG_MAX_PRICE exceeds configured max price limit', async () => {
|
test('phone verification helper stops when WRONG_MAX_PRICE exceeds configured max price limit', async () => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
const helpers = api.createPhoneVerificationHelpers({
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
@@ -2157,6 +2280,77 @@ test('phone verification helper rejects 5sim maxPrice with custom operator befor
|
|||||||
assert.deepStrictEqual(requests, []);
|
assert.deepStrictEqual(requests, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('phone verification helper keeps 5sim maxPrice independent from HeroSMS maxPrice', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
requests.push(parsedUrl);
|
||||||
|
if (parsedUrl.pathname === '/v1/guest/prices') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
openai: {
|
||||||
|
thailand: {
|
||||||
|
any: {
|
||||||
|
low: { cost: 0.08, count: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (parsedUrl.pathname === '/v1/user/buy/activation/thailand/any/openai') {
|
||||||
|
if (parsedUrl.searchParams.get('maxPrice') === '0.08') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
id: 900010,
|
||||||
|
phone: '+66951112235',
|
||||||
|
country: 'thailand',
|
||||||
|
country_name: 'Thailand',
|
||||||
|
product: 'openai',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected 5sim request: ${parsedUrl.pathname}${parsedUrl.search}`);
|
||||||
|
},
|
||||||
|
getState: async () => ({
|
||||||
|
phoneSmsProvider: '5sim',
|
||||||
|
fiveSimApiKey: 'five-token',
|
||||||
|
fiveSimCountryOrder: ['thailand'],
|
||||||
|
fiveSimOperator: 'any',
|
||||||
|
fiveSimProduct: 'openai',
|
||||||
|
heroSmsMaxPrice: '0.06',
|
||||||
|
heroSmsActivationRetryRounds: 1,
|
||||||
|
}),
|
||||||
|
sendToContentScriptResilient: async () => ({}),
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const activation = await helpers.requestPhoneActivation({
|
||||||
|
phoneSmsProvider: '5sim',
|
||||||
|
fiveSimApiKey: 'five-token',
|
||||||
|
fiveSimCountryOrder: ['thailand'],
|
||||||
|
fiveSimOperator: 'any',
|
||||||
|
fiveSimProduct: 'openai',
|
||||||
|
heroSmsMaxPrice: '0.06',
|
||||||
|
heroSmsActivationRetryRounds: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(activation.phoneNumber, '+66951112235');
|
||||||
|
const buyRequests = requests.filter((entry) => entry.pathname === '/v1/user/buy/activation/thailand/any/openai');
|
||||||
|
assert.equal(buyRequests.length, 1);
|
||||||
|
assert.equal(buyRequests[0].searchParams.get('maxPrice'), '0.08');
|
||||||
|
});
|
||||||
|
|
||||||
test('phone verification helper honors price-priority ordering for 5sim countries', async () => {
|
test('phone verification helper honors price-priority ordering for 5sim countries', async () => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
const helpers = api.createPhoneVerificationHelpers({
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
@@ -2324,6 +2518,80 @@ test('phone verification helper tries multiple 5sim price tiers within the same
|
|||||||
assert.equal(buyRequests[1].includes('maxPrice=0.08'), true);
|
assert.equal(buyRequests[1].includes('maxPrice=0.08'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('phone verification helper filters 5sim tiers by minimum price before buying', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
requests.push(parsedUrl.pathname + parsedUrl.search);
|
||||||
|
if (parsedUrl.pathname === '/v1/guest/prices') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
openai: {
|
||||||
|
thailand: {
|
||||||
|
any: {
|
||||||
|
low: { cost: 0.05, count: 3 },
|
||||||
|
high: { cost: 0.08, count: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (parsedUrl.pathname === '/v1/user/buy/activation/thailand/any/openai') {
|
||||||
|
const maxPrice = parsedUrl.searchParams.get('maxPrice');
|
||||||
|
if (maxPrice === '0.08') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
id: 800002,
|
||||||
|
phone: '+66951112234',
|
||||||
|
country: 'thailand',
|
||||||
|
country_name: 'Thailand',
|
||||||
|
product: 'openai',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected 5sim request: ${parsedUrl.pathname}${parsedUrl.search}`);
|
||||||
|
},
|
||||||
|
getState: async () => ({
|
||||||
|
phoneSmsProvider: '5sim',
|
||||||
|
fiveSimApiKey: 'five-token',
|
||||||
|
fiveSimCountryOrder: ['thailand'],
|
||||||
|
fiveSimOperator: 'any',
|
||||||
|
fiveSimProduct: 'openai',
|
||||||
|
fiveSimMinPrice: '0.07',
|
||||||
|
heroSmsActivationRetryRounds: 1,
|
||||||
|
}),
|
||||||
|
sendToContentScriptResilient: async () => ({}),
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const activation = await helpers.requestPhoneActivation({
|
||||||
|
phoneSmsProvider: '5sim',
|
||||||
|
fiveSimApiKey: 'five-token',
|
||||||
|
fiveSimCountryOrder: ['thailand'],
|
||||||
|
fiveSimOperator: 'any',
|
||||||
|
fiveSimProduct: 'openai',
|
||||||
|
fiveSimMinPrice: '0.07',
|
||||||
|
heroSmsActivationRetryRounds: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(activation.phoneNumber, '+66951112234');
|
||||||
|
const buyRequests = requests.filter((entry) => entry.startsWith('/v1/user/buy/activation/thailand/any/openai'));
|
||||||
|
assert.equal(buyRequests.length, 1);
|
||||||
|
assert.equal(buyRequests[0].includes('maxPrice=0.08'), true);
|
||||||
|
assert.equal(buyRequests[0].includes('maxPrice=0.05'), false);
|
||||||
|
});
|
||||||
|
|
||||||
test('phone verification helper polls and parses 5sim verification codes', async () => {
|
test('phone verification helper polls and parses 5sim verification codes', async () => {
|
||||||
let checkCount = 0;
|
let checkCount = 0;
|
||||||
const statusUpdates = [];
|
const statusUpdates = [];
|
||||||
@@ -2661,6 +2929,96 @@ test('phone verification helper acquires a number from NexSMS with ordered fallb
|
|||||||
assert.equal(requests[3].body?.countryId, 6);
|
assert.equal(requests[3].body?.countryId, 6);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('phone verification helper filters NexSMS tiers by minimum price before purchase', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url, options = {}) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
const method = String(options?.method || 'GET').toUpperCase();
|
||||||
|
const body = options?.body ? JSON.parse(options.body) : null;
|
||||||
|
requests.push({
|
||||||
|
pathname: parsedUrl.pathname,
|
||||||
|
search: parsedUrl.searchParams,
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parsedUrl.pathname === '/api/getCountryByService') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
countryId: 6,
|
||||||
|
countryName: 'Indonesia',
|
||||||
|
minPrice: 0.03,
|
||||||
|
priceMap: { '0.03': 4, '0.08': 2 },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedUrl.pathname === '/api/order/purchase') {
|
||||||
|
if (body?.price === 0.08) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
countryId: 6,
|
||||||
|
countryName: 'Indonesia',
|
||||||
|
serviceCode: 'ot',
|
||||||
|
phoneNumbers: ['+6281234567891'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unexpected NexSMS request: ${parsedUrl.pathname}`);
|
||||||
|
},
|
||||||
|
getState: async () => ({
|
||||||
|
phoneSmsProvider: 'nexsms',
|
||||||
|
nexSmsApiKey: 'nex-key',
|
||||||
|
nexSmsCountryOrder: [6],
|
||||||
|
nexSmsServiceCode: 'ot',
|
||||||
|
heroSmsMinPrice: '0.05',
|
||||||
|
heroSmsActivationRetryRounds: 1,
|
||||||
|
}),
|
||||||
|
sendToContentScriptResilient: async () => ({}),
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const activation = await helpers.requestPhoneActivation({
|
||||||
|
phoneSmsProvider: 'nexsms',
|
||||||
|
nexSmsApiKey: 'nex-key',
|
||||||
|
nexSmsCountryOrder: [6],
|
||||||
|
nexSmsServiceCode: 'ot',
|
||||||
|
heroSmsMinPrice: '0.05',
|
||||||
|
heroSmsActivationRetryRounds: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(activation, {
|
||||||
|
activationId: '+6281234567891',
|
||||||
|
phoneNumber: '+6281234567891',
|
||||||
|
provider: 'nexsms',
|
||||||
|
serviceCode: 'ot',
|
||||||
|
countryId: 6,
|
||||||
|
countryLabel: 'Indonesia',
|
||||||
|
successfulUses: 0,
|
||||||
|
maxUses: 1,
|
||||||
|
});
|
||||||
|
const purchaseRequests = requests.filter((entry) => entry.pathname === '/api/order/purchase');
|
||||||
|
assert.equal(purchaseRequests.length, 1);
|
||||||
|
assert.equal(purchaseRequests[0].body?.price, 0.08);
|
||||||
|
});
|
||||||
|
|
||||||
test('phone verification helper polls and parses NexSMS verification codes', async () => {
|
test('phone verification helper polls and parses NexSMS verification codes', async () => {
|
||||||
let pollCount = 0;
|
let pollCount = 0;
|
||||||
const statusUpdates = [];
|
const statusUpdates = [];
|
||||||
@@ -7463,6 +7821,7 @@ test('phone verification helper logs no-supply diagnostics with consecutive stre
|
|||||||
heroSmsCountryId: 52,
|
heroSmsCountryId: 52,
|
||||||
heroSmsCountryLabel: 'Thailand',
|
heroSmsCountryLabel: 'Thailand',
|
||||||
heroSmsCountryFallback: [],
|
heroSmsCountryFallback: [],
|
||||||
|
heroSmsMinPrice: '0.04',
|
||||||
heroSmsMaxPrice: '0.06',
|
heroSmsMaxPrice: '0.06',
|
||||||
currentPhoneActivation: null,
|
currentPhoneActivation: null,
|
||||||
reusablePhoneActivation: null,
|
reusablePhoneActivation: null,
|
||||||
@@ -7525,6 +7884,14 @@ test('phone verification helper logs no-supply diagnostics with consecutive stre
|
|||||||
assert.equal(diagnosticsLogs.every((entry) => entry.options?.stepKey === 'phone-verification'), true);
|
assert.equal(diagnosticsLogs.every((entry) => entry.options?.stepKey === 'phone-verification'), true);
|
||||||
assert.equal(diagnosticsLogs.some((entry) => entry.message.includes('无号连续失败 1 次')), true);
|
assert.equal(diagnosticsLogs.some((entry) => entry.message.includes('无号连续失败 1 次')), true);
|
||||||
assert.equal(diagnosticsLogs.some((entry) => entry.message.includes('无号连续失败 2 次')), true);
|
assert.equal(diagnosticsLogs.some((entry) => entry.message.includes('无号连续失败 2 次')), true);
|
||||||
|
assert.equal(
|
||||||
|
diagnosticsLogs.some((entry) => entry.message.includes('priceRange=0.04~0.06')),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
diagnosticsLogs.some((entry) => entry.message.includes('minPrice=0.04')),
|
||||||
|
true
|
||||||
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
diagnosticsLogs.some((entry) => entry.message.includes('maxPrice=0.06')),
|
diagnosticsLogs.some((entry) => entry.message.includes('maxPrice=0.06')),
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
|
|||||||
assert.doesNotMatch(html, /id="select-hero-sms-country-fallback"/);
|
assert.doesNotMatch(html, /id="select-hero-sms-country-fallback"/);
|
||||||
assert.match(html, /id="row-hero-sms-api-key"/);
|
assert.match(html, /id="row-hero-sms-api-key"/);
|
||||||
assert.match(html, /id="row-hero-sms-max-price"/);
|
assert.match(html, /id="row-hero-sms-max-price"/);
|
||||||
|
assert.match(html, /id="input-hero-sms-min-price"/);
|
||||||
assert.match(html, /id="btn-phone-sms-balance"/);
|
assert.match(html, /id="btn-phone-sms-balance"/);
|
||||||
assert.match(html, /id="display-phone-sms-balance"/);
|
assert.match(html, /id="display-phone-sms-balance"/);
|
||||||
assert.match(html, /id="row-five-sim-operator"/);
|
assert.match(html, /id="row-five-sim-operator"/);
|
||||||
@@ -756,6 +757,8 @@ let latestState = {
|
|||||||
mail2925UseAccountPool: false,
|
mail2925UseAccountPool: false,
|
||||||
currentMail2925AccountId: '',
|
currentMail2925AccountId: '',
|
||||||
fiveSimCountryOrder: ['thailand', 'england'],
|
fiveSimCountryOrder: ['thailand', 'england'],
|
||||||
|
heroSmsMinPrice: '0.0444',
|
||||||
|
fiveSimMinPrice: '0.3333',
|
||||||
};
|
};
|
||||||
let cloudflareDomainEditMode = false;
|
let cloudflareDomainEditMode = false;
|
||||||
let cloudflareTempEmailDomainEditMode = false;
|
let cloudflareTempEmailDomainEditMode = false;
|
||||||
@@ -820,6 +823,7 @@ function getSelectedPhonePreferredActivation() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const inputHeroSmsMaxPrice = { value: '0.12' };
|
const inputHeroSmsMaxPrice = { value: '0.12' };
|
||||||
|
const inputHeroSmsMinPrice = { value: '0.03' };
|
||||||
const inputHeroSmsPreferredPrice = { value: '0.0512' };
|
const inputHeroSmsPreferredPrice = { value: '0.0512' };
|
||||||
const inputPhoneReplacementLimit = { value: '5' };
|
const inputPhoneReplacementLimit = { value: '5' };
|
||||||
const inputPhoneCodeWaitSeconds = { value: '75' };
|
const inputPhoneCodeWaitSeconds = { value: '75' };
|
||||||
@@ -900,6 +904,7 @@ ${extractFunction('normalizeFiveSimOperator')}
|
|||||||
${extractFunction('normalizeFiveSimMaxPriceValue')}
|
${extractFunction('normalizeFiveSimMaxPriceValue')}
|
||||||
${extractFunction('normalizeFiveSimCountryFallbackList')}
|
${extractFunction('normalizeFiveSimCountryFallbackList')}
|
||||||
${extractFunction('normalizePhoneSmsMaxPriceValue')}
|
${extractFunction('normalizePhoneSmsMaxPriceValue')}
|
||||||
|
${extractFunction('normalizePhoneSmsMinPriceValue')}
|
||||||
${extractFunction('normalizeHeroSmsMaxPriceValue')}
|
${extractFunction('normalizeHeroSmsMaxPriceValue')}
|
||||||
${extractFunction('normalizePhoneVerificationReplacementLimit')}
|
${extractFunction('normalizePhoneVerificationReplacementLimit')}
|
||||||
${extractFunction('normalizePhoneCodeWaitSecondsValue')}
|
${extractFunction('normalizePhoneCodeWaitSecondsValue')}
|
||||||
@@ -948,6 +953,7 @@ return { collectSettingsPayload };
|
|||||||
assert.equal(payload.freePhoneReuseEnabled, true);
|
assert.equal(payload.freePhoneReuseEnabled, true);
|
||||||
assert.equal(payload.freePhoneReuseAutoEnabled, true);
|
assert.equal(payload.freePhoneReuseAutoEnabled, true);
|
||||||
assert.equal(payload.heroSmsAcquirePriority, 'price');
|
assert.equal(payload.heroSmsAcquirePriority, 'price');
|
||||||
|
assert.equal(payload.heroSmsMinPrice, '0.03');
|
||||||
assert.equal(payload.heroSmsMaxPrice, '0.12');
|
assert.equal(payload.heroSmsMaxPrice, '0.12');
|
||||||
assert.equal(payload.heroSmsPreferredPrice, '0.0512');
|
assert.equal(payload.heroSmsPreferredPrice, '0.0512');
|
||||||
assert.deepStrictEqual(payload.phonePreferredActivation, {
|
assert.deepStrictEqual(payload.phonePreferredActivation, {
|
||||||
@@ -969,6 +975,7 @@ return { collectSettingsPayload };
|
|||||||
assert.deepStrictEqual(payload.heroSmsCountryFallback, [{ id: 16, label: 'United Kingdom' }]);
|
assert.deepStrictEqual(payload.heroSmsCountryFallback, [{ id: 16, label: 'United Kingdom' }]);
|
||||||
assert.equal(payload.fiveSimApiKey, 'five-sim-key');
|
assert.equal(payload.fiveSimApiKey, 'five-sim-key');
|
||||||
assert.equal(payload.fiveSimCountryId, 'vietnam');
|
assert.equal(payload.fiveSimCountryId, 'vietnam');
|
||||||
|
assert.equal(payload.fiveSimMinPrice, '0.3333');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('switchPhoneSmsProvider saves API keys independently when the select value has already changed', async () => {
|
test('switchPhoneSmsProvider saves API keys independently when the select value has already changed', async () => {
|
||||||
@@ -977,7 +984,9 @@ let latestState = {
|
|||||||
phoneSmsProvider: 'hero-sms',
|
phoneSmsProvider: 'hero-sms',
|
||||||
heroSmsApiKey: 'hero-old',
|
heroSmsApiKey: 'hero-old',
|
||||||
fiveSimApiKey: 'five-old',
|
fiveSimApiKey: 'five-old',
|
||||||
|
heroSmsMinPrice: '0.04',
|
||||||
heroSmsMaxPrice: '0.11',
|
heroSmsMaxPrice: '0.11',
|
||||||
|
fiveSimMinPrice: '0.88',
|
||||||
fiveSimMaxPrice: '12',
|
fiveSimMaxPrice: '12',
|
||||||
heroSmsCountryId: 52,
|
heroSmsCountryId: 52,
|
||||||
heroSmsCountryLabel: 'Thailand',
|
heroSmsCountryLabel: 'Thailand',
|
||||||
@@ -998,6 +1007,7 @@ const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vie
|
|||||||
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
|
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
|
||||||
const selectPhoneSmsProvider = { value: 'hero-sms', dataset: { activeProvider: 'hero-sms' } };
|
const selectPhoneSmsProvider = { value: 'hero-sms', dataset: { activeProvider: 'hero-sms' } };
|
||||||
const inputHeroSmsApiKey = { value: 'hero-live' };
|
const inputHeroSmsApiKey = { value: 'hero-live' };
|
||||||
|
const inputHeroSmsMinPrice = { value: '0.03' };
|
||||||
const inputHeroSmsMaxPrice = { value: '0.22' };
|
const inputHeroSmsMaxPrice = { value: '0.22' };
|
||||||
const inputFiveSimOperator = { value: 'any' };
|
const inputFiveSimOperator = { value: 'any' };
|
||||||
const displayHeroSmsPriceTiers = { textContent: '' };
|
const displayHeroSmsPriceTiers = { textContent: '' };
|
||||||
@@ -1015,6 +1025,7 @@ ${extractFunction('normalizeFiveSimCountryLabel')}
|
|||||||
${extractFunction('normalizeFiveSimOperator')}
|
${extractFunction('normalizeFiveSimOperator')}
|
||||||
${extractFunction('normalizeFiveSimMaxPriceValue')}
|
${extractFunction('normalizeFiveSimMaxPriceValue')}
|
||||||
${extractFunction('normalizeHeroSmsMaxPriceValue')}
|
${extractFunction('normalizeHeroSmsMaxPriceValue')}
|
||||||
|
${extractFunction('normalizePhoneSmsMinPriceValue')}
|
||||||
${extractFunction('normalizePhoneSmsMaxPriceValue')}
|
${extractFunction('normalizePhoneSmsMaxPriceValue')}
|
||||||
${extractFunction('normalizeHeroSmsCountryId')}
|
${extractFunction('normalizeHeroSmsCountryId')}
|
||||||
${extractFunction('normalizeHeroSmsCountryLabel')}
|
${extractFunction('normalizeHeroSmsCountryLabel')}
|
||||||
@@ -1042,6 +1053,7 @@ ${extractFunction('switchPhoneSmsProvider')}
|
|||||||
return {
|
return {
|
||||||
selectPhoneSmsProvider,
|
selectPhoneSmsProvider,
|
||||||
inputHeroSmsApiKey,
|
inputHeroSmsApiKey,
|
||||||
|
inputHeroSmsMinPrice,
|
||||||
get latestState() { return latestState; },
|
get latestState() { return latestState; },
|
||||||
get savedPayload() { return savedPayload; },
|
get savedPayload() { return savedPayload; },
|
||||||
switchPhoneSmsProvider,
|
switchPhoneSmsProvider,
|
||||||
@@ -1055,7 +1067,10 @@ return {
|
|||||||
assert.equal(api.latestState.phoneSmsProvider, '5sim');
|
assert.equal(api.latestState.phoneSmsProvider, '5sim');
|
||||||
assert.equal(api.latestState.heroSmsApiKey, 'hero-live');
|
assert.equal(api.latestState.heroSmsApiKey, 'hero-live');
|
||||||
assert.equal(api.latestState.fiveSimApiKey, 'five-old');
|
assert.equal(api.latestState.fiveSimApiKey, 'five-old');
|
||||||
|
assert.equal(api.latestState.heroSmsMinPrice, '0.03');
|
||||||
|
assert.equal(api.latestState.fiveSimMinPrice, '0.88');
|
||||||
assert.equal(api.inputHeroSmsApiKey.value, 'five-old');
|
assert.equal(api.inputHeroSmsApiKey.value, 'five-old');
|
||||||
|
assert.equal(api.inputHeroSmsMinPrice.value, '0.88');
|
||||||
assert.equal(api.selectPhoneSmsProvider.dataset.activeProvider, '5sim');
|
assert.equal(api.selectPhoneSmsProvider.dataset.activeProvider, '5sim');
|
||||||
|
|
||||||
api.inputHeroSmsApiKey.value = 'five-live';
|
api.inputHeroSmsApiKey.value = 'five-live';
|
||||||
@@ -1065,10 +1080,15 @@ return {
|
|||||||
assert.equal(api.latestState.phoneSmsProvider, 'hero-sms');
|
assert.equal(api.latestState.phoneSmsProvider, 'hero-sms');
|
||||||
assert.equal(api.latestState.heroSmsApiKey, 'hero-live');
|
assert.equal(api.latestState.heroSmsApiKey, 'hero-live');
|
||||||
assert.equal(api.latestState.fiveSimApiKey, 'five-live');
|
assert.equal(api.latestState.fiveSimApiKey, 'five-live');
|
||||||
|
assert.equal(api.latestState.heroSmsMinPrice, '0.03');
|
||||||
|
assert.equal(api.latestState.fiveSimMinPrice, '0.88');
|
||||||
assert.equal(api.inputHeroSmsApiKey.value, 'hero-live');
|
assert.equal(api.inputHeroSmsApiKey.value, 'hero-live');
|
||||||
|
assert.equal(api.inputHeroSmsMinPrice.value, '0.03');
|
||||||
assert.equal(api.selectPhoneSmsProvider.dataset.activeProvider, 'hero-sms');
|
assert.equal(api.selectPhoneSmsProvider.dataset.activeProvider, 'hero-sms');
|
||||||
assert.equal(api.savedPayload.heroSmsApiKey, 'hero-live');
|
assert.equal(api.savedPayload.heroSmsApiKey, 'hero-live');
|
||||||
assert.equal(api.savedPayload.fiveSimApiKey, 'five-live');
|
assert.equal(api.savedPayload.fiveSimApiKey, 'five-live');
|
||||||
|
assert.equal(api.savedPayload.heroSmsMinPrice, '0.03');
|
||||||
|
assert.equal(api.savedPayload.fiveSimMinPrice, '0.88');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('formatPhoneSmsPriceEntriesSummary treats HeroSMS physicalCount=0 as out of stock even when count is positive', () => {
|
test('formatPhoneSmsPriceEntriesSummary treats HeroSMS physicalCount=0 as out of stock even when count is positive', () => {
|
||||||
@@ -1107,6 +1127,7 @@ const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
|
|||||||
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
|
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
|
||||||
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
|
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
|
||||||
const inputHeroSmsMaxPrice = { value: '' };
|
const inputHeroSmsMaxPrice = { value: '' };
|
||||||
|
const inputHeroSmsMinPrice = { value: '0.1' };
|
||||||
const inputHeroSmsApiKey = { value: '' };
|
const inputHeroSmsApiKey = { value: '' };
|
||||||
const inputFiveSimOperator = { value: 'any' };
|
const inputFiveSimOperator = { value: 'any' };
|
||||||
const inputFiveSimProduct = { value: 'openai' };
|
const inputFiveSimProduct = { value: 'openai' };
|
||||||
@@ -1126,6 +1147,7 @@ ${extractFunction('normalizeFiveSimCountryCode')}
|
|||||||
${extractFunction('normalizeFiveSimProductValue')}
|
${extractFunction('normalizeFiveSimProductValue')}
|
||||||
${extractFunction('normalizeFiveSimOperator')}
|
${extractFunction('normalizeFiveSimOperator')}
|
||||||
${extractFunction('normalizePhoneSmsMaxPriceValue')}
|
${extractFunction('normalizePhoneSmsMaxPriceValue')}
|
||||||
|
${extractFunction('normalizePhoneSmsMinPriceValue')}
|
||||||
${extractFunction('normalizeFiveSimMaxPriceValue')}
|
${extractFunction('normalizeFiveSimMaxPriceValue')}
|
||||||
${extractFunction('normalizeHeroSmsMaxPriceValue')}
|
${extractFunction('normalizeHeroSmsMaxPriceValue')}
|
||||||
${extractFunction('normalizeHeroSmsPriceForPreview')}
|
${extractFunction('normalizeHeroSmsPriceForPreview')}
|
||||||
@@ -1135,6 +1157,12 @@ ${extractFunction('collectHeroSmsPriceEntriesForPreview')}
|
|||||||
${extractFunction('formatPhoneSmsPriceEntriesSummary')}
|
${extractFunction('formatPhoneSmsPriceEntriesSummary')}
|
||||||
${extractFunction('describeHeroSmsPreviewPayload')}
|
${extractFunction('describeHeroSmsPreviewPayload')}
|
||||||
${extractFunction('summarizeHeroSmsPreviewError')}
|
${extractFunction('summarizeHeroSmsPreviewError')}
|
||||||
|
${extractFunction('resolvePhoneSmsPricePreviewRange')}
|
||||||
|
${extractFunction('isPhoneSmsPriceWithinPreviewRange')}
|
||||||
|
${extractFunction('filterPhoneSmsPriceEntriesForPreviewRange')}
|
||||||
|
${extractFunction('filterPhoneSmsPriceValuesForPreviewRange')}
|
||||||
|
${extractFunction('formatPhoneSmsPriceRangePreviewText')}
|
||||||
|
${extractFunction('buildPhoneSmsPriceRangePreviewMessage')}
|
||||||
${extractFunction('formatPriceTiersForPreview')}
|
${extractFunction('formatPriceTiersForPreview')}
|
||||||
${extractFunction('formatPriceTiersWithZeroStockForPreview')}
|
${extractFunction('formatPriceTiersWithZeroStockForPreview')}
|
||||||
function normalizeHeroSmsFetchErrorMessage(error) { return error?.message || String(error); }
|
function normalizeHeroSmsFetchErrorMessage(error) { return error?.message || String(error); }
|
||||||
@@ -1193,7 +1221,7 @@ return {
|
|||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
api.displayHeroSmsPriceTiers.textContent,
|
api.displayHeroSmsPriceTiers.textContent,
|
||||||
'5sim:\n越南 (Vietnam): 最低 0.1282;档位:0.0769(x0), 0.1282(x4608)'
|
'5sim:\n越南 (Vietnam): 区间内最低 0.1282;档位:0.1282(x4608)'
|
||||||
);
|
);
|
||||||
assert.equal(api.rowHeroSmsPriceTiers.style.display, '');
|
assert.equal(api.rowHeroSmsPriceTiers.style.display, '');
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
@@ -1211,4 +1239,12 @@ test('hero sms max price input does not auto-save partial typing states', () =>
|
|||||||
sidepanelSource,
|
sidepanelSource,
|
||||||
/inputHeroSmsMaxPrice\?\.\s*addEventListener\('input',\s*\(\)\s*=>\s*\{\s*markSettingsDirty\(true\);\s*scheduleSettingsAutoSave\(\);/
|
/inputHeroSmsMaxPrice\?\.\s*addEventListener\('input',\s*\(\)\s*=>\s*\{\s*markSettingsDirty\(true\);\s*scheduleSettingsAutoSave\(\);/
|
||||||
);
|
);
|
||||||
|
assert.match(
|
||||||
|
sidepanelSource,
|
||||||
|
/inputHeroSmsMinPrice\?\.\s*addEventListener\('input',\s*\(\)\s*=>\s*\{\s*markSettingsDirty\(true\);\s*\}\);/
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
sidepanelSource,
|
||||||
|
/inputHeroSmsMinPrice\?\.\s*addEventListener\('input',\s*\(\)\s*=>\s*\{\s*markSettingsDirty\(true\);\s*scheduleSettingsAutoSave\(\);/
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user