feat: 手动配置 HeroSMS maxPrice 并调整手机号复用计数

This commit is contained in:
zhangkun
2026-04-28 02:51:48 +08:00
parent 0f430113fa
commit 127fd1c653
6 changed files with 264 additions and 396 deletions
+59 -144
View File
@@ -27,7 +27,6 @@
const DEFAULT_PHONE_SUBMIT_ATTEMPTS = 3;
const DEFAULT_PHONE_CODE_WAIT_WINDOW_MS = 60000;
const DEFAULT_PHONE_NUMBER_MAX_USES = 3;
const DEFAULT_PHONE_PRICE_LOOKUP_ATTEMPTS = 3;
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::';
@@ -47,6 +46,18 @@
return String(value || '').trim();
}
function normalizeManualHeroSmsMaxPrice(value) {
const trimmed = String(value ?? '').trim();
if (!trimmed) {
return null;
}
const price = Number(trimmed);
if (!Number.isFinite(price) || price <= 0) {
return null;
}
return String(price);
}
function normalizeUseCount(value) {
return Math.max(0, Math.floor(Number(value) || 0));
}
@@ -241,8 +252,13 @@
if (!apiKey) {
throw new Error('HeroSMS API key is missing. Save it in the side panel before running the phone flow.');
}
const maxPrice = normalizeManualHeroSmsMaxPrice(state.heroSmsMaxPrice);
if (!maxPrice) {
throw new Error('HeroSMS maxPrice is missing. Fill it in below the country selector before running the phone flow.');
}
return {
apiKey,
maxPrice,
baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL),
};
}
@@ -289,91 +305,10 @@
return activation?.statusAction === 'getStatusV2' ? 'getStatusV2' : 'getStatus';
}
function normalizeHeroSmsPrice(value) {
const price = Number(value);
if (!Number.isFinite(price) || price < 0) {
return null;
}
return price;
}
function collectHeroSmsPriceCandidates(payload, candidates = []) {
if (Array.isArray(payload)) {
payload.forEach((entry) => collectHeroSmsPriceCandidates(entry, candidates));
return candidates;
}
if (!payload || typeof payload !== 'object') {
return candidates;
}
const cost = normalizeHeroSmsPrice(payload.cost);
if (cost !== null) {
const count = Number(payload.count);
const physicalCount = Number(payload.physicalCount);
const hasCount = Number.isFinite(count);
const hasPhysicalCount = Number.isFinite(physicalCount);
if ((!hasCount && !hasPhysicalCount) || count > 0 || physicalCount > 0) {
candidates.push(cost);
}
}
Object.values(payload).forEach((value) => collectHeroSmsPriceCandidates(value, candidates));
return candidates;
}
function findLowestHeroSmsPrice(payload) {
const candidates = collectHeroSmsPriceCandidates(payload, []);
if (!candidates.length) {
return null;
}
return Math.min(...candidates);
}
function isHeroSmsNoNumbersPayload(payload) {
return /\bNO_NUMBERS\b/i.test(describeHeroSmsPayload(payload));
}
function extractHeroSmsWrongMaxPrice(payload) {
if (payload && typeof payload === 'object') {
const title = String(payload.title || '').trim();
const minPrice = normalizeHeroSmsPrice(payload.info?.min);
if (/^WRONG_MAX_PRICE$/i.test(title) && minPrice !== null) {
return minPrice;
}
}
const text = describeHeroSmsPayload(payload);
const match = text.match(/\bWRONG_MAX_PRICE:(\d+(?:\.\d+)?)\b/i);
if (!match) {
return null;
}
return normalizeHeroSmsPrice(match[1]);
}
function isNetworkFetchFailure(error) {
const message = String(error?.message || '').trim();
return /failed to fetch|networkerror|load failed/i.test(message);
}
async function resolveCheapestPhoneActivationPrice(config, countryConfig) {
for (let attempt = 1; attempt <= DEFAULT_PHONE_PRICE_LOOKUP_ATTEMPTS; attempt += 1) {
try {
const payload = await fetchHeroSmsPayload(config, {
action: 'getPrices',
service: HERO_SMS_SERVICE_CODE,
country: countryConfig.id,
}, 'HeroSMS getPrices');
const price = findLowestHeroSmsPrice(payload);
if (price !== null) {
return price;
}
} catch (_) {
// Best-effort lookup only.
}
}
return null;
}
async function fetchPhoneActivationPayload(config, countryConfig, action, options = {}) {
const query = {
action,
@@ -387,49 +322,10 @@
return fetchHeroSmsPayload(config, query, `HeroSMS ${action}`);
}
async function requestPhoneActivationWithPrice(config, countryConfig, action, maxPrice) {
let nextMaxPrice = maxPrice;
let retriedWithUpdatedPrice = false;
let retriedWithoutPrice = false;
while (true) {
try {
return await fetchPhoneActivationPayload(config, countryConfig, action, {
maxPrice: nextMaxPrice,
});
} catch (error) {
const updatedMaxPrice = extractHeroSmsWrongMaxPrice(error?.payload || error?.message);
if (
nextMaxPrice !== null
&& nextMaxPrice !== undefined
&& !retriedWithUpdatedPrice
&& updatedMaxPrice !== null
) {
nextMaxPrice = updatedMaxPrice;
retriedWithUpdatedPrice = true;
continue;
}
if (
nextMaxPrice !== null
&& nextMaxPrice !== undefined
&& !retriedWithoutPrice
&& isNetworkFetchFailure(error)
) {
nextMaxPrice = null;
retriedWithoutPrice = true;
continue;
}
throw error;
}
}
}
async function requestPhoneActivation(state = {}) {
const config = resolvePhoneConfig(state);
const countryConfig = resolveCountryConfig(state);
const maxPrice = await resolveCheapestPhoneActivationPrice(config, countryConfig);
const maxPrice = config.maxPrice;
const buildFallbackActivation = (requestAction) => ({
countryId: countryConfig.id,
...(requestAction === 'getNumberV2' ? { statusAction: 'getStatusV2' } : {}),
@@ -438,19 +334,19 @@
let payload;
try {
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
} catch (error) {
if (!isHeroSmsNoNumbersPayload(error?.payload || error?.message)) {
throw error;
}
requestAction = 'getNumberV2';
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
}
let activation = parseActivationPayload(payload, buildFallbackActivation(requestAction));
if (!activation && requestAction === 'getNumber' && isHeroSmsNoNumbersPayload(payload)) {
requestAction = 'getNumberV2';
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
activation = parseActivationPayload(payload, buildFallbackActivation(requestAction));
}
@@ -496,7 +392,7 @@
}
async function completePhoneActivation(state = {}, activation) {
await setPhoneActivationStatus(state, activation, 6, 'HeroSMS setStatus(6)');
await setPhoneActivationStatus(state, activation, 3, 'HeroSMS setStatus(3)');
}
async function cancelPhoneActivation(state = {}, activation) {
@@ -738,6 +634,20 @@
await persistReusableActivation(null);
}
async function incrementCurrentActivationUseCount(activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
return null;
}
const nextActivation = {
...normalizedActivation,
successfulUses: Math.min(normalizedActivation.successfulUses + 1, normalizedActivation.maxUses),
};
await persistCurrentActivation(nextActivation);
return nextActivation;
}
async function acquirePhoneActivation(state = {}) {
const countryConfig = resolveCountryConfig(state);
const reusableActivation = normalizeActivation(state[REUSABLE_PHONE_ACTIVATION_STATE_KEY]);
@@ -769,28 +679,24 @@
return activation;
}
async function markActivationReusableAfterSuccess(activation) {
async function syncReusableActivationAfterUse(activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
await clearReusableActivation();
return;
}
const successfulUses = normalizedActivation.successfulUses + 1;
if (successfulUses >= normalizedActivation.maxUses) {
if (normalizedActivation.successfulUses >= normalizedActivation.maxUses) {
await clearReusableActivation();
return;
}
await persistReusableActivation({
...normalizedActivation,
successfulUses,
});
await persistReusableActivation(normalizedActivation);
}
async function waitForPhoneCodeOrRotateNumber(tabId, state, activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
let currentActivation = normalizeActivation(activation);
if (!currentActivation) {
throw new Error('Phone activation is missing.');
}
@@ -799,11 +705,11 @@
for (let windowIndex = 1; windowIndex <= 2; windowIndex += 1) {
await addLog(
`Step 9: waiting up to 60 seconds for SMS on ${normalizedActivation.phoneNumber} (${windowIndex}/2).`,
`Step 9: waiting up to 60 seconds for SMS on ${currentActivation.phoneNumber} (${windowIndex}/2).`,
'info'
);
try {
const code = await pollPhoneActivationCode(state, normalizedActivation, {
const code = await pollPhoneActivationCode(state, currentActivation, {
actionLabel: windowIndex === 1
? 'poll phone verification code from HeroSMS'
: 'poll resent phone verification code from HeroSMS',
@@ -820,7 +726,7 @@
lastLoggedStatus = statusText;
lastLoggedPollCount = pollCount;
await addLog(
`Step 9: HeroSMS status for ${normalizedActivation.phoneNumber}: ${statusText} (${Math.ceil(elapsedMs / 1000)}s elapsed).`,
`Step 9: HeroSMS status for ${currentActivation.phoneNumber}: ${statusText} (${Math.ceil(elapsedMs / 1000)}s elapsed).`,
'info'
);
},
@@ -836,10 +742,10 @@
if (windowIndex === 1) {
await addLog(
`Step 9: no SMS arrived for ${normalizedActivation.phoneNumber} within 60 seconds, requesting another SMS.`,
`Step 9: no SMS arrived for ${currentActivation.phoneNumber} within 60 seconds, requesting another SMS.`,
'warn'
);
await requestAdditionalPhoneSms(state, normalizedActivation);
await requestAdditionalPhoneSms(state, currentActivation);
try {
await resendPhoneVerificationCode(tabId);
await addLog('Step 9: clicked "Resend text message" on the phone verification page.', 'info');
@@ -850,10 +756,10 @@
}
await addLog(
`Step 9: still no SMS for ${normalizedActivation.phoneNumber} 60 seconds after resend, restarting from step 7 with a new number.`,
`Step 9: still no SMS for ${currentActivation.phoneNumber} 60 seconds after resend, restarting from step 7 with a new number.`,
'warn'
);
throw buildPhoneRestartStep7Error(normalizedActivation.phoneNumber);
throw buildPhoneRestartStep7Error(currentActivation.phoneNumber);
}
}
@@ -965,8 +871,17 @@
continue;
}
await completePhoneActivation(state, activation);
await markActivationReusableAfterSuccess(activation);
activation = await incrementCurrentActivationUseCount(activation);
try {
await completePhoneActivation(state, activation);
await syncReusableActivationAfterUse(activation);
} catch (activationStatusError) {
await clearReusableActivation();
await addLog(
`Step 9: phone verification succeeded, but HeroSMS setStatus(3) failed. The next flow will request a new number. ${activationStatusError.message}`,
'warn'
);
}
shouldCancelActivation = false;
await clearCurrentActivation();
await addLog('Step 9: phone verification finished, waiting for OAuth consent.', 'ok');