Merge pull request #206 from initiatione/codex/free-phone-reuse-upstream
增加 HeroSMS 免费手机号复用
This commit is contained in:
@@ -634,6 +634,8 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
autoRunDelayMinutes: 30,
|
autoRunDelayMinutes: 30,
|
||||||
autoStepDelaySeconds: null,
|
autoStepDelaySeconds: null,
|
||||||
phoneVerificationEnabled: false,
|
phoneVerificationEnabled: false,
|
||||||
|
freePhoneReuseEnabled: true,
|
||||||
|
freePhoneReuseAutoEnabled: true,
|
||||||
signupMethod: DEFAULT_SIGNUP_METHOD,
|
signupMethod: DEFAULT_SIGNUP_METHOD,
|
||||||
phoneSmsProvider: DEFAULT_PHONE_SMS_PROVIDER,
|
phoneSmsProvider: DEFAULT_PHONE_SMS_PROVIDER,
|
||||||
phoneSmsProviderOrder: [],
|
phoneSmsProviderOrder: [],
|
||||||
@@ -800,6 +802,7 @@ const DEFAULT_STATE = {
|
|||||||
currentPhoneVerificationCountdownWindowIndex: 0,
|
currentPhoneVerificationCountdownWindowIndex: 0,
|
||||||
currentPhoneVerificationCountdownWindowTotal: 0,
|
currentPhoneVerificationCountdownWindowTotal: 0,
|
||||||
reusablePhoneActivation: null,
|
reusablePhoneActivation: null,
|
||||||
|
freeReusablePhoneActivation: null,
|
||||||
phoneReusableActivationPool: [],
|
phoneReusableActivationPool: [],
|
||||||
signupPhoneNumber: '',
|
signupPhoneNumber: '',
|
||||||
signupPhoneActivation: null,
|
signupPhoneActivation: null,
|
||||||
@@ -2302,6 +2305,8 @@ function normalizePersistentSettingValue(key, value) {
|
|||||||
case 'gopayHelperLocalSmsHelperEnabled':
|
case 'gopayHelperLocalSmsHelperEnabled':
|
||||||
case 'autoRunDelayEnabled':
|
case 'autoRunDelayEnabled':
|
||||||
case 'phoneVerificationEnabled':
|
case 'phoneVerificationEnabled':
|
||||||
|
case 'freePhoneReuseEnabled':
|
||||||
|
case 'freePhoneReuseAutoEnabled':
|
||||||
case 'plusModeEnabled':
|
case 'plusModeEnabled':
|
||||||
return Boolean(value);
|
return Boolean(value);
|
||||||
case 'phoneSmsProvider':
|
case 'phoneSmsProvider':
|
||||||
@@ -3109,6 +3114,7 @@ async function resetState() {
|
|||||||
'tabRegistry',
|
'tabRegistry',
|
||||||
'sourceLastUrls',
|
'sourceLastUrls',
|
||||||
'reusablePhoneActivation',
|
'reusablePhoneActivation',
|
||||||
|
'freeReusablePhoneActivation',
|
||||||
'phoneReusableActivationPool',
|
'phoneReusableActivationPool',
|
||||||
'luckmailApiKey',
|
'luckmailApiKey',
|
||||||
'luckmailBaseUrl',
|
'luckmailBaseUrl',
|
||||||
@@ -3148,6 +3154,19 @@ async function resetState() {
|
|||||||
.map((entry) => normalizePhonePreferredActivation(entry))
|
.map((entry) => normalizePhonePreferredActivation(entry))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
|
const freeReusablePhoneActivation = (
|
||||||
|
prev.freeReusablePhoneActivation
|
||||||
|
&& typeof prev.freeReusablePhoneActivation === 'object'
|
||||||
|
&& !Array.isArray(prev.freeReusablePhoneActivation)
|
||||||
|
&& String(
|
||||||
|
prev.freeReusablePhoneActivation.phoneNumber
|
||||||
|
?? prev.freeReusablePhoneActivation.number
|
||||||
|
?? prev.freeReusablePhoneActivation.phone
|
||||||
|
?? ''
|
||||||
|
).trim()
|
||||||
|
)
|
||||||
|
? prev.freeReusablePhoneActivation
|
||||||
|
: null;
|
||||||
await chrome.storage.session.clear();
|
await chrome.storage.session.clear();
|
||||||
await chrome.storage.session.set({
|
await chrome.storage.session.set({
|
||||||
...DEFAULT_STATE,
|
...DEFAULT_STATE,
|
||||||
@@ -3170,6 +3189,8 @@ async function resetState() {
|
|||||||
currentLuckmailMailCursor: null,
|
currentLuckmailMailCursor: null,
|
||||||
// Keep reusable phone activation across round resets so the same number can be reactivated up to maxUses.
|
// Keep reusable phone activation across round resets so the same number can be reactivated up to maxUses.
|
||||||
reusablePhoneActivation,
|
reusablePhoneActivation,
|
||||||
|
// Keep free reuse phone activation until the user clears or the flow retires it.
|
||||||
|
freeReusablePhoneActivation,
|
||||||
phoneReusableActivationPool,
|
phoneReusableActivationPool,
|
||||||
preferredIcloudHost: prev.preferredIcloudHost || '',
|
preferredIcloudHost: prev.preferredIcloudHost || '',
|
||||||
});
|
});
|
||||||
@@ -6629,6 +6650,53 @@ async function finalizePhoneActivationAfterSuccessfulFlow(state) {
|
|||||||
return phoneVerificationHelpers.finalizePendingPhoneActivationConfirmation(state);
|
return phoneVerificationHelpers.finalizePendingPhoneActivationConfirmation(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function clearFreeReusablePhoneActivation() {
|
||||||
|
await setState({ freeReusablePhoneActivation: null });
|
||||||
|
broadcastDataUpdate({ freeReusablePhoneActivation: null });
|
||||||
|
await addLog('已清除白嫖复用手机号记录。', 'ok');
|
||||||
|
return { ok: true, freeReusablePhoneActivation: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setFreeReusablePhoneActivation(record = {}) {
|
||||||
|
const phoneNumber = String(record.phoneNumber || record.number || record.phone || '').trim();
|
||||||
|
if (!phoneNumber) {
|
||||||
|
throw new Error('请先填写白嫖复用手机号。');
|
||||||
|
}
|
||||||
|
const state = await getState();
|
||||||
|
const activationId = String(record.activationId || record.id || record.activation || '').trim();
|
||||||
|
const countryId = Math.max(1, Math.floor(Number(record.countryId) || Number(state.heroSmsCountryId) || HERO_SMS_COUNTRY_ID));
|
||||||
|
const stateCountryLabel = Math.floor(Number(state.heroSmsCountryId) || 0) === countryId
|
||||||
|
? String(state.heroSmsCountryLabel || '').trim()
|
||||||
|
: '';
|
||||||
|
const countryLabel = String(
|
||||||
|
record.countryLabel
|
||||||
|
|| stateCountryLabel
|
||||||
|
|| (countryId === HERO_SMS_COUNTRY_ID ? HERO_SMS_COUNTRY_LABEL : `Country #${countryId}`)
|
||||||
|
).trim();
|
||||||
|
const activation = {
|
||||||
|
...(activationId ? { activationId } : {}),
|
||||||
|
phoneNumber,
|
||||||
|
provider: PHONE_SMS_PROVIDER_HERO,
|
||||||
|
serviceCode: HERO_SMS_SERVICE_CODE,
|
||||||
|
countryId,
|
||||||
|
...(countryLabel ? { countryLabel } : {}),
|
||||||
|
successfulUses: Math.max(0, Math.floor(Number(record.successfulUses) || 0)),
|
||||||
|
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || 3)),
|
||||||
|
source: 'free-manual-reuse',
|
||||||
|
recordedAt: Date.now(),
|
||||||
|
manualOnly: !activationId,
|
||||||
|
};
|
||||||
|
await setState({ freeReusablePhoneActivation: activation });
|
||||||
|
broadcastDataUpdate({ freeReusablePhoneActivation: activation });
|
||||||
|
await addLog(
|
||||||
|
activationId
|
||||||
|
? `已手动记录白嫖复用手机号 ${phoneNumber}(#${activationId})。`
|
||||||
|
: `已手动记录白嫖复用手机号 ${phoneNumber}。未填写 HeroSMS 激活 ID,仅支持手动填号复用。`,
|
||||||
|
'ok'
|
||||||
|
);
|
||||||
|
return { ok: true, freeReusablePhoneActivation: activation };
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Tab Registry
|
// Tab Registry
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -10610,6 +10678,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
|||||||
clearAccountRunHistory: (...args) => clearAndBroadcastAccountRunHistory(...args),
|
clearAccountRunHistory: (...args) => clearAndBroadcastAccountRunHistory(...args),
|
||||||
deleteAccountRunHistoryRecords: (...args) => deleteAndBroadcastAccountRunHistoryRecords(...args),
|
deleteAccountRunHistoryRecords: (...args) => deleteAndBroadcastAccountRunHistoryRecords(...args),
|
||||||
clearAutoRunTimerAlarm,
|
clearAutoRunTimerAlarm,
|
||||||
|
clearFreeReusablePhoneActivation,
|
||||||
clearLuckmailRuntimeState,
|
clearLuckmailRuntimeState,
|
||||||
clearStopRequest,
|
clearStopRequest,
|
||||||
closeLocalhostCallbackTabs,
|
closeLocalhostCallbackTabs,
|
||||||
@@ -10698,6 +10767,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
|||||||
setContributionMode,
|
setContributionMode,
|
||||||
setEmailState,
|
setEmailState,
|
||||||
setEmailStateSilently,
|
setEmailStateSilently,
|
||||||
|
setFreeReusablePhoneActivation,
|
||||||
setSignupPhoneState,
|
setSignupPhoneState,
|
||||||
setSignupPhoneStateSilently,
|
setSignupPhoneStateSilently,
|
||||||
setIcloudAliasPreservedState,
|
setIcloudAliasPreservedState,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
clearAccountRunHistory,
|
clearAccountRunHistory,
|
||||||
deleteAccountRunHistoryRecords,
|
deleteAccountRunHistoryRecords,
|
||||||
clearAutoRunTimerAlarm,
|
clearAutoRunTimerAlarm,
|
||||||
|
clearFreeReusablePhoneActivation,
|
||||||
clearLuckmailRuntimeState,
|
clearLuckmailRuntimeState,
|
||||||
clearStopRequest,
|
clearStopRequest,
|
||||||
closeLocalhostCallbackTabs,
|
closeLocalhostCallbackTabs,
|
||||||
@@ -103,6 +104,7 @@
|
|||||||
setContributionMode,
|
setContributionMode,
|
||||||
setEmailState,
|
setEmailState,
|
||||||
setEmailStateSilently,
|
setEmailStateSilently,
|
||||||
|
setFreeReusablePhoneActivation,
|
||||||
setSignupPhoneState,
|
setSignupPhoneState,
|
||||||
setSignupPhoneStateSilently,
|
setSignupPhoneStateSilently,
|
||||||
setIcloudAliasPreservedState,
|
setIcloudAliasPreservedState,
|
||||||
@@ -679,6 +681,20 @@
|
|||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'CLEAR_FREE_REUSABLE_PHONE': {
|
||||||
|
if (typeof clearFreeReusablePhoneActivation !== 'function') {
|
||||||
|
throw new Error('白嫖复用手机号清除能力未接入。');
|
||||||
|
}
|
||||||
|
return await clearFreeReusablePhoneActivation();
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_FREE_REUSABLE_PHONE': {
|
||||||
|
if (typeof setFreeReusablePhoneActivation !== 'function') {
|
||||||
|
throw new Error('白嫖复用手机号记录能力未接入。');
|
||||||
|
}
|
||||||
|
return await setFreeReusablePhoneActivation(message.payload || {});
|
||||||
|
}
|
||||||
|
|
||||||
case 'SET_CONTRIBUTION_MODE': {
|
case 'SET_CONTRIBUTION_MODE': {
|
||||||
const enabled = Boolean(message.payload?.enabled);
|
const enabled = Boolean(message.payload?.enabled);
|
||||||
const state = await ensureManualInteractionAllowed(enabled ? '进入贡献模式' : '退出贡献模式');
|
const state = await ensureManualInteractionAllowed(enabled ? '进入贡献模式' : '退出贡献模式');
|
||||||
@@ -986,6 +1002,9 @@
|
|||||||
if (Boolean(currentState?.contributionMode) && typeof setContributionMode === 'function') {
|
if (Boolean(currentState?.contributionMode) && typeof setContributionMode === 'function') {
|
||||||
await setContributionMode(true);
|
await setContributionMode(true);
|
||||||
}
|
}
|
||||||
|
if (Object.keys(stateUpdates).length > 0 && typeof broadcastDataUpdate === 'function') {
|
||||||
|
broadcastDataUpdate(stateUpdates);
|
||||||
|
}
|
||||||
if (modeChanged) {
|
if (modeChanged) {
|
||||||
const selectedPlusPaymentMethod = getPlusPaymentMethodLabel(
|
const selectedPlusPaymentMethod = getPlusPaymentMethodLabel(
|
||||||
stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal'
|
stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal'
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
generateRandomName,
|
generateRandomName,
|
||||||
getOAuthFlowStepTimeoutMs,
|
getOAuthFlowStepTimeoutMs,
|
||||||
getState,
|
getState,
|
||||||
|
requestStop = null,
|
||||||
sendToContentScript,
|
sendToContentScript,
|
||||||
sendToContentScriptResilient,
|
sendToContentScriptResilient,
|
||||||
setState,
|
setState,
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
const PHONE_VERIFICATION_CODE_STATE_KEY = 'currentPhoneVerificationCode';
|
const PHONE_VERIFICATION_CODE_STATE_KEY = 'currentPhoneVerificationCode';
|
||||||
const REUSABLE_PHONE_ACTIVATION_STATE_KEY = 'reusablePhoneActivation';
|
const REUSABLE_PHONE_ACTIVATION_STATE_KEY = 'reusablePhoneActivation';
|
||||||
const REUSABLE_PHONE_ACTIVATION_POOL_STATE_KEY = 'phoneReusableActivationPool';
|
const REUSABLE_PHONE_ACTIVATION_POOL_STATE_KEY = 'phoneReusableActivationPool';
|
||||||
|
const FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY = 'freeReusablePhoneActivation';
|
||||||
const PREFERRED_PHONE_ACTIVATION_STATE_KEY = 'phonePreferredActivation';
|
const PREFERRED_PHONE_ACTIVATION_STATE_KEY = 'phonePreferredActivation';
|
||||||
const PHONE_RUNTIME_COUNTDOWN_ENDS_AT_KEY = 'currentPhoneVerificationCountdownEndsAt';
|
const PHONE_RUNTIME_COUNTDOWN_ENDS_AT_KEY = 'currentPhoneVerificationCountdownEndsAt';
|
||||||
const PHONE_RUNTIME_COUNTDOWN_WINDOW_INDEX_KEY = 'currentPhoneVerificationCountdownWindowIndex';
|
const PHONE_RUNTIME_COUNTDOWN_WINDOW_INDEX_KEY = 'currentPhoneVerificationCountdownWindowIndex';
|
||||||
@@ -89,7 +91,13 @@
|
|||||||
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
|
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
|
||||||
const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::';
|
const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::';
|
||||||
const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::';
|
const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::';
|
||||||
|
const PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX = 'PHONE_RESEND_BANNED_NUMBER::';
|
||||||
const PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX = 'PHONE_ROUTE_405_RECOVERY_FAILED::';
|
const PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX = 'PHONE_ROUTE_405_RECOVERY_FAILED::';
|
||||||
|
const PHONE_MANUAL_FREE_REUSE_ERROR_PREFIX = 'PHONE_MANUAL_FREE_REUSE::';
|
||||||
|
const PHONE_AUTO_FREE_REUSE_PREPARE_ERROR_PREFIX = 'PHONE_AUTO_FREE_REUSE_PREPARE::';
|
||||||
|
const FREE_PHONE_REUSE_PREPARE_TIMEOUT_MS = 20000;
|
||||||
|
const FREE_PHONE_REUSE_PREPARE_INTERVAL_MS = 2000;
|
||||||
|
const FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS = 10;
|
||||||
const PHONE_SMS_FAILURE_SKIP_THRESHOLD = 2;
|
const PHONE_SMS_FAILURE_SKIP_THRESHOLD = 2;
|
||||||
const MAX_ACTIVATION_PRICE_HINTS = 256;
|
const MAX_ACTIVATION_PRICE_HINTS = 256;
|
||||||
const activationPriceHintsByKey = new Map();
|
const activationPriceHintsByKey = new Map();
|
||||||
@@ -292,6 +300,24 @@
|
|||||||
return Math.max(0, Math.floor(Number(value) || 0));
|
return Math.max(0, Math.floor(Number(value) || 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePhoneDigits(value) {
|
||||||
|
return String(value || '').replace(/\D+/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function phoneNumbersMatch(left, right) {
|
||||||
|
const leftDigits = normalizePhoneDigits(left);
|
||||||
|
const rightDigits = normalizePhoneDigits(right);
|
||||||
|
return Boolean(
|
||||||
|
leftDigits
|
||||||
|
&& rightDigits
|
||||||
|
&& (
|
||||||
|
leftDigits === rightDigits
|
||||||
|
|| leftDigits.endsWith(rightDigits)
|
||||||
|
|| rightDigits.endsWith(leftDigits)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeTimestampMs(value) {
|
function normalizeTimestampMs(value) {
|
||||||
const numeric = Number(value);
|
const numeric = Number(value);
|
||||||
if (Number.isFinite(numeric) && numeric > 0) {
|
if (Number.isFinite(numeric) && numeric > 0) {
|
||||||
@@ -435,6 +461,15 @@
|
|||||||
return Boolean(value);
|
return Boolean(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeFreePhoneReuseEnabled(value) {
|
||||||
|
return Boolean(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFreePhoneReuseAutoEnabled(state = {}) {
|
||||||
|
return normalizeFreePhoneReuseEnabled(state?.freePhoneReuseEnabled)
|
||||||
|
&& Boolean(state?.freePhoneReuseAutoEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeHeroSmsAcquirePriority(value = '') {
|
function normalizeHeroSmsAcquirePriority(value = '') {
|
||||||
const normalized = String(value || '').trim().toLowerCase();
|
const normalized = String(value || '').trim().toLowerCase();
|
||||||
if (normalized === HERO_SMS_ACQUIRE_PRIORITY_PRICE) {
|
if (normalized === HERO_SMS_ACQUIRE_PRIORITY_PRICE) {
|
||||||
@@ -983,6 +1018,66 @@
|
|||||||
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)),
|
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)),
|
||||||
...(expiresAt > 0 ? { expiresAt } : {}),
|
...(expiresAt > 0 ? { expiresAt } : {}),
|
||||||
...(statusAction ? { statusAction } : {}),
|
...(statusAction ? { statusAction } : {}),
|
||||||
|
...(record.source ? { source: String(record.source || '').trim() } : {}),
|
||||||
|
...(record.phoneCodeReceived ? { phoneCodeReceived: true } : {}),
|
||||||
|
...(record.phoneCodeReceivedAt ? { phoneCodeReceivedAt: Math.max(0, Number(record.phoneCodeReceivedAt) || 0) } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeManualFreeReusablePhoneActivation(record) {
|
||||||
|
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const phoneNumber = String(
|
||||||
|
record.phoneNumber ?? record.number ?? record.phone ?? ''
|
||||||
|
).trim();
|
||||||
|
if (!phoneNumber) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const activationId = String(
|
||||||
|
record.activationId ?? record.id ?? record.activation ?? ''
|
||||||
|
).trim();
|
||||||
|
const countryLabel = String(record.countryLabel || '').trim();
|
||||||
|
const statusAction = String(record.statusAction || '').trim();
|
||||||
|
return {
|
||||||
|
...(activationId ? { activationId } : {}),
|
||||||
|
phoneNumber,
|
||||||
|
provider: PHONE_SMS_PROVIDER_HERO,
|
||||||
|
serviceCode: String(record.serviceCode || HERO_SMS_SERVICE_CODE).trim() || HERO_SMS_SERVICE_CODE,
|
||||||
|
countryId: normalizeCountryId(record.countryId, HERO_SMS_COUNTRY_ID),
|
||||||
|
...(countryLabel ? { countryLabel } : {}),
|
||||||
|
successfulUses: normalizeUseCount(record.successfulUses),
|
||||||
|
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)),
|
||||||
|
...(statusAction ? { statusAction } : {}),
|
||||||
|
source: 'free-manual-reuse',
|
||||||
|
recordedAt: Math.max(0, Number(record.recordedAt) || Date.now()),
|
||||||
|
manualOnly: !activationId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFreeReusablePhoneActivation(record) {
|
||||||
|
const normalized = normalizeActivation(record) || normalizeManualFreeReusablePhoneActivation(record);
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const recordedAt = Math.max(0, Number(record?.recordedAt) || 0);
|
||||||
|
return {
|
||||||
|
...normalized,
|
||||||
|
provider: PHONE_SMS_PROVIDER_HERO,
|
||||||
|
source: 'free-manual-reuse',
|
||||||
|
...(recordedAt ? { recordedAt } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function markActivationPhoneCodeReceived(activation) {
|
||||||
|
const normalizedActivation = normalizeActivation(activation);
|
||||||
|
if (!normalizedActivation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...normalizedActivation,
|
||||||
|
phoneCodeReceived: true,
|
||||||
|
phoneCodeReceivedAt: normalizedActivation.phoneCodeReceivedAt || Date.now(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1067,6 +1162,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function persistFreeReusableActivation(activation) {
|
||||||
|
await setPhoneRuntimeState({
|
||||||
|
[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY]: normalizeFreeReusablePhoneActivation(activation),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearFreeReusableActivation() {
|
||||||
|
await setPhoneRuntimeState({
|
||||||
|
[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY]: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeActivationFallback(record) {
|
function normalizeActivationFallback(record) {
|
||||||
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -1183,6 +1290,34 @@
|
|||||||
return /tried\s+to\s+resend\s+too\s+many\s+times|please\s+try\s+again\s+later|too\s+many\s+resend|resend\s+too\s+many|发送.*过于频繁|稍后再试/i.test(message);
|
return /tried\s+to\s+resend\s+too\s+many\s+times|please\s+try\s+again\s+later|too\s+many\s+resend|resend\s+too\s+many|发送.*过于频繁|稍后再试/i.test(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPhoneResendBannedNumberError(error) {
|
||||||
|
const message = String(error?.message || error || '').trim();
|
||||||
|
if (!message) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (message.startsWith(PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return /无法向此电话号码发送短信|无法向此手机号发送短信|无法发送短信到此电话号码|无法发送短信到此手机号|can(?:not|'t)\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number|unable\s+to\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldTreatResendThrottledAsBanned(state = {}) {
|
||||||
|
return Boolean(state?.phoneResendThrottledAsBannedEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHighRiskResendThrottledError(message = '') {
|
||||||
|
return new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${message || 'OpenAI resend is throttled and configured as high-probability banned phone.'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPhoneMaxUsageExceededError(message = '') {
|
||||||
|
return new Error(`PHONE_MAX_USAGE_EXCEEDED::${message || 'OpenAI reported phone_max_usage_exceeded for this phone number.'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPhoneMaxUsageExceededFlowError(error) {
|
||||||
|
const message = String(error?.message || error || '').trim();
|
||||||
|
return message.startsWith('PHONE_MAX_USAGE_EXCEEDED::') || isPhoneNumberUsedError(message);
|
||||||
|
}
|
||||||
|
|
||||||
function isPhoneRoute405RecoveryError(error) {
|
function isPhoneRoute405RecoveryError(error) {
|
||||||
const message = String(error?.message || error || '').trim();
|
const message = String(error?.message || error || '').trim();
|
||||||
if (!message) {
|
if (!message) {
|
||||||
@@ -1505,6 +1640,19 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveHeroSmsPhoneConfig(state = {}) {
|
||||||
|
const apiKey = normalizeApiKey(state.heroSmsApiKey);
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error('HeroSMS API key is missing. Save it in the side panel before running the phone flow.');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
provider: PHONE_SMS_PROVIDER_HERO,
|
||||||
|
apiKey,
|
||||||
|
baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL),
|
||||||
|
countryCandidates: resolveCountryCandidates(state),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function parseActivationPayload(payload, fallback = null) {
|
function parseActivationPayload(payload, fallback = null) {
|
||||||
const normalizedFallback = normalizeActivation(fallback) || normalizeActivationFallback(fallback);
|
const normalizedFallback = normalizeActivation(fallback) || normalizeActivationFallback(fallback);
|
||||||
const directActivation = normalizeActivation(payload);
|
const directActivation = normalizeActivation(payload);
|
||||||
@@ -3220,16 +3368,28 @@
|
|||||||
if (!normalizedActivation) {
|
if (!normalizedActivation) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
const normalizedStatus = Math.floor(Number(status) || 0);
|
||||||
|
if (
|
||||||
|
(normalizedStatus === 6 || normalizedStatus === 8)
|
||||||
|
&& shouldSkipTerminalStatusForFreeReuse(state, normalizedActivation)
|
||||||
|
) {
|
||||||
|
const identifier = normalizedActivation.phoneNumber || normalizedActivation.activationId || 'current activation';
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的 setStatus(${normalizedStatus})。`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
return `free reuse setStatus(${normalizedStatus}) skipped`;
|
||||||
|
}
|
||||||
const config = resolvePhoneConfig(state);
|
const config = resolvePhoneConfig(state);
|
||||||
if (config.provider === PHONE_SMS_PROVIDER_5SIM) {
|
if (config.provider === PHONE_SMS_PROVIDER_5SIM) {
|
||||||
const endpoint = status === 6
|
const endpoint = normalizedStatus === 6
|
||||||
? `/user/finish/${normalizedActivation.activationId}`
|
? `/user/finish/${normalizedActivation.activationId}`
|
||||||
: `/user/cancel/${normalizedActivation.activationId}`;
|
: `/user/cancel/${normalizedActivation.activationId}`;
|
||||||
const payload = await fetchFiveSimPayload(config, endpoint, actionLabel || '5sim set status');
|
const payload = await fetchFiveSimPayload(config, endpoint, actionLabel || '5sim set status');
|
||||||
return describeFiveSimPayload(payload);
|
return describeFiveSimPayload(payload);
|
||||||
}
|
}
|
||||||
if (config.provider === PHONE_SMS_PROVIDER_NEXSMS) {
|
if (config.provider === PHONE_SMS_PROVIDER_NEXSMS) {
|
||||||
if (status === 6) {
|
if (normalizedStatus === 6) {
|
||||||
return 'NexSMS complete skipped';
|
return 'NexSMS complete skipped';
|
||||||
}
|
}
|
||||||
const payload = await fetchNexSmsPayload(
|
const payload = await fetchNexSmsPayload(
|
||||||
@@ -3251,12 +3411,21 @@
|
|||||||
const payload = await fetchHeroSmsPayload(config, {
|
const payload = await fetchHeroSmsPayload(config, {
|
||||||
action: 'setStatus',
|
action: 'setStatus',
|
||||||
id: normalizedActivation.activationId,
|
id: normalizedActivation.activationId,
|
||||||
status,
|
status: normalizedStatus,
|
||||||
}, actionLabel);
|
}, actionLabel);
|
||||||
return describeHeroSmsPayload(payload);
|
return describeHeroSmsPayload(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function completePhoneActivation(state = {}, activation) {
|
async function completePhoneActivation(state = {}, activation) {
|
||||||
|
if (shouldSkipTerminalStatusForFreeReuse(state, activation)) {
|
||||||
|
const normalizedActivation = normalizeActivation(activation);
|
||||||
|
const identifier = normalizedActivation?.phoneNumber || normalizedActivation?.activationId || 'current activation';
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的接码完成状态。`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
|
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
|
||||||
const provider = getFiveSimProviderForState(state);
|
const provider = getFiveSimProviderForState(state);
|
||||||
if (provider) {
|
if (provider) {
|
||||||
@@ -3269,6 +3438,15 @@
|
|||||||
|
|
||||||
async function cancelPhoneActivation(state = {}, activation) {
|
async function cancelPhoneActivation(state = {}, activation) {
|
||||||
try {
|
try {
|
||||||
|
const normalizedActivation = normalizeActivation(activation);
|
||||||
|
if (shouldSkipTerminalStatusForFreeReuse(state, activation)) {
|
||||||
|
const identifier = normalizedActivation?.phoneNumber || normalizedActivation?.activationId || 'current activation';
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的接码取消状态。`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
|
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
|
||||||
const provider = getFiveSimProviderForState(state);
|
const provider = getFiveSimProviderForState(state);
|
||||||
if (provider) {
|
if (provider) {
|
||||||
@@ -3282,8 +3460,62 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function retireFreeReusableActivation(reason = '') {
|
||||||
|
const suffix = reason ? ` ${reason}` : '';
|
||||||
|
await addLog(`步骤 9:已清除白嫖复用手机号记录。${suffix}`, 'warn');
|
||||||
|
await clearFreeReusableActivation();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function discardPhoneActivationFromReuse(reason = '', activation = null, state = {}) {
|
||||||
|
const rejectedPhoneNumber = String(activation?.phoneNumber || '').trim();
|
||||||
|
if (!rejectedPhoneNumber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const updates = {};
|
||||||
|
const currentActivation = normalizeActivation(state[PHONE_ACTIVATION_STATE_KEY]);
|
||||||
|
if (phoneNumbersMatch(currentActivation?.phoneNumber, rejectedPhoneNumber)) {
|
||||||
|
updates[PHONE_ACTIVATION_STATE_KEY] = null;
|
||||||
|
updates[PHONE_VERIFICATION_CODE_STATE_KEY] = '';
|
||||||
|
}
|
||||||
|
const reusableActivation = normalizeActivation(state[REUSABLE_PHONE_ACTIVATION_STATE_KEY]);
|
||||||
|
if (phoneNumbersMatch(reusableActivation?.phoneNumber, rejectedPhoneNumber)) {
|
||||||
|
updates[REUSABLE_PHONE_ACTIVATION_STATE_KEY] = null;
|
||||||
|
}
|
||||||
|
const reusablePool = readReusableActivationPoolFromState(state);
|
||||||
|
const nextReusablePool = reusablePool.filter((entry) => (
|
||||||
|
!phoneNumbersMatch(entry?.phoneNumber, rejectedPhoneNumber)
|
||||||
|
));
|
||||||
|
if (nextReusablePool.length !== reusablePool.length) {
|
||||||
|
updates[REUSABLE_PHONE_ACTIVATION_POOL_STATE_KEY] = nextReusablePool;
|
||||||
|
}
|
||||||
|
const freeReusableActivation = normalizeFreeReusablePhoneActivation(state[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY]);
|
||||||
|
if (phoneNumbersMatch(freeReusableActivation?.phoneNumber, rejectedPhoneNumber)) {
|
||||||
|
updates[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY] = null;
|
||||||
|
}
|
||||||
|
if (Object.keys(updates).length) {
|
||||||
|
await setPhoneRuntimeState(updates);
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:已从复用记录中移除手机号 ${rejectedPhoneNumber}。${reason || '目标站拒绝该号码。'}`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFreeAutoReuseActivation(activation) {
|
||||||
|
return normalizeActivation(activation)?.source === 'free-auto-reuse';
|
||||||
|
}
|
||||||
|
|
||||||
async function banPhoneActivation(state = {}, activation) {
|
async function banPhoneActivation(state = {}, activation) {
|
||||||
try {
|
try {
|
||||||
|
if (shouldSkipTerminalStatusForFreeReuse(state, activation)) {
|
||||||
|
const normalizedActivation = normalizeActivation(activation);
|
||||||
|
const identifier = normalizedActivation?.phoneNumber || normalizedActivation?.activationId || 'current activation';
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的接码封禁状态。`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
|
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
|
||||||
const provider = getFiveSimProviderForState(state);
|
const provider = getFiveSimProviderForState(state);
|
||||||
if (provider) {
|
if (provider) {
|
||||||
@@ -3313,6 +3545,134 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isHeroSmsWaitingStatusText(text) {
|
||||||
|
return /^STATUS_(WAIT_CODE|WAIT_RETRY|WAIT_RESEND)(?::.+)?$/i.test(String(text || '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHeroSmsReadyForFreshSmsText(text) {
|
||||||
|
return /^STATUS_WAIT_CODE(?::.+)?$/i.test(String(text || '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHeroSmsCancelledStatusText(text) {
|
||||||
|
return /^STATUS_CANCEL$/i.test(String(text || '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareFreeReusablePhoneActivation(state = {}, activation) {
|
||||||
|
const normalizedActivation = normalizeFreeReusablePhoneActivation(activation);
|
||||||
|
if (!normalizedActivation) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: 'missing_free_reusable_activation',
|
||||||
|
message: 'Free reusable phone activation is missing.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!String(normalizedActivation.activationId || '').trim()) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: 'missing_activation_id',
|
||||||
|
message: 'Saved free reusable phone has no HeroSMS activation ID; automatic free reuse cannot reactivate it.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusAction = resolveActivationStatusAction(normalizedActivation);
|
||||||
|
const config = resolveHeroSmsPhoneConfig(state);
|
||||||
|
const start = Date.now();
|
||||||
|
let lastStatus = '';
|
||||||
|
let prepareRound = 0;
|
||||||
|
|
||||||
|
while (
|
||||||
|
Date.now() - start < FREE_PHONE_REUSE_PREPARE_TIMEOUT_MS
|
||||||
|
&& prepareRound < FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS
|
||||||
|
) {
|
||||||
|
throwIfStopped();
|
||||||
|
prepareRound += 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await setPhoneActivationStatus(
|
||||||
|
{ ...state, phoneSmsProvider: PHONE_SMS_PROVIDER_HERO },
|
||||||
|
normalizedActivation,
|
||||||
|
3,
|
||||||
|
'HeroSMS setStatus(3) for automatic free reuse'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: 'set_status_failed',
|
||||||
|
message: error.message || 'HeroSMS setStatus(3) failed.',
|
||||||
|
lastStatus,
|
||||||
|
prepareRound,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:自动白嫖复用已刷新 ${normalizedActivation.phoneNumber},${Math.ceil(FREE_PHONE_REUSE_PREPARE_INTERVAL_MS / 1000)} 秒后检查等待状态(${prepareRound}/${FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS})。`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
await sleepWithStop(FREE_PHONE_REUSE_PREPARE_INTERVAL_MS);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await fetchHeroSmsPayload(config, {
|
||||||
|
action: statusAction,
|
||||||
|
id: normalizedActivation.activationId,
|
||||||
|
}, `HeroSMS ${statusAction} for automatic free reuse`);
|
||||||
|
const statusText = describeHeroSmsPayload(payload);
|
||||||
|
lastStatus = statusText;
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:自动白嫖复用号码 ${normalizedActivation.phoneNumber} 状态:${statusText || 'empty response'}(${prepareRound}/${FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS})。`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
|
||||||
|
const v2Waiting = statusAction === 'getStatusV2'
|
||||||
|
&& payload
|
||||||
|
&& typeof payload === 'object'
|
||||||
|
&& !Array.isArray(payload)
|
||||||
|
&& !payload.sms?.code
|
||||||
|
&& !payload.call?.code;
|
||||||
|
if (isHeroSmsReadyForFreshSmsText(statusText) || isHeroSmsWaitingStatusText(statusText) || v2Waiting) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
activation: {
|
||||||
|
...normalizedActivation,
|
||||||
|
source: 'free-auto-reuse',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (/^STATUS_OK:/i.test(statusText)) {
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:自动白嫖复用仍看到旧验证码,将再次刷新等待短信状态。`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isHeroSmsCancelledStatusText(statusText)) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: 'activation_cancelled',
|
||||||
|
message: 'HeroSMS activation was cancelled before automatic free reuse.',
|
||||||
|
lastStatus,
|
||||||
|
prepareRound,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: 'get_status_failed',
|
||||||
|
message: error.message || 'HeroSMS getStatus failed.',
|
||||||
|
lastStatus,
|
||||||
|
prepareRound,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: 'prepare_timeout',
|
||||||
|
message: `Timed out waiting for saved phone to enter SMS waiting state. Last status: ${lastStatus || 'unknown'}.`,
|
||||||
|
lastStatus,
|
||||||
|
prepareRound,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function pollPhoneActivationCode(state = {}, activation, options = {}) {
|
async function pollPhoneActivationCode(state = {}, activation, options = {}) {
|
||||||
const normalizedActivation = normalizeActivation(activation);
|
const normalizedActivation = normalizeActivation(activation);
|
||||||
if (!normalizedActivation) {
|
if (!normalizedActivation) {
|
||||||
@@ -3777,6 +4137,71 @@
|
|||||||
return result || {};
|
return result || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkPhoneResendPageError(tabId, state = {}) {
|
||||||
|
if (!usePageProbeForPhoneResend(state)) {
|
||||||
|
return {
|
||||||
|
hasError: false,
|
||||||
|
reason: '',
|
||||||
|
message: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9;
|
||||||
|
try {
|
||||||
|
const result = await sendToContentScriptResilient('signup-page', {
|
||||||
|
type: 'CHECK_PHONE_RESEND_ERROR',
|
||||||
|
source: 'background',
|
||||||
|
payload: { visibleStep },
|
||||||
|
}, {
|
||||||
|
timeoutMs: 3000,
|
||||||
|
responseTimeoutMs: 3000,
|
||||||
|
retryDelayMs: 500,
|
||||||
|
logStep: visibleStep,
|
||||||
|
logStepKey: 'phone-verification',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
return result || {};
|
||||||
|
} catch (error) {
|
||||||
|
if (isStopRequestedError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (isPhoneResendBannedNumberError(error)) {
|
||||||
|
return {
|
||||||
|
hasError: true,
|
||||||
|
reason: 'resend_phone_banned',
|
||||||
|
message: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isPhoneResendThrottledError(error)) {
|
||||||
|
return {
|
||||||
|
hasError: true,
|
||||||
|
reason: 'resend_throttled',
|
||||||
|
message: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isPhoneMaxUsageExceededFlowError(error)) {
|
||||||
|
return {
|
||||||
|
hasError: true,
|
||||||
|
reason: 'phone_max_usage_exceeded',
|
||||||
|
message: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await addLog(`步骤 9:检查手机重发错误时遇到暂时性问题,已忽略。${error.message}`, 'warn');
|
||||||
|
return {
|
||||||
|
hasError: false,
|
||||||
|
reason: '',
|
||||||
|
message: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePageProbeForPhoneResend(state = {}) {
|
||||||
|
const provider = normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER);
|
||||||
|
return provider === PHONE_SMS_PROVIDER_HERO || provider === PHONE_SMS_PROVIDER_NEXSMS || provider === PHONE_SMS_PROVIDER_5SIM;
|
||||||
|
}
|
||||||
|
|
||||||
async function persistCurrentActivation(activation) {
|
async function persistCurrentActivation(activation) {
|
||||||
const normalizedActivation = normalizeActivation(activation);
|
const normalizedActivation = normalizeActivation(activation);
|
||||||
const updates = {
|
const updates = {
|
||||||
@@ -3843,6 +4268,67 @@
|
|||||||
await persistReusableActivation(null);
|
await persistReusableActivation(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handoffFreeReusablePhone(tabId, state = {}) {
|
||||||
|
if (!normalizeFreePhoneReuseEnabled(state?.freePhoneReuseEnabled)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const freeReusableActivation = normalizeFreeReusablePhoneActivation(
|
||||||
|
state[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY]
|
||||||
|
);
|
||||||
|
if (!freeReusableActivation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (freeReusableActivation.successfulUses >= freeReusableActivation.maxUses) {
|
||||||
|
await retireFreeReusableActivation(
|
||||||
|
`保存的手机号 ${freeReusableActivation.phoneNumber} 已达到 ${freeReusableActivation.successfulUses}/${freeReusableActivation.maxUses} 次。`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizeFreePhoneReuseAutoEnabled(state)) {
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:准备自动白嫖复用已保存手机号 ${freeReusableActivation.phoneNumber}(${freeReusableActivation.successfulUses + 1}/${freeReusableActivation.maxUses})。`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
const prepared = await prepareFreeReusablePhoneActivation(state, freeReusableActivation);
|
||||||
|
if (!prepared.ok) {
|
||||||
|
const reason = prepared.message || prepared.reason || 'unknown error';
|
||||||
|
const stopMessage = `自动白嫖复用准备失败:${freeReusableActivation.phoneNumber} 未确认进入等待短信状态,本次不购买新 HeroSMS 号码。原因:${reason}`;
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:自动白嫖复用准备失败,停止本次接码且不购买新 HeroSMS 号码。${reason}`,
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
if (prepared.reason === 'activation_cancelled') {
|
||||||
|
await retireFreeReusableActivation(
|
||||||
|
`自动白嫖复用号码 ${freeReusableActivation.phoneNumber} 已被 HeroSMS 取消。`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeof requestStop === 'function') {
|
||||||
|
await requestStop({ logMessage: stopMessage });
|
||||||
|
}
|
||||||
|
throw new Error(`${PHONE_AUTO_FREE_REUSE_PREPARE_ERROR_PREFIX}${stopMessage}`);
|
||||||
|
}
|
||||||
|
await persistCurrentActivation(prepared.activation);
|
||||||
|
return prepared.activation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillResult = await submitPhoneNumber(tabId, freeReusableActivation.phoneNumber, freeReusableActivation);
|
||||||
|
await clearCurrentActivation();
|
||||||
|
const message = `开始手动复用手机 ${freeReusableActivation.phoneNumber},请到 SMS 上刷新。`;
|
||||||
|
await addLog(`步骤 9:${message}`, 'warn');
|
||||||
|
if (typeof requestStop === 'function') {
|
||||||
|
await requestStop({ logMessage: message });
|
||||||
|
}
|
||||||
|
const handoffError = new Error(`${PHONE_MANUAL_FREE_REUSE_ERROR_PREFIX}${message}`);
|
||||||
|
handoffError.result = {
|
||||||
|
manualFreePhoneReuse: true,
|
||||||
|
phoneNumber: freeReusableActivation.phoneNumber,
|
||||||
|
fillResult,
|
||||||
|
};
|
||||||
|
throw handoffError;
|
||||||
|
}
|
||||||
|
|
||||||
async function setPhoneRuntimeCountdown(activation, waitSeconds, windowIndex, windowTotal) {
|
async function setPhoneRuntimeCountdown(activation, waitSeconds, windowIndex, windowTotal) {
|
||||||
const normalizedActivation = normalizeActivation(activation);
|
const normalizedActivation = normalizeActivation(activation);
|
||||||
if (!normalizedActivation) {
|
if (!normalizedActivation) {
|
||||||
@@ -4144,6 +4630,8 @@
|
|||||||
...normalizedActivation,
|
...normalizedActivation,
|
||||||
successfulUses,
|
successfulUses,
|
||||||
};
|
};
|
||||||
|
delete nextReusableActivation.phoneCodeReceived;
|
||||||
|
delete nextReusableActivation.phoneCodeReceivedAt;
|
||||||
await upsertReusableActivationPool(nextReusableActivation, { state });
|
await upsertReusableActivationPool(nextReusableActivation, { state });
|
||||||
if (!normalizeHeroSmsReuseEnabled(state?.heroSmsReuseEnabled)) {
|
if (!normalizeHeroSmsReuseEnabled(state?.heroSmsReuseEnabled)) {
|
||||||
await clearReusableActivation();
|
await clearReusableActivation();
|
||||||
@@ -4158,6 +4646,119 @@
|
|||||||
await persistReusableActivation(nextReusableActivation);
|
await persistReusableActivation(nextReusableActivation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldPreserveActivationForFreeReuse(state, activation) {
|
||||||
|
if (!normalizeFreePhoneReuseEnabled(state?.freePhoneReuseEnabled)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const normalizedActivation = normalizeActivation(activation);
|
||||||
|
return Boolean(
|
||||||
|
normalizedActivation
|
||||||
|
&& normalizedActivation.provider === PHONE_SMS_PROVIDER_HERO
|
||||||
|
&& normalizedActivation.source === 'hero-sms-new'
|
||||||
|
&& normalizedActivation.phoneCodeReceived
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSkipTerminalStatusForFreeReuse(state, activation) {
|
||||||
|
const normalizedActivation = normalizeActivation(activation);
|
||||||
|
if (!normalizedActivation || normalizedActivation.provider !== PHONE_SMS_PROVIDER_HERO) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isFreeAutoReuseActivation(normalizedActivation)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (normalizedActivation.source === 'free-manual-reuse') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const savedFreeActivation = normalizeFreeReusablePhoneActivation(
|
||||||
|
state?.[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY]
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
savedFreeActivation
|
||||||
|
&& (
|
||||||
|
isSameActivation(savedFreeActivation, normalizedActivation)
|
||||||
|
|| phoneNumbersMatch(savedFreeActivation.phoneNumber, normalizedActivation.phoneNumber)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return shouldPreserveActivationForFreeReuse(state, normalizedActivation);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markFreeReusableActivationAfterCode(state, activation) {
|
||||||
|
const latestState = {
|
||||||
|
...(state || {}),
|
||||||
|
...(typeof getState === 'function' ? await getState() : {}),
|
||||||
|
};
|
||||||
|
if (!normalizeFreePhoneReuseEnabled(latestState?.freePhoneReuseEnabled)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (normalizeFreeReusablePhoneActivation(latestState[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalizedActivation = normalizeActivation(activation);
|
||||||
|
if (
|
||||||
|
!normalizedActivation
|
||||||
|
|| normalizedActivation.provider !== PHONE_SMS_PROVIDER_HERO
|
||||||
|
|| !normalizedActivation.phoneCodeReceived
|
||||||
|
|| isFreeAutoReuseActivation(normalizedActivation)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const countryConfig = resolveCountryConfigFromActivation(normalizedActivation, latestState);
|
||||||
|
await persistFreeReusableActivation({
|
||||||
|
...normalizedActivation,
|
||||||
|
source: 'free-manual-reuse',
|
||||||
|
countryId: countryConfig.id,
|
||||||
|
...(countryConfig.label ? { countryLabel: countryConfig.label } : {}),
|
||||||
|
recordedAt: Date.now(),
|
||||||
|
});
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:收到有效短信后已保存白嫖复用手机号 ${normalizedActivation.phoneNumber}。`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markFreeReusableActivationAfterAutoSuccess(state, activation) {
|
||||||
|
const normalizedActivation = normalizeFreeReusablePhoneActivation(activation);
|
||||||
|
if (!normalizedActivation || !isFreeAutoReuseActivation(activation)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestState = {
|
||||||
|
...(state || {}),
|
||||||
|
...(typeof getState === 'function' ? await getState() : {}),
|
||||||
|
};
|
||||||
|
const savedActivation = normalizeFreeReusablePhoneActivation(
|
||||||
|
latestState[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY]
|
||||||
|
);
|
||||||
|
if (!savedActivation || savedActivation.activationId !== normalizedActivation.activationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const successfulUses = savedActivation.successfulUses + 1;
|
||||||
|
const maxUses = Math.max(1, Math.floor(Number(savedActivation.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES));
|
||||||
|
if (successfulUses >= maxUses) {
|
||||||
|
await clearFreeReusableActivation();
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:自动白嫖复用手机号 ${savedActivation.phoneNumber} 已达到 ${successfulUses}/${maxUses} 次,已清除本地记录。`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await persistFreeReusableActivation({
|
||||||
|
...savedActivation,
|
||||||
|
source: 'free-manual-reuse',
|
||||||
|
successfulUses,
|
||||||
|
maxUses,
|
||||||
|
});
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:自动白嫖复用手机号 ${savedActivation.phoneNumber} 成功(${successfulUses}/${maxUses}),保留供后续注册使用。`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForPhoneCodeOrRotateNumber(tabId, state, activation) {
|
async function waitForPhoneCodeOrRotateNumber(tabId, state, activation) {
|
||||||
const normalizedActivation = normalizeActivation(activation);
|
const normalizedActivation = normalizeActivation(activation);
|
||||||
if (!normalizedActivation) {
|
if (!normalizedActivation) {
|
||||||
@@ -4191,6 +4792,24 @@
|
|||||||
intervalMs: pollIntervalSeconds * 1000,
|
intervalMs: pollIntervalSeconds * 1000,
|
||||||
maxRounds: pollMaxRounds,
|
maxRounds: pollMaxRounds,
|
||||||
onStatus: async ({ elapsedMs, pollCount, statusText }) => {
|
onStatus: async ({ elapsedMs, pollCount, statusText }) => {
|
||||||
|
if (/^STATUS_(WAIT_CODE|WAIT_RETRY|WAIT_RESEND)(?::.+)?$/i.test(String(statusText || '').trim())) {
|
||||||
|
const pageError = await checkPhoneResendPageError(tabId, state);
|
||||||
|
if (pageError?.reason === 'resend_phone_banned') {
|
||||||
|
throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${pageError.message || 'OpenAI could not send SMS to this phone number.'}`);
|
||||||
|
}
|
||||||
|
if (pageError?.reason === 'phone_max_usage_exceeded') {
|
||||||
|
throw buildPhoneMaxUsageExceededError(pageError.message);
|
||||||
|
}
|
||||||
|
if (pageError?.reason === 'resend_throttled') {
|
||||||
|
if (shouldTreatResendThrottledAsBanned(state)) {
|
||||||
|
throw buildHighRiskResendThrottledError(pageError.message);
|
||||||
|
}
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:检测到号码 ${normalizedActivation.phoneNumber} 重发限流,但未启用“按疑似封禁处理”,继续等待短信。${pageError.message || ''}`.trim(),
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
const shouldLog = (
|
const shouldLog = (
|
||||||
pollCount === 1
|
pollCount === 1
|
||||||
|| statusText !== lastLoggedStatus
|
|| statusText !== lastLoggedStatus
|
||||||
@@ -4214,6 +4833,50 @@
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!isPhoneCodeTimeoutError(error)) {
|
if (!isPhoneCodeTimeoutError(error)) {
|
||||||
|
if (isPhoneResendBannedNumberError(error)) {
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:OpenAI 无法向号码 ${normalizedActivation.phoneNumber} 发送短信,立即更换号码。${error.message}`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
await clearPhoneRuntimeCountdown();
|
||||||
|
return {
|
||||||
|
code: '',
|
||||||
|
replaceNumber: true,
|
||||||
|
reason: 'resend_phone_banned',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isPhoneMaxUsageExceededFlowError(error)) {
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:OpenAI 提示号码 ${normalizedActivation.phoneNumber} 达到使用上限,立即更换号码。${error.message}`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
await clearPhoneRuntimeCountdown();
|
||||||
|
return {
|
||||||
|
code: '',
|
||||||
|
replaceNumber: true,
|
||||||
|
reason: 'phone_max_usage_exceeded',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isPhoneResendThrottledError(error)) {
|
||||||
|
if (shouldTreatResendThrottledAsBanned(state)) {
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:号码 ${normalizedActivation.phoneNumber} 重发限流且配置为高风险封禁,立即更换号码。${error.message}`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
await clearPhoneRuntimeCountdown();
|
||||||
|
return {
|
||||||
|
code: '',
|
||||||
|
replaceNumber: true,
|
||||||
|
reason: 'resend_throttled_high_risk_banned',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:号码 ${normalizedActivation.phoneNumber} 重发限流,但未启用高风险换号,继续原等待逻辑。${error.message}`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
await sleepWithStop(pollIntervalSeconds * 1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (isPhoneActivationOrderMissingError(error, normalizedActivation.provider)) {
|
if (isPhoneActivationOrderMissingError(error, normalizedActivation.provider)) {
|
||||||
await addLog(
|
await addLog(
|
||||||
`Step 9: ${providerLabel} activation for ${normalizedActivation.phoneNumber} became invalid (${error.message || error}), replacing number immediately.`,
|
`Step 9: ${providerLabel} activation for ${normalizedActivation.phoneNumber} became invalid (${error.message || error}), replacing number immediately.`,
|
||||||
@@ -4257,6 +4920,18 @@
|
|||||||
if (isStopRequestedError(resendError)) {
|
if (isStopRequestedError(resendError)) {
|
||||||
throw resendError;
|
throw resendError;
|
||||||
}
|
}
|
||||||
|
if (isPhoneResendBannedNumberError(resendError)) {
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:OpenAI 无法向号码 ${normalizedActivation.phoneNumber} 发送短信,立即更换号码。${resendError.message}`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
await clearPhoneRuntimeCountdown();
|
||||||
|
return {
|
||||||
|
code: '',
|
||||||
|
replaceNumber: true,
|
||||||
|
reason: 'resend_phone_banned',
|
||||||
|
};
|
||||||
|
}
|
||||||
if (isPhoneResendThrottledError(resendError)) {
|
if (isPhoneResendThrottledError(resendError)) {
|
||||||
await addLog(
|
await addLog(
|
||||||
`步骤 9:号码 ${normalizedActivation.phoneNumber} 重发短信被限流,立即更换号码。${resendError.message}`,
|
`步骤 9:号码 ${normalizedActivation.phoneNumber} 重发短信被限流,立即更换号码。${resendError.message}`,
|
||||||
@@ -4266,7 +4941,9 @@
|
|||||||
return {
|
return {
|
||||||
code: '',
|
code: '',
|
||||||
replaceNumber: true,
|
replaceNumber: true,
|
||||||
reason: 'resend_throttled',
|
reason: shouldTreatResendThrottledAsBanned(state)
|
||||||
|
? 'resend_throttled_high_risk_banned'
|
||||||
|
: 'resend_throttled',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await addLog(`步骤 9:点击手机验证码页面重发按钮失败。${resendError.message}`, 'warn');
|
await addLog(`步骤 9:点击手机验证码页面重发按钮失败。${resendError.message}`, 'warn');
|
||||||
@@ -5101,13 +5778,18 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!activation) {
|
if (!activation) {
|
||||||
activation = await acquirePhoneActivation(state, {
|
activation = await handoffFreeReusablePhone(tabId, state);
|
||||||
blockedCountryIds: getBlockedCountryIds(),
|
if (activation) {
|
||||||
countryPriceFloorByCountryId: getCountryPriceFloorById(),
|
shouldCancelActivation = false;
|
||||||
skipPreferredActivation: preferredActivationExhausted,
|
} else {
|
||||||
});
|
activation = await acquirePhoneActivation(state, {
|
||||||
shouldCancelActivation = true;
|
blockedCountryIds: getBlockedCountryIds(),
|
||||||
await persistCurrentActivation(activation);
|
countryPriceFloorByCountryId: getCountryPriceFloorById(),
|
||||||
|
skipPreferredActivation: preferredActivationExhausted,
|
||||||
|
});
|
||||||
|
shouldCancelActivation = true;
|
||||||
|
await persistCurrentActivation(activation);
|
||||||
|
}
|
||||||
addPhoneReentryWithSameActivation = 0;
|
addPhoneReentryWithSameActivation = 0;
|
||||||
} else if (preferReuseExistingActivationOnAddPhone) {
|
} else if (preferReuseExistingActivationOnAddPhone) {
|
||||||
addPhoneReentryWithSameActivation += 1;
|
addPhoneReentryWithSameActivation += 1;
|
||||||
@@ -5120,6 +5802,11 @@
|
|||||||
`步骤 9:当前号码 ${activation.phoneNumber} 反复返回添加手机号页,正在更换号码(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`,
|
`步骤 9:当前号码 ${activation.phoneNumber} 反复返回添加手机号页,正在更换号码(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`,
|
||||||
'warn'
|
'warn'
|
||||||
);
|
);
|
||||||
|
if (isFreeAutoReuseActivation(activation)) {
|
||||||
|
await retireFreeReusableActivation(
|
||||||
|
`自动白嫖复用号码 ${activation.phoneNumber} 反复返回添加手机号页。`
|
||||||
|
);
|
||||||
|
}
|
||||||
if (shouldCancelActivation && activation) {
|
if (shouldCancelActivation && activation) {
|
||||||
await cancelPhoneActivation(state, activation);
|
await cancelPhoneActivation(state, activation);
|
||||||
}
|
}
|
||||||
@@ -5170,6 +5857,16 @@
|
|||||||
`步骤 9:添加手机号页面提示 ${activation.phoneNumber} 已被使用(${addPhoneRejectText}),正在更换号码(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`,
|
`步骤 9:添加手机号页面提示 ${activation.phoneNumber} 已被使用(${addPhoneRejectText}),正在更换号码(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`,
|
||||||
'warn'
|
'warn'
|
||||||
);
|
);
|
||||||
|
await discardPhoneActivationFromReuse(
|
||||||
|
`目标站拒绝该号码(${addPhoneRejectText})。`,
|
||||||
|
activation,
|
||||||
|
await getState()
|
||||||
|
);
|
||||||
|
if (isFreeAutoReuseActivation(activation)) {
|
||||||
|
await retireFreeReusableActivation(
|
||||||
|
`自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。`
|
||||||
|
);
|
||||||
|
}
|
||||||
if (shouldCancelActivation && activation) {
|
if (shouldCancelActivation && activation) {
|
||||||
await banPhoneActivation(state, activation);
|
await banPhoneActivation(state, activation);
|
||||||
}
|
}
|
||||||
@@ -5258,6 +5955,12 @@
|
|||||||
await setPhoneRuntimeState({
|
await setPhoneRuntimeState({
|
||||||
[PHONE_VERIFICATION_CODE_STATE_KEY]: String(codeResult.code || '').trim(),
|
[PHONE_VERIFICATION_CODE_STATE_KEY]: String(codeResult.code || '').trim(),
|
||||||
});
|
});
|
||||||
|
activation = markActivationPhoneCodeReceived(activation) || activation;
|
||||||
|
await persistCurrentActivation(activation);
|
||||||
|
await setPhoneRuntimeState({
|
||||||
|
[PHONE_VERIFICATION_CODE_STATE_KEY]: String(codeResult.code || '').trim(),
|
||||||
|
});
|
||||||
|
await markFreeReusableActivationAfterCode(state, activation);
|
||||||
await addLog(`步骤 9:已收到手机验证码 ${codeResult.code}。`, 'info');
|
await addLog(`步骤 9:已收到手机验证码 ${codeResult.code}。`, 'info');
|
||||||
const submitResult = await submitPhoneVerificationCode(tabId, codeResult.code);
|
const submitResult = await submitPhoneVerificationCode(tabId, codeResult.code);
|
||||||
|
|
||||||
@@ -5281,6 +5984,16 @@
|
|||||||
if (isPhoneNumberUsedError(invalidErrorText)) {
|
if (isPhoneNumberUsedError(invalidErrorText)) {
|
||||||
shouldReplaceNumber = true;
|
shouldReplaceNumber = true;
|
||||||
replaceReason = 'phone_number_used';
|
replaceReason = 'phone_number_used';
|
||||||
|
await discardPhoneActivationFromReuse(
|
||||||
|
`目标站拒绝该号码(${invalidErrorText})。`,
|
||||||
|
activation,
|
||||||
|
await getState()
|
||||||
|
);
|
||||||
|
if (isFreeAutoReuseActivation(activation)) {
|
||||||
|
await retireFreeReusableActivation(
|
||||||
|
`自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。`
|
||||||
|
);
|
||||||
|
}
|
||||||
if (shouldCancelActivation && activation) {
|
if (shouldCancelActivation && activation) {
|
||||||
await banPhoneActivation(state, activation);
|
await banPhoneActivation(state, activation);
|
||||||
shouldCancelActivation = false;
|
shouldCancelActivation = false;
|
||||||
@@ -5331,8 +6044,19 @@
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await completePhoneActivation(state, activation);
|
const latestSuccessState = await getState();
|
||||||
await markActivationReusableAfterSuccess(state, activation);
|
if (shouldSkipTerminalStatusForFreeReuse(latestSuccessState, activation)) {
|
||||||
|
await addLog(
|
||||||
|
`步骤 9:已跳过 HeroSMS 完成状态,保留 ${activation.phoneNumber} 供白嫖复用。`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await completePhoneActivation(latestSuccessState, activation);
|
||||||
|
}
|
||||||
|
await markFreeReusableActivationAfterAutoSuccess(state, activation);
|
||||||
|
if (!isFreeAutoReuseActivation(activation)) {
|
||||||
|
await markActivationReusableAfterSuccess(state, activation);
|
||||||
|
}
|
||||||
clearCountrySmsFailure(activation.countryId, activation.provider);
|
clearCountrySmsFailure(activation.countryId, activation.provider);
|
||||||
shouldCancelActivation = false;
|
shouldCancelActivation = false;
|
||||||
await clearCurrentActivation();
|
await clearCurrentActivation();
|
||||||
@@ -5372,6 +6096,18 @@
|
|||||||
if (shouldCancelActivation && activation) {
|
if (shouldCancelActivation && activation) {
|
||||||
await cancelPhoneActivation(state, activation);
|
await cancelPhoneActivation(state, activation);
|
||||||
}
|
}
|
||||||
|
if (isFreeAutoReuseActivation(activation)) {
|
||||||
|
await retireFreeReusableActivation(
|
||||||
|
`自动白嫖复用号码 ${activation.phoneNumber} 在失败后被更换。`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isPhoneNumberUsedError(replaceReason)) {
|
||||||
|
await discardPhoneActivationFromReuse(
|
||||||
|
`目标站拒绝该号码(${replaceReason})。`,
|
||||||
|
activation,
|
||||||
|
await getState()
|
||||||
|
);
|
||||||
|
}
|
||||||
await clearCurrentActivation();
|
await clearCurrentActivation();
|
||||||
activation = null;
|
activation = null;
|
||||||
shouldCancelActivation = false;
|
shouldCancelActivation = false;
|
||||||
@@ -5430,8 +6166,20 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMessage = String(error?.message || error || '');
|
||||||
|
if (
|
||||||
|
errorMessage.startsWith(PHONE_MANUAL_FREE_REUSE_ERROR_PREFIX)
|
||||||
|
|| errorMessage.startsWith(PHONE_AUTO_FREE_REUSE_PREPARE_ERROR_PREFIX)
|
||||||
|
) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (isFreeAutoReuseActivation(activation)) {
|
||||||
|
await retireFreeReusableActivation(
|
||||||
|
`自动白嫖复用号码 ${activation.phoneNumber} 执行失败:${errorMessage || 'unknown error'}。`
|
||||||
|
);
|
||||||
|
}
|
||||||
if (shouldCancelActivation && activation) {
|
if (shouldCancelActivation && activation) {
|
||||||
await cancelPhoneActivation(state, activation);
|
await cancelPhoneActivation(await getState(), activation);
|
||||||
}
|
}
|
||||||
await clearCurrentActivation();
|
await clearCurrentActivation();
|
||||||
throw sanitizePhoneRestartStep7Error(sanitizePhoneCodeTimeoutError(error));
|
throw sanitizePhoneRestartStep7Error(sanitizePhoneCodeTimeoutError(error));
|
||||||
|
|||||||
@@ -19,11 +19,14 @@
|
|||||||
waitForElement,
|
waitForElement,
|
||||||
} = deps;
|
} = deps;
|
||||||
const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::';
|
const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::';
|
||||||
|
const PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX = 'PHONE_RESEND_BANNED_NUMBER::';
|
||||||
|
const PHONE_MAX_USAGE_EXCEEDED_PATTERN = /phone_max_usage_exceeded/i;
|
||||||
const PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX = 'PHONE_ROUTE_405_RECOVERY_FAILED::';
|
const PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX = 'PHONE_ROUTE_405_RECOVERY_FAILED::';
|
||||||
const PHONE_ROUTE_405_RECOVERY_COOLDOWN_MS = 6000;
|
const PHONE_ROUTE_405_RECOVERY_COOLDOWN_MS = 6000;
|
||||||
const PHONE_RESEND_ROUTE_405_MAX_RECOVERIES = 2;
|
const PHONE_RESEND_ROUTE_405_MAX_RECOVERIES = 2;
|
||||||
const PHONE_RESEND_ROUTE_405_MAX_RECOVERY_TOTAL_MS = 12000;
|
const PHONE_RESEND_ROUTE_405_MAX_RECOVERY_TOTAL_MS = 12000;
|
||||||
const PHONE_RESEND_THROTTLED_PATTERN = /tried\s+to\s+resend\s+too\s+many\s+times|please\s+try\s+again\s+later|too\s+many\s+resend|resend\s+too\s+many|发送.*过于频繁|稍后再试|重试次数过多/i;
|
const PHONE_RESEND_THROTTLED_PATTERN = /tried\s+to\s+resend\s+too\s+many\s+times|please\s+try\s+again\s+later|too\s+many\s+resend|resend\s+too\s+many|发送.*过于频繁|稍后再试|重试次数过多/i;
|
||||||
|
const PHONE_RESEND_BANNED_NUMBER_PATTERN = /无法向此电话号码发送短信|无法向此手机号发送短信|无法发送短信到此电话号码|无法发送短信到此手机号|can(?:not|'t)\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number|unable\s+to\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number/i;
|
||||||
const PHONE_ROUTE_405_PATTERN = /405\s+method\s+not\s+allowed|route\s+error.*405|did\s+not\s+provide\s+an?\s+[`'"]?action|post\s+request\s+to\s+["']?\/phone-verification/i;
|
const PHONE_ROUTE_405_PATTERN = /405\s+method\s+not\s+allowed|route\s+error.*405|did\s+not\s+provide\s+an?\s+[`'"]?action|post\s+request\s+to\s+["']?\/phone-verification/i;
|
||||||
const PHONE_ROUTE_405_MAX_RECOVERY_CLICKS = 3;
|
const PHONE_ROUTE_405_MAX_RECOVERY_CLICKS = 3;
|
||||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||||
@@ -493,6 +496,63 @@
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPhoneResendBannedNumberText() {
|
||||||
|
const inlineMatch = getPhoneVerificationInlineMessages()
|
||||||
|
.find((text) => PHONE_RESEND_BANNED_NUMBER_PATTERN.test(text));
|
||||||
|
if (inlineMatch) {
|
||||||
|
return inlineMatch;
|
||||||
|
}
|
||||||
|
const pageSnapshot = String(getPageTextSnapshot?.() || '').replace(/\s+/g, ' ').trim();
|
||||||
|
if (pageSnapshot && PHONE_RESEND_BANNED_NUMBER_PATTERN.test(pageSnapshot)) {
|
||||||
|
const concise = pageSnapshot.match(
|
||||||
|
/无法向此电话号码发送短信|无法向此手机号发送短信|无法发送短信到此电话号码|无法发送短信到此手机号|can(?:not|'t)\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number[^.。!?]*[.。!?]?|unable\s+to\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number[^.。!?]*[.。!?]?/i
|
||||||
|
);
|
||||||
|
return String(concise?.[0] || pageSnapshot).trim();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPhoneResendError() {
|
||||||
|
const maxUsageText = getAddPhoneErrorText();
|
||||||
|
if (maxUsageText && PHONE_MAX_USAGE_EXCEEDED_PATTERN.test(maxUsageText)) {
|
||||||
|
return {
|
||||||
|
hasError: true,
|
||||||
|
reason: 'phone_max_usage_exceeded',
|
||||||
|
message: maxUsageText,
|
||||||
|
url: location.href,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const bannedNumberText = getPhoneResendBannedNumberText();
|
||||||
|
if (bannedNumberText) {
|
||||||
|
return {
|
||||||
|
hasError: true,
|
||||||
|
reason: 'resend_phone_banned',
|
||||||
|
prefix: PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX,
|
||||||
|
message: bannedNumberText,
|
||||||
|
url: location.href,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const throttledText = getPhoneResendThrottleText();
|
||||||
|
if (throttledText) {
|
||||||
|
return {
|
||||||
|
hasError: true,
|
||||||
|
reason: 'resend_throttled',
|
||||||
|
prefix: PHONE_RESEND_THROTTLED_ERROR_PREFIX,
|
||||||
|
message: throttledText,
|
||||||
|
url: location.href,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasError: false,
|
||||||
|
reason: '',
|
||||||
|
message: '',
|
||||||
|
url: location.href,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getAuthRetryButton(options = {}) {
|
function getAuthRetryButton(options = {}) {
|
||||||
const { allowDisabled = false } = options;
|
const { allowDisabled = false } = options;
|
||||||
const direct = document.querySelector('button[data-dd-action-name="Try again"]');
|
const direct = document.querySelector('button[data-dd-action-name="Try again"]');
|
||||||
@@ -779,6 +839,10 @@
|
|||||||
await recoverRoute405WithinResend();
|
await recoverRoute405WithinResend();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const bannedNumberText = getPhoneResendBannedNumberText();
|
||||||
|
if (bannedNumberText) {
|
||||||
|
throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${bannedNumberText}`);
|
||||||
|
}
|
||||||
const throttledText = getPhoneResendThrottleText();
|
const throttledText = getPhoneResendThrottleText();
|
||||||
if (throttledText) {
|
if (throttledText) {
|
||||||
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${throttledText}`);
|
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${throttledText}`);
|
||||||
@@ -792,6 +856,10 @@
|
|||||||
await recoverRoute405WithinResend();
|
await recoverRoute405WithinResend();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const afterClickBannedNumberText = getPhoneResendBannedNumberText();
|
||||||
|
if (afterClickBannedNumberText) {
|
||||||
|
throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${afterClickBannedNumberText}`);
|
||||||
|
}
|
||||||
const afterClickThrottleText = getPhoneResendThrottleText();
|
const afterClickThrottleText = getPhoneResendThrottleText();
|
||||||
if (afterClickThrottleText) {
|
if (afterClickThrottleText) {
|
||||||
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${afterClickThrottleText}`);
|
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${afterClickThrottleText}`);
|
||||||
@@ -804,6 +872,11 @@
|
|||||||
await sleep(250);
|
await sleep(250);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timeoutBannedNumberText = getPhoneResendBannedNumberText();
|
||||||
|
if (timeoutBannedNumberText) {
|
||||||
|
throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${timeoutBannedNumberText}`);
|
||||||
|
}
|
||||||
|
|
||||||
const timeoutThrottleText = getPhoneResendThrottleText();
|
const timeoutThrottleText = getPhoneResendThrottleText();
|
||||||
if (timeoutThrottleText) {
|
if (timeoutThrottleText) {
|
||||||
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${timeoutThrottleText}`);
|
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${timeoutThrottleText}`);
|
||||||
@@ -839,6 +912,7 @@
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
getPhoneVerificationDisplayedPhone,
|
getPhoneVerificationDisplayedPhone,
|
||||||
|
checkPhoneResendError,
|
||||||
isPhoneVerificationPageReady,
|
isPhoneVerificationPageReady,
|
||||||
resendPhoneVerificationCode,
|
resendPhoneVerificationCode,
|
||||||
returnToAddPhone,
|
returnToAddPhone,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1'
|
|||||||
|| message.type === 'SUBMIT_PHONE_NUMBER'
|
|| message.type === 'SUBMIT_PHONE_NUMBER'
|
||||||
|| message.type === 'SUBMIT_PHONE_VERIFICATION_CODE'
|
|| message.type === 'SUBMIT_PHONE_VERIFICATION_CODE'
|
||||||
|| message.type === 'RESEND_PHONE_VERIFICATION_CODE'
|
|| message.type === 'RESEND_PHONE_VERIFICATION_CODE'
|
||||||
|
|| message.type === 'CHECK_PHONE_RESEND_ERROR'
|
||||||
|| message.type === 'RETURN_TO_ADD_PHONE'
|
|| message.type === 'RETURN_TO_ADD_PHONE'
|
||||||
|| message.type === 'ENSURE_SIGNUP_ENTRY_READY'
|
|| message.type === 'ENSURE_SIGNUP_ENTRY_READY'
|
||||||
|| message.type === 'ENSURE_SIGNUP_PHONE_ENTRY_READY'
|
|| message.type === 'ENSURE_SIGNUP_PHONE_ENTRY_READY'
|
||||||
@@ -97,6 +98,8 @@ async function handleCommand(message) {
|
|||||||
return await submitPhoneVerificationCodeWithProfileFallback(message.payload);
|
return await submitPhoneVerificationCodeWithProfileFallback(message.payload);
|
||||||
case 'RESEND_PHONE_VERIFICATION_CODE':
|
case 'RESEND_PHONE_VERIFICATION_CODE':
|
||||||
return await phoneAuthHelpers.resendPhoneVerificationCode();
|
return await phoneAuthHelpers.resendPhoneVerificationCode();
|
||||||
|
case 'CHECK_PHONE_RESEND_ERROR':
|
||||||
|
return phoneAuthHelpers.checkPhoneResendError();
|
||||||
case 'RETURN_TO_ADD_PHONE':
|
case 'RETURN_TO_ADD_PHONE':
|
||||||
return await phoneAuthHelpers.returnToAddPhone();
|
return await phoneAuthHelpers.returnToAddPhone();
|
||||||
case 'ENSURE_SIGNUP_ENTRY_READY':
|
case 'ENSURE_SIGNUP_ENTRY_READY':
|
||||||
@@ -2915,6 +2918,7 @@ const phoneAuthHelpers = self.MultiPagePhoneAuth?.createPhoneAuthHelpers?.({
|
|||||||
resendPhoneVerificationCode: async () => {
|
resendPhoneVerificationCode: async () => {
|
||||||
throw new Error('Phone auth helpers are unavailable.');
|
throw new Error('Phone auth helpers are unavailable.');
|
||||||
},
|
},
|
||||||
|
checkPhoneResendError: () => ({ hasError: false, reason: '', message: '', url: location.href }),
|
||||||
returnToAddPhone: async () => {
|
returnToAddPhone: async () => {
|
||||||
throw new Error('Phone auth helpers are unavailable.');
|
throw new Error('Phone auth helpers are unavailable.');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2367,6 +2367,20 @@ header {
|
|||||||
min-height: 33px;
|
min-height: 33px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-sms-free-phone-cell {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-manual-phone-input {
|
||||||
|
flex: 1 1 150px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-free-phone-country {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.signup-phone-runtime-inline,
|
.signup-phone-runtime-inline,
|
||||||
.signup-phone-runtime-input {
|
.signup-phone-runtime-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1471,6 +1471,28 @@
|
|||||||
<span class="data-unit">次</span>
|
<span class="data-unit">次</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="row-free-phone-reuse-enabled" class="hero-sms-settings-cell hero-sms-reuse-cell" style="display:none;">
|
||||||
|
<span class="hero-sms-settings-caption">白嫖复用</span>
|
||||||
|
<div class="setting-controls hero-sms-toggle-controls">
|
||||||
|
<label class="toggle-switch" for="input-free-phone-reuse-enabled" title="开启后保存首个成功接码手机号;自动复用关闭时,下次只填号码、不提交,并停止自动流程">
|
||||||
|
<input type="checkbox" id="input-free-phone-reuse-enabled" />
|
||||||
|
<span class="toggle-switch-track" aria-hidden="true">
|
||||||
|
<span class="toggle-switch-thumb"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="row-free-phone-reuse-auto-enabled" class="hero-sms-settings-cell hero-sms-reuse-cell" style="display:none;">
|
||||||
|
<span class="hero-sms-settings-caption">自动白嫖复用</span>
|
||||||
|
<div class="setting-controls hero-sms-toggle-controls">
|
||||||
|
<label class="toggle-switch" for="input-free-phone-reuse-auto-enabled" title="开启后自动将已记录手机号设为等待短信状态,自动提交并轮询验证码;最多复用 3 次">
|
||||||
|
<input type="checkbox" id="input-free-phone-reuse-auto-enabled" />
|
||||||
|
<span class="toggle-switch-track" aria-hidden="true">
|
||||||
|
<span class="toggle-switch-thumb"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1491,6 +1513,14 @@
|
|||||||
<span class="hero-sms-runtime-key">验证码</span>
|
<span class="hero-sms-runtime-key">验证码</span>
|
||||||
<span id="display-hero-sms-current-code" class="data-value mono hero-sms-runtime-value">未获取</span>
|
<span id="display-hero-sms-current-code" class="data-value mono hero-sms-runtime-value">未获取</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="row-free-reusable-phone" class="hero-sms-runtime-cell hero-sms-runtime-cell-span2 hero-sms-free-phone-cell" style="display:none;">
|
||||||
|
<span class="hero-sms-runtime-key">白嫖号码</span>
|
||||||
|
<input type="text" id="input-free-reusable-phone" class="data-input mono hero-sms-manual-phone-input" placeholder="复用手机号" title="已记录号码会自动显示;未记录时可手动填写并记录" />
|
||||||
|
<span id="display-free-reusable-phone-country" class="data-value hero-sms-free-phone-country">地区:未保存</span>
|
||||||
|
<span id="display-free-reusable-phone" class="data-value mono hero-sms-runtime-value">未保存</span>
|
||||||
|
<button id="btn-save-free-reusable-phone" class="btn btn-outline btn-xs data-inline-btn hero-sms-runtime-action" type="button">记录</button>
|
||||||
|
<button id="btn-clear-free-reusable-phone" class="btn btn-ghost btn-xs data-inline-btn hero-sms-runtime-action" type="button">清除</button>
|
||||||
|
</div>
|
||||||
<div id="row-hero-sms-preferred-activation" class="hero-sms-runtime-cell hero-sms-runtime-cell-span2" style="display:none;">
|
<div id="row-hero-sms-preferred-activation" class="hero-sms-runtime-cell hero-sms-runtime-cell-span2" style="display:none;">
|
||||||
<span class="hero-sms-runtime-key">优先号码</span>
|
<span class="hero-sms-runtime-key">优先号码</span>
|
||||||
<select id="select-hero-sms-preferred-activation" class="data-input mono hero-sms-runtime-select">
|
<select id="select-hero-sms-preferred-activation" class="data-input mono hero-sms-runtime-select">
|
||||||
|
|||||||
+191
-1
@@ -408,6 +408,9 @@ const rowPhoneCodeWaitSeconds = document.getElementById('row-phone-code-wait-sec
|
|||||||
const rowPhoneCodeTimeoutWindows = document.getElementById('row-phone-code-timeout-windows');
|
const rowPhoneCodeTimeoutWindows = document.getElementById('row-phone-code-timeout-windows');
|
||||||
const rowPhoneCodePollIntervalSeconds = document.getElementById('row-phone-code-poll-interval-seconds');
|
const rowPhoneCodePollIntervalSeconds = document.getElementById('row-phone-code-poll-interval-seconds');
|
||||||
const rowPhoneCodePollMaxRounds = document.getElementById('row-phone-code-poll-max-rounds');
|
const rowPhoneCodePollMaxRounds = document.getElementById('row-phone-code-poll-max-rounds');
|
||||||
|
const rowFreePhoneReuseEnabled = document.getElementById('row-free-phone-reuse-enabled');
|
||||||
|
const rowFreePhoneReuseAutoEnabled = document.getElementById('row-free-phone-reuse-auto-enabled');
|
||||||
|
const rowFreeReusablePhone = document.getElementById('row-free-reusable-phone');
|
||||||
const inputHeroSmsApiKey = document.getElementById('input-hero-sms-api-key');
|
const inputHeroSmsApiKey = document.getElementById('input-hero-sms-api-key');
|
||||||
const btnToggleHeroSmsApiKey = document.getElementById('btn-toggle-hero-sms-api-key');
|
const btnToggleHeroSmsApiKey = document.getElementById('btn-toggle-hero-sms-api-key');
|
||||||
const inputFiveSimApiKey = document.getElementById('input-five-sim-api-key');
|
const inputFiveSimApiKey = document.getElementById('input-five-sim-api-key');
|
||||||
@@ -425,6 +428,9 @@ const inputPhoneCodeTimeoutWindows = document.getElementById('input-phone-code-t
|
|||||||
const inputPhoneCodePollIntervalSeconds = document.getElementById('input-phone-code-poll-interval-seconds');
|
const inputPhoneCodePollIntervalSeconds = document.getElementById('input-phone-code-poll-interval-seconds');
|
||||||
const inputPhoneCodePollMaxRounds = document.getElementById('input-phone-code-poll-max-rounds');
|
const inputPhoneCodePollMaxRounds = document.getElementById('input-phone-code-poll-max-rounds');
|
||||||
const inputHeroSmsReuseEnabled = document.getElementById('input-hero-sms-reuse-enabled');
|
const inputHeroSmsReuseEnabled = document.getElementById('input-hero-sms-reuse-enabled');
|
||||||
|
const inputFreePhoneReuseEnabled = document.getElementById('input-free-phone-reuse-enabled');
|
||||||
|
const inputFreePhoneReuseAutoEnabled = document.getElementById('input-free-phone-reuse-auto-enabled');
|
||||||
|
const inputFreeReusablePhone = document.getElementById('input-free-reusable-phone');
|
||||||
const selectHeroSmsCountry = document.getElementById('select-hero-sms-country');
|
const selectHeroSmsCountry = document.getElementById('select-hero-sms-country');
|
||||||
const selectHeroSmsCountryFallback = document.getElementById('select-hero-sms-country-fallback');
|
const selectHeroSmsCountryFallback = document.getElementById('select-hero-sms-country-fallback');
|
||||||
const selectHeroSmsAcquirePriority = document.getElementById('select-hero-sms-acquire-priority');
|
const selectHeroSmsAcquirePriority = document.getElementById('select-hero-sms-acquire-priority');
|
||||||
@@ -456,10 +462,14 @@ const displayHeroSmsCurrentCountdown = document.getElementById('display-hero-sms
|
|||||||
const displayHeroSmsPriceTiers = document.getElementById('display-hero-sms-price-tiers');
|
const displayHeroSmsPriceTiers = document.getElementById('display-hero-sms-price-tiers');
|
||||||
const displayPhoneSmsBalance = document.getElementById('display-phone-sms-balance');
|
const displayPhoneSmsBalance = document.getElementById('display-phone-sms-balance');
|
||||||
const displayHeroSmsCurrentCode = document.getElementById('display-hero-sms-current-code');
|
const displayHeroSmsCurrentCode = document.getElementById('display-hero-sms-current-code');
|
||||||
|
const displayFreeReusablePhoneCountry = document.getElementById('display-free-reusable-phone-country');
|
||||||
|
const displayFreeReusablePhone = document.getElementById('display-free-reusable-phone');
|
||||||
const displayHeroSmsCountryFallbackOrder = document.getElementById('display-hero-sms-country-fallback-order');
|
const displayHeroSmsCountryFallbackOrder = document.getElementById('display-hero-sms-country-fallback-order');
|
||||||
const displayFiveSimCountryFallbackOrder = document.getElementById('display-five-sim-country-fallback-order');
|
const displayFiveSimCountryFallbackOrder = document.getElementById('display-five-sim-country-fallback-order');
|
||||||
const displayNexSmsCountryFallbackOrder = document.getElementById('display-nex-sms-country-fallback-order');
|
const displayNexSmsCountryFallbackOrder = document.getElementById('display-nex-sms-country-fallback-order');
|
||||||
const displayPhoneSmsProviderOrder = document.getElementById('display-phone-sms-provider-order');
|
const displayPhoneSmsProviderOrder = document.getElementById('display-phone-sms-provider-order');
|
||||||
|
const btnSaveFreeReusablePhone = document.getElementById('btn-save-free-reusable-phone');
|
||||||
|
const btnClearFreeReusablePhone = document.getElementById('btn-clear-free-reusable-phone');
|
||||||
const rowAccountRunHistoryHelperBaseUrl = document.getElementById('row-account-run-history-helper-base-url');
|
const rowAccountRunHistoryHelperBaseUrl = document.getElementById('row-account-run-history-helper-base-url');
|
||||||
const inputAccountRunHistoryHelperBaseUrl = document.getElementById('input-account-run-history-helper-base-url');
|
const inputAccountRunHistoryHelperBaseUrl = document.getElementById('input-account-run-history-helper-base-url');
|
||||||
const autoStartModal = document.getElementById('auto-start-modal');
|
const autoStartModal = document.getElementById('auto-start-modal');
|
||||||
@@ -2989,6 +2999,12 @@ function collectSettingsPayload() {
|
|||||||
const heroSmsReuseEnabledValue = typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled
|
const heroSmsReuseEnabledValue = typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled
|
||||||
? normalizeHeroSmsReuseEnabledValue(inputHeroSmsReuseEnabled.checked)
|
? normalizeHeroSmsReuseEnabledValue(inputHeroSmsReuseEnabled.checked)
|
||||||
: defaultHeroSmsReuseEnabled;
|
: defaultHeroSmsReuseEnabled;
|
||||||
|
const freePhoneReuseEnabledValue = typeof inputFreePhoneReuseEnabled !== 'undefined' && inputFreePhoneReuseEnabled
|
||||||
|
? Boolean(inputFreePhoneReuseEnabled.checked)
|
||||||
|
: Boolean(latestState?.freePhoneReuseEnabled);
|
||||||
|
const freePhoneReuseAutoEnabledValue = typeof inputFreePhoneReuseAutoEnabled !== 'undefined' && inputFreePhoneReuseAutoEnabled
|
||||||
|
? Boolean(inputFreePhoneReuseAutoEnabled.checked)
|
||||||
|
: Boolean(latestState?.freePhoneReuseAutoEnabled);
|
||||||
const defaultHeroSmsAcquirePriority = typeof DEFAULT_HERO_SMS_ACQUIRE_PRIORITY !== 'undefined'
|
const defaultHeroSmsAcquirePriority = typeof DEFAULT_HERO_SMS_ACQUIRE_PRIORITY !== 'undefined'
|
||||||
? DEFAULT_HERO_SMS_ACQUIRE_PRIORITY
|
? DEFAULT_HERO_SMS_ACQUIRE_PRIORITY
|
||||||
: (typeof HERO_SMS_ACQUIRE_PRIORITY_COUNTRY !== 'undefined' ? HERO_SMS_ACQUIRE_PRIORITY_COUNTRY : 'country');
|
: (typeof HERO_SMS_ACQUIRE_PRIORITY_COUNTRY !== 'undefined' ? HERO_SMS_ACQUIRE_PRIORITY_COUNTRY : 'country');
|
||||||
@@ -3364,6 +3380,8 @@ function collectSettingsPayload() {
|
|||||||
nexSmsCountryOrder: nexSmsCountryOrderValue,
|
nexSmsCountryOrder: nexSmsCountryOrderValue,
|
||||||
nexSmsServiceCode: nexSmsServiceCodeValue,
|
nexSmsServiceCode: nexSmsServiceCodeValue,
|
||||||
heroSmsReuseEnabled: heroSmsReuseEnabledValue,
|
heroSmsReuseEnabled: heroSmsReuseEnabledValue,
|
||||||
|
freePhoneReuseEnabled: freePhoneReuseEnabledValue,
|
||||||
|
freePhoneReuseAutoEnabled: freePhoneReuseAutoEnabledValue,
|
||||||
heroSmsAcquirePriority: heroSmsAcquirePriorityValue,
|
heroSmsAcquirePriority: heroSmsAcquirePriorityValue,
|
||||||
heroSmsMaxPrice: heroSmsMaxPriceValue,
|
heroSmsMaxPrice: heroSmsMaxPriceValue,
|
||||||
heroSmsPreferredPrice: heroSmsPreferredPriceValue,
|
heroSmsPreferredPrice: heroSmsPreferredPriceValue,
|
||||||
@@ -5363,10 +5381,79 @@ function updateHeroSmsRuntimeDisplay(state = {}) {
|
|||||||
const code = String(state?.currentPhoneVerificationCode ?? latestState?.currentPhoneVerificationCode ?? '').trim();
|
const code = String(state?.currentPhoneVerificationCode ?? latestState?.currentPhoneVerificationCode ?? '').trim();
|
||||||
displayHeroSmsCurrentCode.textContent = code || '未获取';
|
displayHeroSmsCurrentCode.textContent = code || '未获取';
|
||||||
}
|
}
|
||||||
|
if (displayFreeReusablePhone || displayFreeReusablePhoneCountry || inputFreeReusablePhone) {
|
||||||
|
const activation = state?.freeReusablePhoneActivation ?? latestState?.freeReusablePhoneActivation ?? null;
|
||||||
|
const phoneNumber = String(activation?.phoneNumber || '').trim();
|
||||||
|
const activationId = String(activation?.activationId || '').trim();
|
||||||
|
const successfulUses = Number.isFinite(Number(activation?.successfulUses))
|
||||||
|
? Number(activation.successfulUses)
|
||||||
|
: null;
|
||||||
|
const maxUses = Number.isFinite(Number(activation?.maxUses))
|
||||||
|
? Number(activation.maxUses)
|
||||||
|
: null;
|
||||||
|
const countryLabel = normalizePhoneSmsCountryLabel(
|
||||||
|
activation?.countryLabel
|
||||||
|
|| getHeroSmsCountryLabelById(activation?.countryId || '')
|
||||||
|
|| state?.heroSmsCountryLabel
|
||||||
|
|| latestState?.heroSmsCountryLabel
|
||||||
|
|| getHeroSmsCountryLabelById(state?.heroSmsCountryId || latestState?.heroSmsCountryId || ''),
|
||||||
|
activation?.provider || getSelectedPhoneSmsProvider()
|
||||||
|
);
|
||||||
|
const usesText = successfulUses !== null || maxUses !== null
|
||||||
|
? ` / ${successfulUses ?? 0}/${maxUses ?? 3}`
|
||||||
|
: '';
|
||||||
|
if (displayFreeReusablePhone) {
|
||||||
|
displayFreeReusablePhone.textContent = phoneNumber
|
||||||
|
? `${phoneNumber}${activationId ? ` (#${activationId})` : ''}${usesText}`
|
||||||
|
: '未保存';
|
||||||
|
}
|
||||||
|
if (displayFreeReusablePhoneCountry) {
|
||||||
|
displayFreeReusablePhoneCountry.textContent = phoneNumber
|
||||||
|
? `地区:${countryLabel || '未保存'}`
|
||||||
|
: '地区:未保存';
|
||||||
|
}
|
||||||
|
if (inputFreeReusablePhone) {
|
||||||
|
inputFreeReusablePhone.value = phoneNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
syncPhoneRuntimeCountdown(state);
|
syncPhoneRuntimeCountdown(state);
|
||||||
renderPhonePreferredActivationOptions(state);
|
renderPhonePreferredActivationOptions(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyFreeReusablePhoneMutationResult(refreshedState = {}, mutationResult = {}, options = {}) {
|
||||||
|
const { clear = false } = options;
|
||||||
|
const explicitActivation = clear
|
||||||
|
? null
|
||||||
|
: (
|
||||||
|
mutationResult?.freeReusablePhoneActivation
|
||||||
|
|| mutationResult?.state?.freeReusablePhoneActivation
|
||||||
|
|| null
|
||||||
|
);
|
||||||
|
const statePatch = {
|
||||||
|
...(refreshedState || {}),
|
||||||
|
...(mutationResult?.state || {}),
|
||||||
|
...(clear || explicitActivation
|
||||||
|
? { freeReusablePhoneActivation: explicitActivation }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
syncLatestState(statePatch);
|
||||||
|
updateHeroSmsRuntimeDisplay(latestState || statePatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshFreeReusablePhoneStateFallback(mutationResult = {}, options = {}) {
|
||||||
|
if (mutationResult?.state || mutationResult?.freeReusablePhoneActivation || options.clear) {
|
||||||
|
applyFreeReusablePhoneMutationResult(latestState, mutationResult, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const refreshedState = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' });
|
||||||
|
applyFreeReusablePhoneMutationResult(refreshedState || latestState, mutationResult, options);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to refresh free reusable phone state:', error);
|
||||||
|
applyFreeReusablePhoneMutationResult(latestState, mutationResult, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadHeroSmsCountries() {
|
async function loadHeroSmsCountries() {
|
||||||
const countrySelect = selectHeroSmsCountry || selectHeroSmsCountryFallback;
|
const countrySelect = selectHeroSmsCountry || selectHeroSmsCountryFallback;
|
||||||
if (!countrySelect) {
|
if (!countrySelect) {
|
||||||
@@ -6970,6 +7057,8 @@ function updatePhoneVerificationSettingsUI() {
|
|||||||
typeof rowPhoneCodeTimeoutWindows !== 'undefined' ? rowPhoneCodeTimeoutWindows : null,
|
typeof rowPhoneCodeTimeoutWindows !== 'undefined' ? rowPhoneCodeTimeoutWindows : null,
|
||||||
typeof rowPhoneCodePollIntervalSeconds !== 'undefined' ? rowPhoneCodePollIntervalSeconds : null,
|
typeof rowPhoneCodePollIntervalSeconds !== 'undefined' ? rowPhoneCodePollIntervalSeconds : null,
|
||||||
typeof rowPhoneCodePollMaxRounds !== 'undefined' ? rowPhoneCodePollMaxRounds : null,
|
typeof rowPhoneCodePollMaxRounds !== 'undefined' ? rowPhoneCodePollMaxRounds : null,
|
||||||
|
typeof rowFreePhoneReuseEnabled !== 'undefined' ? rowFreePhoneReuseEnabled : null,
|
||||||
|
typeof rowFreePhoneReuseAutoEnabled !== 'undefined' ? rowFreePhoneReuseAutoEnabled : null,
|
||||||
];
|
];
|
||||||
phoneVerificationRows.forEach((row) => {
|
phoneVerificationRows.forEach((row) => {
|
||||||
if (row) {
|
if (row) {
|
||||||
@@ -6992,12 +7081,33 @@ function updatePhoneVerificationSettingsUI() {
|
|||||||
if (rowFiveSimOperator) {
|
if (rowFiveSimOperator) {
|
||||||
rowFiveSimOperator.style.display = showSettings && fiveSimProvider ? '' : 'none';
|
rowFiveSimOperator.style.display = showSettings && fiveSimProvider ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
if (typeof rowFreePhoneReuseEnabled !== 'undefined' && rowFreePhoneReuseEnabled) {
|
||||||
|
rowFreePhoneReuseEnabled.style.display = showSettings ? '' : 'none';
|
||||||
|
}
|
||||||
|
if (typeof rowFreePhoneReuseAutoEnabled !== 'undefined' && rowFreePhoneReuseAutoEnabled) {
|
||||||
|
rowFreePhoneReuseAutoEnabled.style.display = showSettings ? '' : 'none';
|
||||||
|
}
|
||||||
|
const freePhoneReuseEnabled = Boolean(
|
||||||
|
typeof inputFreePhoneReuseEnabled !== 'undefined' && inputFreePhoneReuseEnabled?.checked
|
||||||
|
);
|
||||||
|
const freePhoneReuseAutoAvailable = showSettings && freePhoneReuseEnabled;
|
||||||
|
if (typeof inputFreePhoneReuseAutoEnabled !== 'undefined' && inputFreePhoneReuseAutoEnabled) {
|
||||||
|
inputFreePhoneReuseAutoEnabled.disabled = !freePhoneReuseAutoAvailable;
|
||||||
|
if (!freePhoneReuseAutoAvailable) {
|
||||||
|
inputFreePhoneReuseAutoEnabled.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFreePhoneReuseControlsLocked(isAutoRunLockedPhase() || isAutoRunScheduledPhase());
|
||||||
|
if (typeof rowFreePhoneReuseAutoEnabled !== 'undefined' && rowFreePhoneReuseAutoEnabled) {
|
||||||
|
rowFreePhoneReuseAutoEnabled.classList.toggle('is-disabled', !freePhoneReuseAutoAvailable);
|
||||||
|
}
|
||||||
const runtimeVisible = enabled;
|
const runtimeVisible = enabled;
|
||||||
[
|
[
|
||||||
typeof rowHeroSmsRuntimePair !== 'undefined' ? rowHeroSmsRuntimePair : null,
|
typeof rowHeroSmsRuntimePair !== 'undefined' ? rowHeroSmsRuntimePair : null,
|
||||||
typeof rowHeroSmsCurrentNumber !== 'undefined' ? rowHeroSmsCurrentNumber : null,
|
typeof rowHeroSmsCurrentNumber !== 'undefined' ? rowHeroSmsCurrentNumber : null,
|
||||||
typeof rowHeroSmsCurrentCountdown !== 'undefined' ? rowHeroSmsCurrentCountdown : null,
|
typeof rowHeroSmsCurrentCountdown !== 'undefined' ? rowHeroSmsCurrentCountdown : null,
|
||||||
typeof rowHeroSmsCurrentCode !== 'undefined' ? rowHeroSmsCurrentCode : null,
|
typeof rowHeroSmsCurrentCode !== 'undefined' ? rowHeroSmsCurrentCode : null,
|
||||||
|
typeof rowFreeReusablePhone !== 'undefined' ? rowFreeReusablePhone : null,
|
||||||
typeof rowHeroSmsPreferredActivation !== 'undefined' ? rowHeroSmsPreferredActivation : null,
|
typeof rowHeroSmsPreferredActivation !== 'undefined' ? rowHeroSmsPreferredActivation : null,
|
||||||
].forEach((row) => {
|
].forEach((row) => {
|
||||||
if (row) {
|
if (row) {
|
||||||
@@ -7119,6 +7229,17 @@ function setSettingsCardLocked(locked) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setFreePhoneReuseControlsLocked(locked) {
|
||||||
|
if (inputFreePhoneReuseEnabled) {
|
||||||
|
inputFreePhoneReuseEnabled.disabled = locked;
|
||||||
|
}
|
||||||
|
if (inputFreePhoneReuseAutoEnabled) {
|
||||||
|
inputFreePhoneReuseAutoEnabled.disabled = locked
|
||||||
|
|| !Boolean(inputFreePhoneReuseEnabled?.checked)
|
||||||
|
|| !Boolean(inputPhoneVerificationEnabled?.checked && phoneVerificationSectionExpanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function setRuntimeEmailState(email) {
|
async function setRuntimeEmailState(email) {
|
||||||
const normalizedEmail = String(email || '').trim() || null;
|
const normalizedEmail = String(email || '').trim() || null;
|
||||||
const response = await chrome.runtime.sendMessage({
|
const response = await chrome.runtime.sendMessage({
|
||||||
@@ -7558,6 +7679,7 @@ function applyAutoRunStatus(payload = currentAutoRun) {
|
|||||||
const settingsCardLocked = scheduled || locked;
|
const settingsCardLocked = scheduled || locked;
|
||||||
|
|
||||||
setSettingsCardLocked(settingsCardLocked);
|
setSettingsCardLocked(settingsCardLocked);
|
||||||
|
setFreePhoneReuseControlsLocked(settingsCardLocked);
|
||||||
|
|
||||||
inputRunCount.disabled = currentAutoRun.autoRunning || (
|
inputRunCount.disabled = currentAutoRun.autoRunning || (
|
||||||
typeof shouldLockRunCountToEmailPool === 'function'
|
typeof shouldLockRunCountToEmailPool === 'function'
|
||||||
@@ -8120,6 +8242,12 @@ function applySettingsState(state) {
|
|||||||
if (typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled) {
|
if (typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled) {
|
||||||
inputHeroSmsReuseEnabled.checked = normalizeHeroSmsReuseEnabledValue(state?.heroSmsReuseEnabled);
|
inputHeroSmsReuseEnabled.checked = normalizeHeroSmsReuseEnabledValue(state?.heroSmsReuseEnabled);
|
||||||
}
|
}
|
||||||
|
if (typeof inputFreePhoneReuseEnabled !== 'undefined' && inputFreePhoneReuseEnabled) {
|
||||||
|
inputFreePhoneReuseEnabled.checked = Boolean(state?.freePhoneReuseEnabled);
|
||||||
|
}
|
||||||
|
if (typeof inputFreePhoneReuseAutoEnabled !== 'undefined' && inputFreePhoneReuseAutoEnabled) {
|
||||||
|
inputFreePhoneReuseAutoEnabled.checked = Boolean(state?.freePhoneReuseAutoEnabled);
|
||||||
|
}
|
||||||
if (typeof selectHeroSmsAcquirePriority !== 'undefined' && selectHeroSmsAcquirePriority) {
|
if (typeof selectHeroSmsAcquirePriority !== 'undefined' && selectHeroSmsAcquirePriority) {
|
||||||
selectHeroSmsAcquirePriority.value = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
|
selectHeroSmsAcquirePriority.value = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
|
||||||
}
|
}
|
||||||
@@ -12459,6 +12587,59 @@ inputHeroSmsReuseEnabled?.addEventListener('change', () => {
|
|||||||
saveSettings({ silent: true }).catch(() => { });
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
inputFreePhoneReuseEnabled?.addEventListener('change', () => {
|
||||||
|
if (!inputFreePhoneReuseEnabled.checked && inputFreePhoneReuseAutoEnabled) {
|
||||||
|
inputFreePhoneReuseAutoEnabled.checked = false;
|
||||||
|
}
|
||||||
|
updatePhoneVerificationSettingsUI();
|
||||||
|
markSettingsDirty(true);
|
||||||
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
|
});
|
||||||
|
|
||||||
|
inputFreePhoneReuseAutoEnabled?.addEventListener('change', () => {
|
||||||
|
updatePhoneVerificationSettingsUI();
|
||||||
|
markSettingsDirty(true);
|
||||||
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
|
});
|
||||||
|
|
||||||
|
btnSaveFreeReusablePhone?.addEventListener('click', async () => {
|
||||||
|
const phoneNumber = String(inputFreeReusablePhone?.value || '').trim();
|
||||||
|
if (!phoneNumber) {
|
||||||
|
showToast?.('请先填写白嫖复用手机号。', 'warn', 2200);
|
||||||
|
inputFreeReusablePhone?.focus?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
type: 'SET_FREE_REUSABLE_PHONE', payload: { phoneNumber },
|
||||||
|
});
|
||||||
|
if (response?.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
await refreshFreeReusablePhoneStateFallback(response || {});
|
||||||
|
showToast?.('已记录白嫖复用手机号。', 'success', 1800);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save free reusable phone:', error);
|
||||||
|
showToast?.(`记录白嫖复用手机号失败:${error?.message || error}`, 'error', 4000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnClearFreeReusablePhone?.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
type: 'CLEAR_FREE_REUSABLE_PHONE',
|
||||||
|
});
|
||||||
|
if (response?.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
await refreshFreeReusablePhoneStateFallback(response || {}, { clear: true });
|
||||||
|
showToast?.('已清除白嫖复用手机号。', 'info', 1800);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear free reusable phone:', error);
|
||||||
|
showToast?.(`清除白嫖复用手机号失败:${error?.message || error}`, 'error', 4000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
selectHeroSmsAcquirePriority?.addEventListener('change', () => {
|
selectHeroSmsAcquirePriority?.addEventListener('change', () => {
|
||||||
selectHeroSmsAcquirePriority.value = normalizeHeroSmsAcquirePriority(selectHeroSmsAcquirePriority.value);
|
selectHeroSmsAcquirePriority.value = normalizeHeroSmsAcquirePriority(selectHeroSmsAcquirePriority.value);
|
||||||
markSettingsDirty(true);
|
markSettingsDirty(true);
|
||||||
@@ -13283,6 +13464,14 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|||||||
if (message.payload.heroSmsReuseEnabled !== undefined && inputHeroSmsReuseEnabled) {
|
if (message.payload.heroSmsReuseEnabled !== undefined && inputHeroSmsReuseEnabled) {
|
||||||
inputHeroSmsReuseEnabled.checked = normalizeHeroSmsReuseEnabledValue(message.payload.heroSmsReuseEnabled);
|
inputHeroSmsReuseEnabled.checked = normalizeHeroSmsReuseEnabledValue(message.payload.heroSmsReuseEnabled);
|
||||||
}
|
}
|
||||||
|
if (message.payload.freePhoneReuseEnabled !== undefined && inputFreePhoneReuseEnabled) {
|
||||||
|
inputFreePhoneReuseEnabled.checked = Boolean(message.payload.freePhoneReuseEnabled);
|
||||||
|
updatePhoneVerificationSettingsUI();
|
||||||
|
}
|
||||||
|
if (message.payload.freePhoneReuseAutoEnabled !== undefined && inputFreePhoneReuseAutoEnabled) {
|
||||||
|
inputFreePhoneReuseAutoEnabled.checked = Boolean(message.payload.freePhoneReuseAutoEnabled);
|
||||||
|
updatePhoneVerificationSettingsUI();
|
||||||
|
}
|
||||||
if (message.payload.heroSmsAcquirePriority !== undefined && selectHeroSmsAcquirePriority) {
|
if (message.payload.heroSmsAcquirePriority !== undefined && selectHeroSmsAcquirePriority) {
|
||||||
selectHeroSmsAcquirePriority.value = normalizeHeroSmsAcquirePriority(message.payload.heroSmsAcquirePriority);
|
selectHeroSmsAcquirePriority.value = normalizeHeroSmsAcquirePriority(message.payload.heroSmsAcquirePriority);
|
||||||
}
|
}
|
||||||
@@ -13437,7 +13626,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|||||||
: latestState?.heroSmsCountryFallback
|
: latestState?.heroSmsCountryFallback
|
||||||
);
|
);
|
||||||
applyHeroSmsFallbackSelection(
|
applyHeroSmsFallbackSelection(
|
||||||
[...nextPrimaryCountries, ...nextFallback],
|
[nextPrimary, ...nextFallback],
|
||||||
{ includePrimary: true }
|
{ includePrimary: true }
|
||||||
);
|
);
|
||||||
updateHeroSmsPlatformDisplay();
|
updateHeroSmsPlatformDisplay();
|
||||||
@@ -13451,6 +13640,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|||||||
|| message.payload.currentPhoneVerificationCountdownEndsAt !== undefined
|
|| message.payload.currentPhoneVerificationCountdownEndsAt !== undefined
|
||||||
|| message.payload.currentPhoneVerificationCountdownWindowIndex !== undefined
|
|| message.payload.currentPhoneVerificationCountdownWindowIndex !== undefined
|
||||||
|| message.payload.currentPhoneVerificationCountdownWindowTotal !== undefined
|
|| message.payload.currentPhoneVerificationCountdownWindowTotal !== undefined
|
||||||
|
|| message.payload.freeReusablePhoneActivation !== undefined
|
||||||
|| message.payload.heroSmsLastPriceTiers !== undefined
|
|| message.payload.heroSmsLastPriceTiers !== undefined
|
||||||
|| message.payload.heroSmsLastPriceCountryId !== undefined
|
|| message.payload.heroSmsLastPriceCountryId !== undefined
|
||||||
|| message.payload.heroSmsLastPriceCountryLabel !== undefined
|
|| message.payload.heroSmsLastPriceCountryLabel !== undefined
|
||||||
|
|||||||
@@ -7,6 +7,27 @@ test('background imports message router module', () => {
|
|||||||
assert.match(source, /background\/message-router\.js/);
|
assert.match(source, /background\/message-router\.js/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('background defaults enable free phone reuse switches', () => {
|
||||||
|
const source = fs.readFileSync('background.js', 'utf8');
|
||||||
|
const defaultsStart = source.indexOf('const PERSISTED_SETTING_DEFAULTS = {');
|
||||||
|
const defaultsEnd = source.indexOf('const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);');
|
||||||
|
const defaultsBlock = source.slice(defaultsStart, defaultsEnd);
|
||||||
|
|
||||||
|
assert.match(defaultsBlock, /freePhoneReuseEnabled:\s*true/);
|
||||||
|
assert.match(defaultsBlock, /freePhoneReuseAutoEnabled:\s*true/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('background free reusable phone setter does not depend on module-scoped phone flow constants', () => {
|
||||||
|
const source = fs.readFileSync('background.js', 'utf8');
|
||||||
|
const setterStart = source.indexOf('async function setFreeReusablePhoneActivation');
|
||||||
|
const setterEnd = source.indexOf('// ============================================================\n// Tab Registry', setterStart);
|
||||||
|
const setterBlock = source.slice(setterStart, setterEnd);
|
||||||
|
|
||||||
|
assert.ok(setterStart >= 0, 'expected setFreeReusablePhoneActivation to exist');
|
||||||
|
assert.doesNotMatch(setterBlock, /DEFAULT_PHONE_NUMBER_MAX_USES/);
|
||||||
|
assert.match(setterBlock, /maxUses:\s*Math\.max\(1,\s*Math\.floor\(Number\(record\.maxUses\)\s*\|\|\s*3\)\)/);
|
||||||
|
});
|
||||||
|
|
||||||
test('message router module exposes a factory', () => {
|
test('message router module exposes a factory', () => {
|
||||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||||
const globalScope = {};
|
const globalScope = {};
|
||||||
@@ -15,3 +36,56 @@ test('message router module exposes a factory', () => {
|
|||||||
|
|
||||||
assert.equal(typeof api?.createMessageRouter, 'function');
|
assert.equal(typeof api?.createMessageRouter, 'function');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('SAVE_SETTING broadcasts free phone reuse setting updates for realtime sidepanel sync', async () => {
|
||||||
|
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||||
|
const globalScope = { console };
|
||||||
|
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||||||
|
const broadcasts = [];
|
||||||
|
let state = {
|
||||||
|
freePhoneReuseEnabled: false,
|
||||||
|
freePhoneReuseAutoEnabled: false,
|
||||||
|
plusModeEnabled: false,
|
||||||
|
plusPaymentMethod: 'paypal',
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = api.createMessageRouter({
|
||||||
|
addLog: async () => {},
|
||||||
|
buildLuckmailSessionSettingsPayload: () => ({}),
|
||||||
|
buildPersistentSettingsPayload: (input = {}) => {
|
||||||
|
const updates = {};
|
||||||
|
if (Object.prototype.hasOwnProperty.call(input, 'freePhoneReuseEnabled')) {
|
||||||
|
updates.freePhoneReuseEnabled = Boolean(input.freePhoneReuseEnabled);
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(input, 'freePhoneReuseAutoEnabled')) {
|
||||||
|
updates.freePhoneReuseAutoEnabled = Boolean(input.freePhoneReuseAutoEnabled);
|
||||||
|
}
|
||||||
|
return updates;
|
||||||
|
},
|
||||||
|
broadcastDataUpdate: (payload) => broadcasts.push(payload),
|
||||||
|
getState: async () => ({ ...state }),
|
||||||
|
setPersistentSettings: async () => {},
|
||||||
|
setState: async (updates) => {
|
||||||
|
state = { ...state, ...updates };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await router.handleMessage({
|
||||||
|
type: 'SAVE_SETTING',
|
||||||
|
payload: {
|
||||||
|
freePhoneReuseEnabled: true,
|
||||||
|
freePhoneReuseAutoEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(response.ok, true);
|
||||||
|
assert.equal(state.freePhoneReuseEnabled, true);
|
||||||
|
assert.equal(state.freePhoneReuseAutoEnabled, true);
|
||||||
|
assert.ok(
|
||||||
|
broadcasts.some((payload) => (
|
||||||
|
payload.freePhoneReuseEnabled === true
|
||||||
|
&& payload.freePhoneReuseAutoEnabled === true
|
||||||
|
)),
|
||||||
|
'expected SAVE_SETTING to broadcast free reuse switch updates'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -448,3 +448,52 @@ test('phone auth resend stops with PHONE_ROUTE_405_RECOVERY_FAILED instead of en
|
|||||||
global.window = originalWindow;
|
global.window = originalWindow;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('phone auth exposes resend page error checks for banned numbers', () => {
|
||||||
|
const originalLocation = global.location;
|
||||||
|
const originalDocument = global.document;
|
||||||
|
try {
|
||||||
|
global.location = {
|
||||||
|
href: 'https://auth.openai.com/phone-verification',
|
||||||
|
pathname: '/phone-verification',
|
||||||
|
};
|
||||||
|
global.document = {
|
||||||
|
querySelector() {
|
||||||
|
return {
|
||||||
|
querySelector() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
querySelectorAll() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const helpers = api.createPhoneAuthHelpers({
|
||||||
|
fillInput: () => {},
|
||||||
|
getActionText: () => '',
|
||||||
|
getPageTextSnapshot: () => 'We cannot send a text message to this phone number. Please try another.',
|
||||||
|
getVerificationErrorText: () => '',
|
||||||
|
humanPause: async () => {},
|
||||||
|
isActionEnabled: () => true,
|
||||||
|
isAddPhonePageReady: () => false,
|
||||||
|
isConsentReady: () => false,
|
||||||
|
isPhoneVerificationPageReady: () => true,
|
||||||
|
isVisibleElement: () => true,
|
||||||
|
simulateClick: () => {},
|
||||||
|
sleep: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
waitForElement: async () => null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = helpers.checkPhoneResendError();
|
||||||
|
assert.equal(result.hasError, true);
|
||||||
|
assert.equal(result.reason, 'resend_phone_banned');
|
||||||
|
assert.equal(result.prefix, 'PHONE_RESEND_BANNED_NUMBER::');
|
||||||
|
assert.match(result.message, /cannot send/i);
|
||||||
|
} finally {
|
||||||
|
global.location = originalLocation;
|
||||||
|
global.document = originalDocument;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -2794,6 +2794,28 @@ test('phone verification helper immediately replaces number when page says the p
|
|||||||
verificationResendCount: 1,
|
verificationResendCount: 1,
|
||||||
currentPhoneActivation: null,
|
currentPhoneActivation: null,
|
||||||
reusablePhoneActivation: null,
|
reusablePhoneActivation: null,
|
||||||
|
phoneReusableActivationPool: [
|
||||||
|
{
|
||||||
|
activationId: 'pool-used-match',
|
||||||
|
phoneNumber: '+66 95 000 0011',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 52,
|
||||||
|
successfulUses: 1,
|
||||||
|
maxUses: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
freeReusablePhoneActivation: {
|
||||||
|
activationId: 'free-used-match',
|
||||||
|
phoneNumber: '+66 95 000 0011',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 52,
|
||||||
|
countryLabel: 'Thailand',
|
||||||
|
successfulUses: 0,
|
||||||
|
maxUses: 3,
|
||||||
|
source: 'free-manual-reuse',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const numbers = [
|
const numbers = [
|
||||||
@@ -2903,6 +2925,11 @@ test('phone verification helper immediately replaces number when page says the p
|
|||||||
'SUBMIT_PHONE_NUMBER',
|
'SUBMIT_PHONE_NUMBER',
|
||||||
'SUBMIT_PHONE_VERIFICATION_CODE',
|
'SUBMIT_PHONE_VERIFICATION_CODE',
|
||||||
]);
|
]);
|
||||||
|
assert.equal(currentState.freeReusablePhoneActivation, null);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
currentState.phoneReusableActivationPool.filter((activation) => activation.phoneNumber === '+66 95 000 0011'),
|
||||||
|
[]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('phone verification helper treats phone_max_usage_exceeded as used-number and rotates immediately', async () => {
|
test('phone verification helper treats phone_max_usage_exceeded as used-number and rotates immediately', async () => {
|
||||||
@@ -2913,6 +2940,28 @@ test('phone verification helper treats phone_max_usage_exceeded as used-number a
|
|||||||
verificationResendCount: 1,
|
verificationResendCount: 1,
|
||||||
currentPhoneActivation: null,
|
currentPhoneActivation: null,
|
||||||
reusablePhoneActivation: null,
|
reusablePhoneActivation: null,
|
||||||
|
phoneReusableActivationPool: [
|
||||||
|
{
|
||||||
|
activationId: 'pool-max-match',
|
||||||
|
phoneNumber: '+66 95 000 1011',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 52,
|
||||||
|
successfulUses: 1,
|
||||||
|
maxUses: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
freeReusablePhoneActivation: {
|
||||||
|
activationId: 'free-max-match',
|
||||||
|
phoneNumber: '+66 95 000 1011',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 52,
|
||||||
|
countryLabel: 'Thailand',
|
||||||
|
successfulUses: 0,
|
||||||
|
maxUses: 3,
|
||||||
|
source: 'free-manual-reuse',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const numbers = [
|
const numbers = [
|
||||||
@@ -3022,6 +3071,11 @@ test('phone verification helper treats phone_max_usage_exceeded as used-number a
|
|||||||
'SUBMIT_PHONE_NUMBER',
|
'SUBMIT_PHONE_NUMBER',
|
||||||
'SUBMIT_PHONE_VERIFICATION_CODE',
|
'SUBMIT_PHONE_VERIFICATION_CODE',
|
||||||
]);
|
]);
|
||||||
|
assert.equal(currentState.freeReusablePhoneActivation, null);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
currentState.phoneReusableActivationPool.filter((activation) => activation.phoneNumber === '+66 95 000 1011'),
|
||||||
|
[]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('phone verification helper rotates number when submitPhoneVerificationCode throws phone_max_usage_exceeded', async () => {
|
test('phone verification helper rotates number when submitPhoneVerificationCode throws phone_max_usage_exceeded', async () => {
|
||||||
@@ -3418,6 +3472,771 @@ test('phone verification helper keeps maxUses behavior for reused V2 activations
|
|||||||
assert.deepStrictEqual(currentState.reusablePhoneActivation, null);
|
assert.deepStrictEqual(currentState.reusablePhoneActivation, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('phone verification helper gives automatic free reuse priority over paid reuse and new acquisition', async () => {
|
||||||
|
const requests = [];
|
||||||
|
let statusPollCount = 0;
|
||||||
|
let currentState = {
|
||||||
|
heroSmsApiKey: 'demo-key',
|
||||||
|
heroSmsReuseEnabled: true,
|
||||||
|
freePhoneReuseEnabled: true,
|
||||||
|
freePhoneReuseAutoEnabled: true,
|
||||||
|
verificationResendCount: 0,
|
||||||
|
currentPhoneActivation: null,
|
||||||
|
reusablePhoneActivation: {
|
||||||
|
activationId: 'paid-reuse',
|
||||||
|
phoneNumber: '66950003333',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 52,
|
||||||
|
countryLabel: 'Thailand',
|
||||||
|
successfulUses: 0,
|
||||||
|
maxUses: 3,
|
||||||
|
},
|
||||||
|
freeReusablePhoneActivation: {
|
||||||
|
activationId: 'free-priority',
|
||||||
|
phoneNumber: '66950004444',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 52,
|
||||||
|
countryLabel: 'Thailand',
|
||||||
|
successfulUses: 0,
|
||||||
|
maxUses: 3,
|
||||||
|
source: 'free-manual-reuse',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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 id = parsedUrl.searchParams.get('id');
|
||||||
|
if (action === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
|
||||||
|
throw new Error(`${action} should not be called before automatic free reuse`);
|
||||||
|
}
|
||||||
|
if (action === 'setStatus' && id === 'free-priority') {
|
||||||
|
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
|
||||||
|
}
|
||||||
|
if (action === 'getStatus' && id === 'free-priority') {
|
||||||
|
statusPollCount += 1;
|
||||||
|
return { ok: true, text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_CODE' : 'STATUS_OK:112233') };
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
|
||||||
|
},
|
||||||
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
|
getState: async () => ({ ...currentState }),
|
||||||
|
sendToContentScriptResilient: async (_source, message) => {
|
||||||
|
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||||
|
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||||
|
}
|
||||||
|
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||||
|
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async (updates) => {
|
||||||
|
currentState = { ...currentState, ...updates };
|
||||||
|
},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||||
|
addPhonePage: true,
|
||||||
|
phoneVerificationPage: false,
|
||||||
|
url: 'https://auth.openai.com/add-phone',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result, {
|
||||||
|
success: true,
|
||||||
|
consentReady: true,
|
||||||
|
url: 'https://auth.openai.com/authorize',
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
requests.map((requestUrl) => requestUrl.searchParams.get('action')),
|
||||||
|
['setStatus', 'getStatus', 'getStatus']
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
requests
|
||||||
|
.filter((requestUrl) => requestUrl.searchParams.get('action') === 'setStatus')
|
||||||
|
.map((requestUrl) => requestUrl.searchParams.get('status')),
|
||||||
|
['3']
|
||||||
|
);
|
||||||
|
assert.equal(currentState.freeReusablePhoneActivation.successfulUses, 1);
|
||||||
|
assert.equal(currentState.freeReusablePhoneActivation.activationId, 'free-priority');
|
||||||
|
assert.equal(currentState.reusablePhoneActivation.activationId, 'paid-reuse');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper accepts HeroSMS WAIT_RETRY as free-reuse ready without using its suffix as code', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const events = [];
|
||||||
|
const submittedCodes = [];
|
||||||
|
let statusPollCount = 0;
|
||||||
|
let currentState = {
|
||||||
|
heroSmsApiKey: 'demo-key',
|
||||||
|
freePhoneReuseEnabled: true,
|
||||||
|
freePhoneReuseAutoEnabled: true,
|
||||||
|
verificationResendCount: 0,
|
||||||
|
currentPhoneActivation: null,
|
||||||
|
freeReusablePhoneActivation: {
|
||||||
|
activationId: 'retry-ready-free',
|
||||||
|
phoneNumber: '6283184934060',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 6,
|
||||||
|
countryLabel: 'Indonesia',
|
||||||
|
successfulUses: 0,
|
||||||
|
maxUses: 3,
|
||||||
|
source: 'free-manual-reuse',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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 id = parsedUrl.searchParams.get('id');
|
||||||
|
const status = parsedUrl.searchParams.get('status');
|
||||||
|
events.push(action === 'setStatus' ? `${action}:${status}` : action);
|
||||||
|
if (action === 'setStatus' && id === 'retry-ready-free' && status === '3') {
|
||||||
|
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
|
||||||
|
}
|
||||||
|
if (action === 'getStatus' && id === 'retry-ready-free') {
|
||||||
|
statusPollCount += 1;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_RETRY:100001' : 'STATUS_OK:654321'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}:${status || ''}`);
|
||||||
|
},
|
||||||
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
|
getState: async () => ({ ...currentState }),
|
||||||
|
sendToContentScriptResilient: async (_source, message) => {
|
||||||
|
events.push(message.type);
|
||||||
|
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||||
|
assert.equal(message.payload.phoneNumber, '6283184934060');
|
||||||
|
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||||
|
}
|
||||||
|
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||||
|
submittedCodes.push(message.payload.code);
|
||||||
|
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async (updates) => {
|
||||||
|
currentState = { ...currentState, ...updates };
|
||||||
|
},
|
||||||
|
sleepWithStop: async () => {
|
||||||
|
events.push('sleep');
|
||||||
|
},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||||
|
addPhonePage: true,
|
||||||
|
phoneVerificationPage: false,
|
||||||
|
url: 'https://auth.openai.com/add-phone',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result, {
|
||||||
|
success: true,
|
||||||
|
consentReady: true,
|
||||||
|
url: 'https://auth.openai.com/authorize',
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(submittedCodes, ['654321']);
|
||||||
|
assert.equal(submittedCodes.includes('100001'), false);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
events.slice(0, 4),
|
||||||
|
['setStatus:3', 'sleep', 'getStatus', 'SUBMIT_PHONE_NUMBER']
|
||||||
|
);
|
||||||
|
assert.equal(currentState.freeReusablePhoneActivation.successfulUses, 1);
|
||||||
|
assert.equal(
|
||||||
|
requests.some((requestUrl) => ['reactivate', 'getPrices', 'getNumber', 'getNumberV2'].includes(requestUrl.searchParams.get('action'))),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper stops failed automatic free reuse without buying a new number', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const messages = [];
|
||||||
|
const stops = [];
|
||||||
|
let currentState = {
|
||||||
|
heroSmsApiKey: 'demo-key',
|
||||||
|
freePhoneReuseEnabled: true,
|
||||||
|
freePhoneReuseAutoEnabled: true,
|
||||||
|
verificationResendCount: 0,
|
||||||
|
currentPhoneActivation: null,
|
||||||
|
freeReusablePhoneActivation: {
|
||||||
|
activationId: 'cancelled-free',
|
||||||
|
phoneNumber: '66950007777',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 52,
|
||||||
|
countryLabel: 'Thailand',
|
||||||
|
successfulUses: 0,
|
||||||
|
maxUses: 3,
|
||||||
|
source: 'free-manual-reuse',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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 id = parsedUrl.searchParams.get('id');
|
||||||
|
if (action === 'setStatus' && id === 'cancelled-free') {
|
||||||
|
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
|
||||||
|
}
|
||||||
|
if (action === 'getStatus' && id === 'cancelled-free') {
|
||||||
|
return { ok: true, text: async () => 'STATUS_CANCEL' };
|
||||||
|
}
|
||||||
|
if (action === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
|
||||||
|
throw new Error(`${action} should not be called after automatic free reuse preparation fails`);
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
|
||||||
|
},
|
||||||
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
|
getState: async () => ({ ...currentState }),
|
||||||
|
requestStop: async (payload) => {
|
||||||
|
stops.push(payload);
|
||||||
|
},
|
||||||
|
sendToContentScriptResilient: async (_source, message) => {
|
||||||
|
messages.push(message);
|
||||||
|
throw new Error(`Auth page should not be touched after failed free reuse: ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async (updates) => {
|
||||||
|
currentState = { ...currentState, ...updates };
|
||||||
|
},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
helpers.completePhoneVerificationFlow(1, {
|
||||||
|
addPhonePage: true,
|
||||||
|
phoneVerificationPage: false,
|
||||||
|
url: 'https://auth.openai.com/add-phone',
|
||||||
|
}),
|
||||||
|
/PHONE_AUTO_FREE_REUSE_PREPARE::/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(messages, []);
|
||||||
|
assert.equal(stops.length, 1);
|
||||||
|
assert.match(stops[0].logMessage, /不购买新 HeroSMS 号码/);
|
||||||
|
assert.equal(currentState.freeReusablePhoneActivation, null);
|
||||||
|
assert.equal(
|
||||||
|
requests.some((requestUrl) => ['reactivate', 'getPrices', 'getNumber', 'getNumberV2'].includes(requestUrl.searchParams.get('action'))),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper stops never-ready automatic free reuse without buying or reactivating', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const messages = [];
|
||||||
|
const stops = [];
|
||||||
|
let currentState = {
|
||||||
|
heroSmsApiKey: 'demo-key',
|
||||||
|
heroSmsReuseEnabled: true,
|
||||||
|
freePhoneReuseEnabled: true,
|
||||||
|
freePhoneReuseAutoEnabled: true,
|
||||||
|
verificationResendCount: 0,
|
||||||
|
currentPhoneActivation: null,
|
||||||
|
freeReusablePhoneActivation: {
|
||||||
|
activationId: 'never-ready-free',
|
||||||
|
phoneNumber: '66950008888',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 52,
|
||||||
|
countryLabel: 'Thailand',
|
||||||
|
successfulUses: 0,
|
||||||
|
maxUses: 3,
|
||||||
|
source: 'free-manual-reuse',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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 id = parsedUrl.searchParams.get('id');
|
||||||
|
if (action === 'setStatus' && id === 'never-ready-free') {
|
||||||
|
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
|
||||||
|
}
|
||||||
|
if (action === 'getStatus' && id === 'never-ready-free') {
|
||||||
|
return { ok: true, text: async () => 'STATUS_OK:445566' };
|
||||||
|
}
|
||||||
|
if (action === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
|
||||||
|
throw new Error(`${action} should not be called after automatic free reuse never becomes ready`);
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
|
||||||
|
},
|
||||||
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
|
getState: async () => ({ ...currentState }),
|
||||||
|
requestStop: async (payload) => {
|
||||||
|
stops.push(payload);
|
||||||
|
},
|
||||||
|
sendToContentScriptResilient: async (_source, message) => {
|
||||||
|
messages.push(message);
|
||||||
|
throw new Error(`Auth page should not be touched when automatic free reuse is never ready: ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async (updates) => {
|
||||||
|
currentState = { ...currentState, ...updates };
|
||||||
|
},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
helpers.completePhoneVerificationFlow(1, {
|
||||||
|
addPhonePage: true,
|
||||||
|
phoneVerificationPage: false,
|
||||||
|
url: 'https://auth.openai.com/add-phone',
|
||||||
|
}),
|
||||||
|
/PHONE_AUTO_FREE_REUSE_PREPARE::/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(messages, []);
|
||||||
|
assert.equal(stops.length, 1);
|
||||||
|
assert.match(stops[0].logMessage, /不购买新 HeroSMS 号码/);
|
||||||
|
assert.equal(currentState.freeReusablePhoneActivation.activationId, 'never-ready-free');
|
||||||
|
assert.equal(
|
||||||
|
requests.some((requestUrl) => ['reactivate', 'getPrices', 'getNumber', 'getNumberV2'].includes(requestUrl.searchParams.get('action'))),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
assert.equal(requests.every((requestUrl) => requestUrl.searchParams.get('id') === 'never-ready-free'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper accepts HeroSMS WAIT_RESEND as free-reuse ready before submit', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const messages = [];
|
||||||
|
let statusPollCount = 0;
|
||||||
|
let currentState = {
|
||||||
|
heroSmsApiKey: 'demo-key',
|
||||||
|
freePhoneReuseEnabled: true,
|
||||||
|
freePhoneReuseAutoEnabled: true,
|
||||||
|
verificationResendCount: 0,
|
||||||
|
currentPhoneActivation: null,
|
||||||
|
freeReusablePhoneActivation: {
|
||||||
|
activationId: 'not-waiting-free',
|
||||||
|
phoneNumber: '66950005555',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 52,
|
||||||
|
countryLabel: 'Thailand',
|
||||||
|
successfulUses: 0,
|
||||||
|
maxUses: 3,
|
||||||
|
source: 'free-manual-reuse',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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 id = parsedUrl.searchParams.get('id');
|
||||||
|
if (action === 'setStatus' && id === 'not-waiting-free') {
|
||||||
|
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
|
||||||
|
}
|
||||||
|
if (action === 'getStatus' && id === 'not-waiting-free') {
|
||||||
|
statusPollCount += 1;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_RESEND' : 'STATUS_OK:778899'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
|
||||||
|
throw new Error(`${action} should not be called for accepted saved free reuse waiting state`);
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
|
||||||
|
},
|
||||||
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
|
getState: async () => ({ ...currentState }),
|
||||||
|
sendToContentScriptResilient: async (_source, message) => {
|
||||||
|
messages.push(message);
|
||||||
|
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||||
|
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||||
|
}
|
||||||
|
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||||
|
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async (updates) => {
|
||||||
|
currentState = { ...currentState, ...updates };
|
||||||
|
},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||||
|
addPhonePage: true,
|
||||||
|
phoneVerificationPage: false,
|
||||||
|
url: 'https://auth.openai.com/add-phone',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result, {
|
||||||
|
success: true,
|
||||||
|
consentReady: true,
|
||||||
|
url: 'https://auth.openai.com/authorize',
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
messages.map((message) => message.type),
|
||||||
|
['SUBMIT_PHONE_NUMBER', 'SUBMIT_PHONE_VERIFICATION_CODE']
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
requests
|
||||||
|
.filter((requestUrl) => requestUrl.searchParams.get('action') === 'setStatus')
|
||||||
|
.map((requestUrl) => requestUrl.searchParams.get('status')),
|
||||||
|
['3']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper accepts HeroSMS WAIT_RESEND suffix as free-reuse ready before submit', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const messages = [];
|
||||||
|
let statusPollCount = 0;
|
||||||
|
let currentState = {
|
||||||
|
heroSmsApiKey: 'demo-key',
|
||||||
|
freePhoneReuseEnabled: true,
|
||||||
|
freePhoneReuseAutoEnabled: true,
|
||||||
|
verificationResendCount: 0,
|
||||||
|
currentPhoneActivation: null,
|
||||||
|
freeReusablePhoneActivation: {
|
||||||
|
activationId: 'resend-info-free',
|
||||||
|
phoneNumber: '66950005656',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 52,
|
||||||
|
countryLabel: 'Thailand',
|
||||||
|
successfulUses: 0,
|
||||||
|
maxUses: 3,
|
||||||
|
source: 'free-manual-reuse',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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 id = parsedUrl.searchParams.get('id');
|
||||||
|
if (action === 'setStatus' && id === 'resend-info-free') {
|
||||||
|
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
|
||||||
|
}
|
||||||
|
if (action === 'getStatus' && id === 'resend-info-free') {
|
||||||
|
statusPollCount += 1;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_RESEND:100001' : 'STATUS_OK:887766'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
|
||||||
|
throw new Error(`${action} should not be called for accepted saved free reuse resend state`);
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
|
||||||
|
},
|
||||||
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
|
getState: async () => ({ ...currentState }),
|
||||||
|
sendToContentScriptResilient: async (_source, message) => {
|
||||||
|
messages.push(message);
|
||||||
|
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||||
|
assert.equal(message.payload.phoneNumber, '66950005656');
|
||||||
|
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||||
|
}
|
||||||
|
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||||
|
assert.equal(message.payload.code, '887766');
|
||||||
|
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async (updates) => {
|
||||||
|
currentState = { ...currentState, ...updates };
|
||||||
|
},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||||
|
addPhonePage: true,
|
||||||
|
phoneVerificationPage: false,
|
||||||
|
url: 'https://auth.openai.com/add-phone',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result, {
|
||||||
|
success: true,
|
||||||
|
consentReady: true,
|
||||||
|
url: 'https://auth.openai.com/authorize',
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
messages.map((message) => message.type),
|
||||||
|
['SUBMIT_PHONE_NUMBER', 'SUBMIT_PHONE_VERIFICATION_CODE']
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
requests
|
||||||
|
.filter((requestUrl) => requestUrl.searchParams.get('action') === 'setStatus')
|
||||||
|
.map((requestUrl) => requestUrl.searchParams.get('status')),
|
||||||
|
['3']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper retires automatic free reuse record at max uses', async () => {
|
||||||
|
const requests = [];
|
||||||
|
let statusPollCount = 0;
|
||||||
|
let currentState = {
|
||||||
|
heroSmsApiKey: 'demo-key',
|
||||||
|
freePhoneReuseEnabled: true,
|
||||||
|
freePhoneReuseAutoEnabled: true,
|
||||||
|
verificationResendCount: 0,
|
||||||
|
currentPhoneActivation: null,
|
||||||
|
freeReusablePhoneActivation: {
|
||||||
|
activationId: 'free-max',
|
||||||
|
phoneNumber: '66950009999',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 52,
|
||||||
|
countryLabel: 'Thailand',
|
||||||
|
successfulUses: 2,
|
||||||
|
maxUses: 3,
|
||||||
|
source: 'free-manual-reuse',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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 id = parsedUrl.searchParams.get('id');
|
||||||
|
if (action === 'setStatus' && id === 'free-max') {
|
||||||
|
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
|
||||||
|
}
|
||||||
|
if (action === 'getStatus' && id === 'free-max') {
|
||||||
|
statusPollCount += 1;
|
||||||
|
return { ok: true, text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_CODE' : 'STATUS_OK:778899') };
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
|
||||||
|
},
|
||||||
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
|
getState: async () => ({ ...currentState }),
|
||||||
|
sendToContentScriptResilient: async (_source, message) => {
|
||||||
|
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||||
|
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||||
|
}
|
||||||
|
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||||
|
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async (updates) => {
|
||||||
|
currentState = { ...currentState, ...updates };
|
||||||
|
},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.completePhoneVerificationFlow(1, {
|
||||||
|
addPhonePage: true,
|
||||||
|
phoneVerificationPage: false,
|
||||||
|
url: 'https://auth.openai.com/add-phone',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(currentState.freeReusablePhoneActivation, null);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
requests.map((requestUrl) => requestUrl.searchParams.get('action')),
|
||||||
|
['setStatus', 'getStatus', 'getStatus']
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
requests
|
||||||
|
.filter((requestUrl) => requestUrl.searchParams.get('action') === 'setStatus')
|
||||||
|
.map((requestUrl) => requestUrl.searchParams.get('status')),
|
||||||
|
['3']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper preserves coded free-reuse activation when code submission errors', async () => {
|
||||||
|
const requests = [];
|
||||||
|
let currentState = {
|
||||||
|
heroSmsApiKey: 'demo-key',
|
||||||
|
heroSmsCountryId: 52,
|
||||||
|
heroSmsCountryLabel: 'Thailand',
|
||||||
|
freePhoneReuseEnabled: true,
|
||||||
|
verificationResendCount: 0,
|
||||||
|
currentPhoneActivation: null,
|
||||||
|
freeReusablePhoneActivation: null,
|
||||||
|
reusablePhoneActivation: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 id = parsedUrl.searchParams.get('id');
|
||||||
|
const status = parsedUrl.searchParams.get('status');
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
return { ok: true, text: async () => buildHeroSmsPricesPayload() };
|
||||||
|
}
|
||||||
|
if (action === 'getNumber') {
|
||||||
|
return { ok: true, text: async () => 'ACCESS_NUMBER:new-free-save:66950006666' };
|
||||||
|
}
|
||||||
|
if (action === 'getStatus' && id === 'new-free-save') {
|
||||||
|
return { ok: true, text: async () => 'STATUS_OK:445566' };
|
||||||
|
}
|
||||||
|
if (action === 'setStatus' && id === 'new-free-save' && status === '8') {
|
||||||
|
throw new Error('free-reuse activation should not be cancelled after SMS code was received');
|
||||||
|
}
|
||||||
|
if (action === 'setStatus' && id === 'new-free-save' && status === '6') {
|
||||||
|
throw new Error('free-reuse activation should not be completed after SMS code was received');
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}:${status || ''}`);
|
||||||
|
},
|
||||||
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
|
getState: async () => ({ ...currentState }),
|
||||||
|
sendToContentScriptResilient: async (_source, message) => {
|
||||||
|
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||||
|
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||||
|
}
|
||||||
|
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||||
|
throw new Error('simulated auth-page failure after code submit');
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async (updates) => {
|
||||||
|
currentState = { ...currentState, ...updates };
|
||||||
|
},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
helpers.completePhoneVerificationFlow(1, {
|
||||||
|
addPhonePage: true,
|
||||||
|
phoneVerificationPage: false,
|
||||||
|
url: 'https://auth.openai.com/add-phone',
|
||||||
|
}),
|
||||||
|
/simulated auth-page failure/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(currentState.freeReusablePhoneActivation.activationId, 'new-free-save');
|
||||||
|
assert.equal(currentState.freeReusablePhoneActivation.phoneNumber, '66950006666');
|
||||||
|
assert.equal(
|
||||||
|
requests.some((requestUrl) => (
|
||||||
|
requestUrl.searchParams.get('action') === 'setStatus'
|
||||||
|
&& requestUrl.searchParams.get('id') === 'new-free-save'
|
||||||
|
&& ['6', '8'].includes(requestUrl.searchParams.get('status'))
|
||||||
|
)),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
requests
|
||||||
|
.filter((requestUrl) => (
|
||||||
|
requestUrl.searchParams.get('action') === 'setStatus'
|
||||||
|
&& requestUrl.searchParams.get('id') === 'new-free-save'
|
||||||
|
))
|
||||||
|
.map((requestUrl) => requestUrl.searchParams.get('status')),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper preserves newly saved free-reuse activation after OAuth success without source marker', async () => {
|
||||||
|
const requests = [];
|
||||||
|
let currentState = {
|
||||||
|
heroSmsApiKey: 'demo-key',
|
||||||
|
heroSmsCountryId: 52,
|
||||||
|
heroSmsCountryLabel: 'Thailand',
|
||||||
|
freePhoneReuseEnabled: true,
|
||||||
|
verificationResendCount: 0,
|
||||||
|
currentPhoneActivation: null,
|
||||||
|
freeReusablePhoneActivation: null,
|
||||||
|
reusablePhoneActivation: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 id = parsedUrl.searchParams.get('id');
|
||||||
|
const status = parsedUrl.searchParams.get('status');
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
return { ok: true, text: async () => buildHeroSmsPricesPayload() };
|
||||||
|
}
|
||||||
|
if (action === 'getNumber') {
|
||||||
|
return { ok: true, text: async () => 'ACCESS_NUMBER:new-free-success:66950007777' };
|
||||||
|
}
|
||||||
|
if (action === 'getStatus' && id === 'new-free-success') {
|
||||||
|
return { ok: true, text: async () => 'STATUS_OK:778899' };
|
||||||
|
}
|
||||||
|
if (action === 'setStatus' && id === 'new-free-success' && ['6', '8'].includes(status)) {
|
||||||
|
throw new Error(`free-reuse activation should not receive terminal setStatus(${status}) after OAuth success`);
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}:${status || ''}`);
|
||||||
|
},
|
||||||
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
|
getState: async () => ({ ...currentState }),
|
||||||
|
sendToContentScriptResilient: async (_source, message) => {
|
||||||
|
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||||
|
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||||
|
}
|
||||||
|
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||||
|
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async (updates) => {
|
||||||
|
currentState = { ...currentState, ...updates };
|
||||||
|
},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||||
|
addPhonePage: true,
|
||||||
|
phoneVerificationPage: false,
|
||||||
|
url: 'https://auth.openai.com/add-phone',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result, {
|
||||||
|
success: true,
|
||||||
|
consentReady: true,
|
||||||
|
url: 'https://auth.openai.com/authorize',
|
||||||
|
});
|
||||||
|
assert.equal(currentState.freeReusablePhoneActivation.activationId, 'new-free-success');
|
||||||
|
assert.equal(currentState.freeReusablePhoneActivation.phoneNumber, '66950007777');
|
||||||
|
assert.equal(currentState.freeReusablePhoneActivation.source, 'free-manual-reuse');
|
||||||
|
assert.equal(
|
||||||
|
requests.some((requestUrl) => (
|
||||||
|
requestUrl.searchParams.get('action') === 'setStatus'
|
||||||
|
&& requestUrl.searchParams.get('id') === 'new-free-success'
|
||||||
|
&& ['6', '8'].includes(requestUrl.searchParams.get('status'))
|
||||||
|
)),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('phone verification helper replaces number immediately when resend is throttled and does not spam resend clicks', async () => {
|
test('phone verification helper replaces number immediately when resend is throttled and does not spam resend clicks', async () => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
const messages = [];
|
const messages = [];
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ const inputAutoSkipFailures = { disabled: false };
|
|||||||
const autoContinueBar = { style: { display: '' } };
|
const autoContinueBar = { style: { display: '' } };
|
||||||
|
|
||||||
function setSettingsCardLocked() {}
|
function setSettingsCardLocked() {}
|
||||||
|
function setFreePhoneReuseControlsLocked() {}
|
||||||
function getAutoRunLabel() { return ''; }
|
function getAutoRunLabel() { return ''; }
|
||||||
function getLockedRunCountFromEmailPool() { return lockedRunCount; }
|
function getLockedRunCountFromEmailPool() { return lockedRunCount; }
|
||||||
function isCustomMailProvider() { return false; }
|
function isCustomMailProvider() { return false; }
|
||||||
@@ -198,6 +199,7 @@ const inputAutoSkipFailures = { disabled: false };
|
|||||||
const autoContinueBar = { style: { display: '' } };
|
const autoContinueBar = { style: { display: '' } };
|
||||||
|
|
||||||
function setSettingsCardLocked() {}
|
function setSettingsCardLocked() {}
|
||||||
|
function setFreePhoneReuseControlsLocked() {}
|
||||||
function getAutoRunLabel() { return ''; }
|
function getAutoRunLabel() { return ''; }
|
||||||
function getLockedRunCountFromEmailPool() { return 0; }
|
function getLockedRunCountFromEmailPool() { return 0; }
|
||||||
function isCustomMailProvider() { return false; }
|
function isCustomMailProvider() { return false; }
|
||||||
|
|||||||
@@ -108,6 +108,18 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
|
|||||||
assert.match(html, /id="row-hero-sms-current-code"/);
|
assert.match(html, /id="row-hero-sms-current-code"/);
|
||||||
assert.match(html, /id="row-hero-sms-preferred-activation"/);
|
assert.match(html, /id="row-hero-sms-preferred-activation"/);
|
||||||
assert.match(html, /id="select-hero-sms-preferred-activation"/);
|
assert.match(html, /id="select-hero-sms-preferred-activation"/);
|
||||||
|
assert.match(html, /id="row-free-phone-reuse-enabled"/);
|
||||||
|
assert.match(html, /id="input-free-phone-reuse-enabled"/);
|
||||||
|
assert.match(html, /id="row-free-phone-reuse-auto-enabled"/);
|
||||||
|
assert.match(html, /id="input-free-phone-reuse-auto-enabled"/);
|
||||||
|
assert.match(html, /id="row-free-reusable-phone"/);
|
||||||
|
assert.match(html, /id="display-free-reusable-phone"/);
|
||||||
|
assert.match(html, /id="display-free-reusable-phone-country"/);
|
||||||
|
assert.match(html, /id="input-free-reusable-phone"/);
|
||||||
|
assert.match(html, /id="btn-save-free-reusable-phone"/);
|
||||||
|
assert.match(html, /id="btn-clear-free-reusable-phone"/);
|
||||||
|
assert.match(html, /白嫖复用/);
|
||||||
|
assert.match(html, /自动白嫖复用/);
|
||||||
assert.match(html, /id="row-phone-replacement-limit"/);
|
assert.match(html, /id="row-phone-replacement-limit"/);
|
||||||
assert.match(html, /id="row-phone-verification-resend-count"/);
|
assert.match(html, /id="row-phone-verification-resend-count"/);
|
||||||
assert.match(html, /id="row-phone-code-wait-seconds"/);
|
assert.match(html, /id="row-phone-code-wait-seconds"/);
|
||||||
@@ -134,6 +146,49 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
|
|||||||
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
|
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sidepanel source wires free reusable phone save and clear actions to runtime messages', () => {
|
||||||
|
assert.match(sidepanelSource, /const inputFreePhoneReuseEnabled = document\.getElementById\('input-free-phone-reuse-enabled'\);/);
|
||||||
|
assert.match(sidepanelSource, /const inputFreePhoneReuseAutoEnabled = document\.getElementById\('input-free-phone-reuse-auto-enabled'\);/);
|
||||||
|
assert.match(sidepanelSource, /const displayFreeReusablePhone = document\.getElementById\('display-free-reusable-phone'\);/);
|
||||||
|
assert.match(sidepanelSource, /const inputFreeReusablePhone = document\.getElementById\('input-free-reusable-phone'\);/);
|
||||||
|
assert.match(sidepanelSource, /const btnSaveFreeReusablePhone = document\.getElementById\('btn-save-free-reusable-phone'\);/);
|
||||||
|
assert.match(sidepanelSource, /const btnClearFreeReusablePhone = document\.getElementById\('btn-clear-free-reusable-phone'\);/);
|
||||||
|
assert.match(sidepanelSource, /type:\s*'SET_FREE_REUSABLE_PHONE'/);
|
||||||
|
assert.match(sidepanelSource, /payload:\s*\{\s*phoneNumber\s*\}/s);
|
||||||
|
assert.match(sidepanelSource, /type:\s*'CLEAR_FREE_REUSABLE_PHONE'/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sidepanel keeps free reuse switches realtime and locks them during auto run', () => {
|
||||||
|
assert.match(
|
||||||
|
sidepanelSource,
|
||||||
|
/message\.payload\.freePhoneReuseEnabled !== undefined[\s\S]*updatePhoneVerificationSettingsUI\(\);/
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
sidepanelSource,
|
||||||
|
/message\.payload\.freePhoneReuseAutoEnabled !== undefined[\s\S]*updatePhoneVerificationSettingsUI\(\);/
|
||||||
|
);
|
||||||
|
assert.match(sidepanelSource, /setFreePhoneReuseControlsLocked\(settingsCardLocked\);/);
|
||||||
|
assert.match(
|
||||||
|
sidepanelSource,
|
||||||
|
/inputFreePhoneReuseEnabled\.disabled = locked;[\s\S]*inputFreePhoneReuseAutoEnabled\.disabled = locked/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sidepanel free reusable phone paths avoid stale identifiers and empty-save errors', () => {
|
||||||
|
assert.doesNotMatch(
|
||||||
|
sidepanelSource,
|
||||||
|
/applyHeroSmsFallbackSelection\(\s*\[\.\.\.nextPrimaryCountries,\s*\.\.\.nextFallback\]/
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
sidepanelSource,
|
||||||
|
/applyHeroSmsFallbackSelection\(\s*\[\s*nextPrimary,\s*\.\.\.nextFallback\]/
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
sidepanelSource,
|
||||||
|
/if \(!phoneNumber\) \{[\s\S]*请先填写白嫖复用手机号[\s\S]*return;[\s\S]*chrome\.runtime\.sendMessage\(\{\s*type:\s*'SET_FREE_REUSABLE_PHONE'/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('sidepanel source wires runtime signup phone field to background sync messages', () => {
|
test('sidepanel source wires runtime signup phone field to background sync messages', () => {
|
||||||
assert.match(sidepanelSource, /function getRuntimeSignupPhoneValue\(state = latestState\)/);
|
assert.match(sidepanelSource, /function getRuntimeSignupPhoneValue\(state = latestState\)/);
|
||||||
assert.match(sidepanelSource, /function shouldExecuteStep3WithSignupPhoneIdentity\(state = latestState\)/);
|
assert.match(sidepanelSource, /function shouldExecuteStep3WithSignupPhoneIdentity\(state = latestState\)/);
|
||||||
@@ -494,6 +549,9 @@ function updateSignupMethodUI() {
|
|||||||
function syncSignupPhoneInputFromState() {
|
function syncSignupPhoneInputFromState() {
|
||||||
rowSignupPhone.style.display = inputPhoneVerificationEnabled.checked && latestState.signupPhoneNumber ? '' : 'none';
|
rowSignupPhone.style.display = inputPhoneVerificationEnabled.checked && latestState.signupPhoneNumber ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
function setFreePhoneReuseControlsLocked() {}
|
||||||
|
function isAutoRunLockedPhase() { return false; }
|
||||||
|
function isAutoRunScheduledPhase() { return false; }
|
||||||
|
|
||||||
${extractFunction('updatePhoneVerificationSettingsUI')}
|
${extractFunction('updatePhoneVerificationSettingsUI')}
|
||||||
|
|
||||||
@@ -686,6 +744,8 @@ const inputAutoDelayEnabled = { checked: false };
|
|||||||
const inputAutoDelayMinutes = { value: '30' };
|
const inputAutoDelayMinutes = { value: '30' };
|
||||||
const inputAutoStepDelaySeconds = { value: '' };
|
const inputAutoStepDelaySeconds = { value: '' };
|
||||||
const inputPhoneVerificationEnabled = { checked: true };
|
const inputPhoneVerificationEnabled = { checked: true };
|
||||||
|
const inputFreePhoneReuseEnabled = { checked: true };
|
||||||
|
const inputFreePhoneReuseAutoEnabled = { checked: true };
|
||||||
const selectPhoneSmsProvider = { value: 'hero-sms' };
|
const selectPhoneSmsProvider = { value: 'hero-sms' };
|
||||||
const inputVerificationResendCount = { value: '4' };
|
const inputVerificationResendCount = { value: '4' };
|
||||||
const inputHeroSmsApiKey = { value: 'demo-key' };
|
const inputHeroSmsApiKey = { value: 'demo-key' };
|
||||||
@@ -832,6 +892,8 @@ return { collectSettingsPayload };
|
|||||||
assert.deepStrictEqual(payload.nexSmsCountryOrder, [1]);
|
assert.deepStrictEqual(payload.nexSmsCountryOrder, [1]);
|
||||||
assert.equal(payload.nexSmsServiceCode, 'ot');
|
assert.equal(payload.nexSmsServiceCode, 'ot');
|
||||||
assert.equal(payload.heroSmsReuseEnabled, true);
|
assert.equal(payload.heroSmsReuseEnabled, true);
|
||||||
|
assert.equal(payload.freePhoneReuseEnabled, true);
|
||||||
|
assert.equal(payload.freePhoneReuseAutoEnabled, true);
|
||||||
assert.equal(payload.heroSmsAcquirePriority, 'price');
|
assert.equal(payload.heroSmsAcquirePriority, 'price');
|
||||||
assert.equal(payload.heroSmsMaxPrice, '0.12');
|
assert.equal(payload.heroSmsMaxPrice, '0.12');
|
||||||
assert.equal(payload.heroSmsPreferredPrice, '0.0512');
|
assert.equal(payload.heroSmsPreferredPrice, '0.0512');
|
||||||
|
|||||||
Reference in New Issue
Block a user