diff --git a/background.js b/background.js
index 4e3c3df..cb414ab 100644
--- a/background.js
+++ b/background.js
@@ -242,6 +242,7 @@ const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
const DEFAULT_GPC_HELPER_API_URL = 'https://gopay.hwork.pro';
const DEFAULT_SUB2API_GROUP_NAME = 'codex';
const DEFAULT_SUB2API_PROXY_NAME = '';
+const DEFAULT_SUB2API_ACCOUNT_PRIORITY = 1;
const CONTRIBUTION_SOURCE_CPA = 'cpa';
const CONTRIBUTION_SOURCE_SUB2API = 'sub2api';
const CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME = 'codex号池';
@@ -574,6 +575,7 @@ const PERSISTED_SETTING_DEFAULTS = {
sub2apiPassword: '',
sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME,
sub2apiGroupNames: DEFAULT_SUB2API_GROUP_NAMES,
+ sub2apiAccountPriority: DEFAULT_SUB2API_ACCOUNT_PRIORITY,
sub2apiDefaultProxyName: DEFAULT_SUB2API_PROXY_NAME,
ipProxyEnabled: false,
ipProxyService: DEFAULT_IP_PROXY_SERVICE,
@@ -632,6 +634,8 @@ const PERSISTED_SETTING_DEFAULTS = {
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
phoneVerificationEnabled: false,
+ freePhoneReuseEnabled: true,
+ freePhoneReuseAutoEnabled: true,
signupMethod: DEFAULT_SIGNUP_METHOD,
phoneSmsProvider: DEFAULT_PHONE_SMS_PROVIDER,
phoneSmsProviderOrder: [],
@@ -798,6 +802,7 @@ const DEFAULT_STATE = {
currentPhoneVerificationCountdownWindowIndex: 0,
currentPhoneVerificationCountdownWindowTotal: 0,
reusablePhoneActivation: null,
+ freeReusablePhoneActivation: null,
phoneReusableActivationPool: [],
signupPhoneNumber: '',
signupPhoneActivation: null,
@@ -2105,6 +2110,18 @@ function normalizeSub2ApiGroupNames(value = '') {
return names;
}
+function normalizeSub2ApiAccountPriority(value, fallback = DEFAULT_SUB2API_ACCOUNT_PRIORITY) {
+ const rawValue = String(value ?? '').trim();
+ const numeric = Number(rawValue);
+ if (!rawValue || !Number.isSafeInteger(numeric) || numeric < 1) {
+ const fallbackNumber = Number(fallback);
+ return Number.isSafeInteger(fallbackNumber) && fallbackNumber >= 1
+ ? fallbackNumber
+ : DEFAULT_SUB2API_ACCOUNT_PRIORITY;
+ }
+ return numeric;
+}
+
function normalizePersistentSettingValue(key, value) {
switch (key) {
case 'panelMode':
@@ -2125,6 +2142,8 @@ function normalizePersistentSettingValue(key, value) {
return String(value || '').trim();
case 'sub2apiGroupNames':
return normalizeSub2ApiGroupNames(value);
+ case 'sub2apiAccountPriority':
+ return normalizeSub2ApiAccountPriority(value);
case 'sub2apiDefaultProxyName':
return String(value || '').trim();
case 'ipProxyEnabled':
@@ -2286,6 +2305,8 @@ function normalizePersistentSettingValue(key, value) {
case 'gopayHelperLocalSmsHelperEnabled':
case 'autoRunDelayEnabled':
case 'phoneVerificationEnabled':
+ case 'freePhoneReuseEnabled':
+ case 'freePhoneReuseAutoEnabled':
case 'plusModeEnabled':
return Boolean(value);
case 'phoneSmsProvider':
@@ -3093,6 +3114,7 @@ async function resetState() {
'tabRegistry',
'sourceLastUrls',
'reusablePhoneActivation',
+ 'freeReusablePhoneActivation',
'phoneReusableActivationPool',
'luckmailApiKey',
'luckmailBaseUrl',
@@ -3132,6 +3154,19 @@ async function resetState() {
.map((entry) => normalizePhonePreferredActivation(entry))
.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.set({
...DEFAULT_STATE,
@@ -3154,6 +3189,8 @@ async function resetState() {
currentLuckmailMailCursor: null,
// Keep reusable phone activation across round resets so the same number can be reactivated up to maxUses.
reusablePhoneActivation,
+ // Keep free reuse phone activation until the user clears or the flow retires it.
+ freeReusablePhoneActivation,
phoneReusableActivationPool,
preferredIcloudHost: prev.preferredIcloudHost || '',
});
@@ -5448,7 +5485,6 @@ async function withIcloudLoginHelp(actionLabel, action) {
if (shouldEmitIcloudTransientLog(`${safeActionLabel}:final`)) {
await addLog(`iCloud:${safeActionLabel}受网络/上下文波动影响:${getErrorMessage(err)}`, 'warn');
}
- const safeActionLabel = String(actionLabel || '操作').trim() || '操作';
const transientError = new Error(`iCloud:${safeActionLabel}受网络/上下文波动影响,请稍后重试。`);
transientError.code = 'ICLOUD_TRANSIENT_CONTEXT';
transientError.actionLabel = safeActionLabel;
@@ -6613,6 +6649,53 @@ async function finalizePhoneActivationAfterSuccessfulFlow(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
// ============================================================
@@ -10689,6 +10772,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
clearAccountRunHistory: (...args) => clearAndBroadcastAccountRunHistory(...args),
deleteAccountRunHistoryRecords: (...args) => deleteAndBroadcastAccountRunHistoryRecords(...args),
clearAutoRunTimerAlarm,
+ clearFreeReusablePhoneActivation,
clearLuckmailRuntimeState,
clearStopRequest,
closeLocalhostCallbackTabs,
@@ -10777,6 +10861,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
setContributionMode,
setEmailState,
setEmailStateSilently,
+ setFreeReusablePhoneActivation,
setSignupPhoneState,
setSignupPhoneStateSilently,
setIcloudAliasPreservedState,
diff --git a/background/message-router.js b/background/message-router.js
index 7121e48..9960bde 100644
--- a/background/message-router.js
+++ b/background/message-router.js
@@ -16,6 +16,7 @@
clearAccountRunHistory,
deleteAccountRunHistoryRecords,
clearAutoRunTimerAlarm,
+ clearFreeReusablePhoneActivation,
clearLuckmailRuntimeState,
clearStopRequest,
closeLocalhostCallbackTabs,
@@ -103,6 +104,7 @@
setContributionMode,
setEmailState,
setEmailStateSilently,
+ setFreeReusablePhoneActivation,
setSignupPhoneState,
setSignupPhoneStateSilently,
setIcloudAliasPreservedState,
@@ -684,6 +686,20 @@
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': {
const enabled = Boolean(message.payload?.enabled);
const state = await ensureManualInteractionAllowed(enabled ? '进入贡献模式' : '退出贡献模式');
@@ -991,6 +1007,9 @@
if (Boolean(currentState?.contributionMode) && typeof setContributionMode === 'function') {
await setContributionMode(true);
}
+ if (Object.keys(stateUpdates).length > 0 && typeof broadcastDataUpdate === 'function') {
+ broadcastDataUpdate(stateUpdates);
+ }
if (modeChanged) {
const selectedPlusPaymentMethod = getPlusPaymentMethodLabel(
stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal'
diff --git a/background/panel-bridge.js b/background/panel-bridge.js
index 2b0f077..a4dff4c 100644
--- a/background/panel-bridge.js
+++ b/background/panel-bridge.js
@@ -293,6 +293,7 @@
sub2apiPassword: state.sub2apiPassword,
sub2apiGroupName: groupName,
sub2apiDefaultProxyName: state.sub2apiDefaultProxyName,
+ sub2apiAccountPriority: state.sub2apiAccountPriority,
logStep: 7,
},
}, {
diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js
index f497774..429e990 100644
--- a/background/phone-verification-flow.js
+++ b/background/phone-verification-flow.js
@@ -10,6 +10,7 @@
generateRandomName,
getOAuthFlowStepTimeoutMs,
getState,
+ requestStop = null,
sendToContentScript,
sendToContentScriptResilient,
setState,
@@ -40,6 +41,7 @@
const PHONE_VERIFICATION_CODE_STATE_KEY = 'currentPhoneVerificationCode';
const REUSABLE_PHONE_ACTIVATION_STATE_KEY = 'reusablePhoneActivation';
const REUSABLE_PHONE_ACTIVATION_POOL_STATE_KEY = 'phoneReusableActivationPool';
+ const FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY = 'freeReusablePhoneActivation';
const PREFERRED_PHONE_ACTIVATION_STATE_KEY = 'phonePreferredActivation';
const PHONE_RUNTIME_COUNTDOWN_ENDS_AT_KEY = 'currentPhoneVerificationCountdownEndsAt';
const PHONE_RUNTIME_COUNTDOWN_WINDOW_INDEX_KEY = 'currentPhoneVerificationCountdownWindowIndex';
@@ -89,7 +91,13 @@
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::';
const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::';
+ const PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX = 'PHONE_RESEND_BANNED_NUMBER::';
const PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX = 'PHONE_ROUTE_405_RECOVERY_FAILED::';
+ const PHONE_MANUAL_FREE_REUSE_ERROR_PREFIX = 'PHONE_MANUAL_FREE_REUSE::';
+ const PHONE_AUTO_FREE_REUSE_PREPARE_ERROR_PREFIX = 'PHONE_AUTO_FREE_REUSE_PREPARE::';
+ const FREE_PHONE_REUSE_PREPARE_TIMEOUT_MS = 20000;
+ const FREE_PHONE_REUSE_PREPARE_INTERVAL_MS = 2000;
+ const FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS = 10;
const PHONE_SMS_FAILURE_SKIP_THRESHOLD = 2;
const MAX_ACTIVATION_PRICE_HINTS = 256;
const activationPriceHintsByKey = new Map();
@@ -292,6 +300,24 @@
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) {
const numeric = Number(value);
if (Number.isFinite(numeric) && numeric > 0) {
@@ -435,6 +461,15 @@
return Boolean(value);
}
+ function normalizeFreePhoneReuseEnabled(value) {
+ return Boolean(value);
+ }
+
+ function normalizeFreePhoneReuseAutoEnabled(state = {}) {
+ return normalizeFreePhoneReuseEnabled(state?.freePhoneReuseEnabled)
+ && Boolean(state?.freePhoneReuseAutoEnabled);
+ }
+
function normalizeHeroSmsAcquirePriority(value = '') {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === HERO_SMS_ACQUIRE_PRIORITY_PRICE) {
@@ -983,6 +1018,66 @@
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)),
...(expiresAt > 0 ? { expiresAt } : {}),
...(statusAction ? { statusAction } : {}),
+ ...(record.source ? { source: String(record.source || '').trim() } : {}),
+ ...(record.phoneCodeReceived ? { phoneCodeReceived: true } : {}),
+ ...(record.phoneCodeReceivedAt ? { phoneCodeReceivedAt: Math.max(0, Number(record.phoneCodeReceivedAt) || 0) } : {}),
+ };
+ }
+
+ 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) {
if (!record || typeof record !== 'object' || Array.isArray(record)) {
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);
}
+ 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) {
const message = String(error?.message || error || '').trim();
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) {
const normalizedFallback = normalizeActivation(fallback) || normalizeActivationFallback(fallback);
const directActivation = normalizeActivation(payload);
@@ -3220,16 +3368,28 @@
if (!normalizedActivation) {
return '';
}
+ const normalizedStatus = Math.floor(Number(status) || 0);
+ if (
+ (normalizedStatus === 6 || normalizedStatus === 8)
+ && shouldSkipTerminalStatusForFreeReuse(state, normalizedActivation)
+ ) {
+ const identifier = normalizedActivation.phoneNumber || normalizedActivation.activationId || 'current activation';
+ await addLog(
+ `步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的 setStatus(${normalizedStatus})。`,
+ 'info'
+ );
+ return `free reuse setStatus(${normalizedStatus}) skipped`;
+ }
const config = resolvePhoneConfig(state);
if (config.provider === PHONE_SMS_PROVIDER_5SIM) {
- const endpoint = status === 6
+ const endpoint = normalizedStatus === 6
? `/user/finish/${normalizedActivation.activationId}`
: `/user/cancel/${normalizedActivation.activationId}`;
const payload = await fetchFiveSimPayload(config, endpoint, actionLabel || '5sim set status');
return describeFiveSimPayload(payload);
}
if (config.provider === PHONE_SMS_PROVIDER_NEXSMS) {
- if (status === 6) {
+ if (normalizedStatus === 6) {
return 'NexSMS complete skipped';
}
const payload = await fetchNexSmsPayload(
@@ -3251,12 +3411,21 @@
const payload = await fetchHeroSmsPayload(config, {
action: 'setStatus',
id: normalizedActivation.activationId,
- status,
+ status: normalizedStatus,
}, actionLabel);
return describeHeroSmsPayload(payload);
}
async function completePhoneActivation(state = {}, activation) {
+ if (shouldSkipTerminalStatusForFreeReuse(state, activation)) {
+ const normalizedActivation = normalizeActivation(activation);
+ const identifier = normalizedActivation?.phoneNumber || normalizedActivation?.activationId || 'current activation';
+ await addLog(
+ `步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的接码完成状态。`,
+ 'info'
+ );
+ return;
+ }
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
@@ -3269,6 +3438,15 @@
async function cancelPhoneActivation(state = {}, activation) {
try {
+ const normalizedActivation = normalizeActivation(activation);
+ if (shouldSkipTerminalStatusForFreeReuse(state, activation)) {
+ const identifier = normalizedActivation?.phoneNumber || normalizedActivation?.activationId || 'current activation';
+ await addLog(
+ `步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的接码取消状态。`,
+ 'info'
+ );
+ return;
+ }
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
@@ -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) {
try {
+ if (shouldSkipTerminalStatusForFreeReuse(state, activation)) {
+ const normalizedActivation = normalizeActivation(activation);
+ const identifier = normalizedActivation?.phoneNumber || normalizedActivation?.activationId || 'current activation';
+ await addLog(
+ `步骤 9:白嫖复用模式仅请求短信,跳过 ${identifier} 的接码封禁状态。`,
+ 'info'
+ );
+ return;
+ }
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
@@ -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 = {}) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
@@ -3777,6 +4137,71 @@
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) {
const normalizedActivation = normalizeActivation(activation);
const updates = {
@@ -3843,6 +4268,67 @@
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) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
@@ -4144,6 +4630,8 @@
...normalizedActivation,
successfulUses,
};
+ delete nextReusableActivation.phoneCodeReceived;
+ delete nextReusableActivation.phoneCodeReceivedAt;
await upsertReusableActivationPool(nextReusableActivation, { state });
if (!normalizeHeroSmsReuseEnabled(state?.heroSmsReuseEnabled)) {
await clearReusableActivation();
@@ -4158,6 +4646,119 @@
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) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
@@ -4191,6 +4792,24 @@
intervalMs: pollIntervalSeconds * 1000,
maxRounds: pollMaxRounds,
onStatus: async ({ elapsedMs, pollCount, statusText }) => {
+ if (/^STATUS_(WAIT_CODE|WAIT_RETRY|WAIT_RESEND)(?::.+)?$/i.test(String(statusText || '').trim())) {
+ const pageError = await checkPhoneResendPageError(tabId, state);
+ if (pageError?.reason === 'resend_phone_banned') {
+ throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${pageError.message || 'OpenAI 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 = (
pollCount === 1
|| statusText !== lastLoggedStatus
@@ -4214,6 +4833,50 @@
};
} catch (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)) {
await addLog(
`Step 9: ${providerLabel} activation for ${normalizedActivation.phoneNumber} became invalid (${error.message || error}), replacing number immediately.`,
@@ -4257,6 +4920,18 @@
if (isStopRequestedError(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)) {
await addLog(
`步骤 9:号码 ${normalizedActivation.phoneNumber} 重发短信被限流,立即更换号码。${resendError.message}`,
@@ -4266,7 +4941,9 @@
return {
code: '',
replaceNumber: true,
- reason: 'resend_throttled',
+ reason: shouldTreatResendThrottledAsBanned(state)
+ ? 'resend_throttled_high_risk_banned'
+ : 'resend_throttled',
};
}
await addLog(`步骤 9:点击手机验证码页面重发按钮失败。${resendError.message}`, 'warn');
@@ -5101,13 +5778,18 @@
);
}
if (!activation) {
- activation = await acquirePhoneActivation(state, {
- blockedCountryIds: getBlockedCountryIds(),
- countryPriceFloorByCountryId: getCountryPriceFloorById(),
- skipPreferredActivation: preferredActivationExhausted,
- });
- shouldCancelActivation = true;
- await persistCurrentActivation(activation);
+ activation = await handoffFreeReusablePhone(tabId, state);
+ if (activation) {
+ shouldCancelActivation = false;
+ } else {
+ activation = await acquirePhoneActivation(state, {
+ blockedCountryIds: getBlockedCountryIds(),
+ countryPriceFloorByCountryId: getCountryPriceFloorById(),
+ skipPreferredActivation: preferredActivationExhausted,
+ });
+ shouldCancelActivation = true;
+ await persistCurrentActivation(activation);
+ }
addPhoneReentryWithSameActivation = 0;
} else if (preferReuseExistingActivationOnAddPhone) {
addPhoneReentryWithSameActivation += 1;
@@ -5120,6 +5802,11 @@
`步骤 9:当前号码 ${activation.phoneNumber} 反复返回添加手机号页,正在更换号码(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`,
'warn'
);
+ if (isFreeAutoReuseActivation(activation)) {
+ await retireFreeReusableActivation(
+ `自动白嫖复用号码 ${activation.phoneNumber} 反复返回添加手机号页。`
+ );
+ }
if (shouldCancelActivation && activation) {
await cancelPhoneActivation(state, activation);
}
@@ -5170,6 +5857,16 @@
`步骤 9:添加手机号页面提示 ${activation.phoneNumber} 已被使用(${addPhoneRejectText}),正在更换号码(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`,
'warn'
);
+ await discardPhoneActivationFromReuse(
+ `目标站拒绝该号码(${addPhoneRejectText})。`,
+ activation,
+ await getState()
+ );
+ if (isFreeAutoReuseActivation(activation)) {
+ await retireFreeReusableActivation(
+ `自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。`
+ );
+ }
if (shouldCancelActivation && activation) {
await banPhoneActivation(state, activation);
}
@@ -5258,6 +5955,12 @@
await setPhoneRuntimeState({
[PHONE_VERIFICATION_CODE_STATE_KEY]: String(codeResult.code || '').trim(),
});
+ activation = markActivationPhoneCodeReceived(activation) || activation;
+ await persistCurrentActivation(activation);
+ await setPhoneRuntimeState({
+ [PHONE_VERIFICATION_CODE_STATE_KEY]: String(codeResult.code || '').trim(),
+ });
+ await markFreeReusableActivationAfterCode(state, activation);
await addLog(`步骤 9:已收到手机验证码 ${codeResult.code}。`, 'info');
const submitResult = await submitPhoneVerificationCode(tabId, codeResult.code);
@@ -5281,6 +5984,16 @@
if (isPhoneNumberUsedError(invalidErrorText)) {
shouldReplaceNumber = true;
replaceReason = 'phone_number_used';
+ await discardPhoneActivationFromReuse(
+ `目标站拒绝该号码(${invalidErrorText})。`,
+ activation,
+ await getState()
+ );
+ if (isFreeAutoReuseActivation(activation)) {
+ await retireFreeReusableActivation(
+ `自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。`
+ );
+ }
if (shouldCancelActivation && activation) {
await banPhoneActivation(state, activation);
shouldCancelActivation = false;
@@ -5331,8 +6044,19 @@
continue;
}
- await completePhoneActivation(state, activation);
- await markActivationReusableAfterSuccess(state, activation);
+ const latestSuccessState = await getState();
+ 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);
shouldCancelActivation = false;
await clearCurrentActivation();
@@ -5372,6 +6096,18 @@
if (shouldCancelActivation && activation) {
await cancelPhoneActivation(state, activation);
}
+ if (isFreeAutoReuseActivation(activation)) {
+ await retireFreeReusableActivation(
+ `自动白嫖复用号码 ${activation.phoneNumber} 在失败后被更换。`
+ );
+ }
+ if (isPhoneNumberUsedError(replaceReason)) {
+ await discardPhoneActivationFromReuse(
+ `目标站拒绝该号码(${replaceReason})。`,
+ activation,
+ await getState()
+ );
+ }
await clearCurrentActivation();
activation = null;
shouldCancelActivation = false;
@@ -5430,8 +6166,20 @@
};
}
} 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) {
- await cancelPhoneActivation(state, activation);
+ await cancelPhoneActivation(await getState(), activation);
}
await clearCurrentActivation();
throw sanitizePhoneRestartStep7Error(sanitizePhoneCodeTimeoutError(error));
diff --git a/background/steps/fill-plus-checkout.js b/background/steps/fill-plus-checkout.js
index 89133ec..78168f2 100644
--- a/background/steps/fill-plus-checkout.js
+++ b/background/steps/fill-plus-checkout.js
@@ -423,8 +423,7 @@
}
await addLog(`步骤 7:GPC 模式开始 OTP 验证(reference_id: ${referenceId})...`, 'info');
let otp = '';
- const useLocalSmsHelper = Boolean(state?.gopayHelperLocalSmsHelperEnabled)
- && normalizeGpcOtpChannel(state?.gopayHelperOtpChannel) === 'sms';
+ const useLocalSmsHelper = Boolean(state?.gopayHelperLocalSmsHelperEnabled);
if (useLocalSmsHelper) {
try {
await addLog('步骤 7:正在从本地 SMS Helper 等待 GPC OTP...', 'info');
diff --git a/background/steps/platform-verify.js b/background/steps/platform-verify.js
index 8237339..e9fb06c 100644
--- a/background/steps/platform-verify.js
+++ b/background/steps/platform-verify.js
@@ -380,6 +380,7 @@
sub2apiPassword: state.sub2apiPassword,
sub2apiGroupName: state.sub2apiGroupName,
sub2apiDefaultProxyName: state.sub2apiDefaultProxyName,
+ sub2apiAccountPriority: state.sub2apiAccountPriority,
sub2apiProxyId: state.sub2apiProxyId,
sub2apiSessionId: state.sub2apiSessionId,
sub2apiOAuthState: state.sub2apiOAuthState,
diff --git a/content/icloud-mail.js b/content/icloud-mail.js
index e6befa7..8f434ad 100644
--- a/content/icloud-mail.js
+++ b/content/icloud-mail.js
@@ -307,9 +307,13 @@ if (shouldHandlePollEmailInCurrentFrame) {
const items = collectThreadItems();
const useFallback = (fallbackCarry + attempt) > FALLBACK_AFTER;
- for (const item of items) {
+ for (const [index, item] of items.entries()) {
const signature = buildItemSignature(item);
- if (!useFallback && existingSignatures.has(signature)) {
+ const allowInitialStep8VisibleCode = Number(step) === 8
+ && attempt === 1
+ && index === 0
+ && !sessionBaseline.fromCache;
+ if (!useFallback && existingSignatures.has(signature) && !allowInitialStep8VisibleCode) {
continue;
}
diff --git a/content/phone-auth.js b/content/phone-auth.js
index 84804aa..be79514 100644
--- a/content/phone-auth.js
+++ b/content/phone-auth.js
@@ -19,11 +19,14 @@
waitForElement,
} = deps;
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_COOLDOWN_MS = 6000;
const PHONE_RESEND_ROUTE_405_MAX_RECOVERIES = 2;
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_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_MAX_RECOVERY_CLICKS = 3;
const rootScope = typeof self !== 'undefined' ? self : globalThis;
@@ -493,6 +496,63 @@
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 = {}) {
const { allowDisabled = false } = options;
const direct = document.querySelector('button[data-dd-action-name="Try again"]');
@@ -779,6 +839,10 @@
await recoverRoute405WithinResend();
continue;
}
+ const bannedNumberText = getPhoneResendBannedNumberText();
+ if (bannedNumberText) {
+ throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${bannedNumberText}`);
+ }
const throttledText = getPhoneResendThrottleText();
if (throttledText) {
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${throttledText}`);
@@ -792,6 +856,10 @@
await recoverRoute405WithinResend();
continue;
}
+ const afterClickBannedNumberText = getPhoneResendBannedNumberText();
+ if (afterClickBannedNumberText) {
+ throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${afterClickBannedNumberText}`);
+ }
const afterClickThrottleText = getPhoneResendThrottleText();
if (afterClickThrottleText) {
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${afterClickThrottleText}`);
@@ -804,6 +872,11 @@
await sleep(250);
}
+ const timeoutBannedNumberText = getPhoneResendBannedNumberText();
+ if (timeoutBannedNumberText) {
+ throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${timeoutBannedNumberText}`);
+ }
+
const timeoutThrottleText = getPhoneResendThrottleText();
if (timeoutThrottleText) {
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${timeoutThrottleText}`);
@@ -839,6 +912,7 @@
return {
getPhoneVerificationDisplayedPhone,
+ checkPhoneResendError,
isPhoneVerificationPageReady,
resendPhoneVerificationCode,
returnToAddPhone,
diff --git a/content/signup-page.js b/content/signup-page.js
index 11d395a..797236a 100644
--- a/content/signup-page.js
+++ b/content/signup-page.js
@@ -25,6 +25,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1'
|| message.type === 'SUBMIT_PHONE_NUMBER'
|| message.type === 'SUBMIT_PHONE_VERIFICATION_CODE'
|| message.type === 'RESEND_PHONE_VERIFICATION_CODE'
+ || message.type === 'CHECK_PHONE_RESEND_ERROR'
|| message.type === 'RETURN_TO_ADD_PHONE'
|| message.type === 'ENSURE_SIGNUP_ENTRY_READY'
|| message.type === 'ENSURE_SIGNUP_PHONE_ENTRY_READY'
@@ -97,6 +98,8 @@ async function handleCommand(message) {
return await submitPhoneVerificationCodeWithProfileFallback(message.payload);
case 'RESEND_PHONE_VERIFICATION_CODE':
return await phoneAuthHelpers.resendPhoneVerificationCode();
+ case 'CHECK_PHONE_RESEND_ERROR':
+ return phoneAuthHelpers.checkPhoneResendError();
case 'RETURN_TO_ADD_PHONE':
return await phoneAuthHelpers.returnToAddPhone();
case 'ENSURE_SIGNUP_ENTRY_READY':
@@ -2948,6 +2951,7 @@ const phoneAuthHelpers = self.MultiPagePhoneAuth?.createPhoneAuthHelpers?.({
resendPhoneVerificationCode: async () => {
throw new Error('Phone auth helpers are unavailable.');
},
+ checkPhoneResendError: () => ({ hasError: false, reason: '', message: '', url: location.href }),
returnToAddPhone: async () => {
throw new Error('Phone auth helpers are unavailable.');
},
diff --git a/content/sub2api-panel.js b/content/sub2api-panel.js
index 1bf82b7..8f5c39c 100644
--- a/content/sub2api-panel.js
+++ b/content/sub2api-panel.js
@@ -252,6 +252,21 @@ function resolveSub2ApiProxyPreference(payload = {}, backgroundState = {}) {
return SUB2API_DEFAULT_PROXY_NAME;
}
+function resolveSub2ApiAccountPriority(payload = {}, backgroundState = {}) {
+ const candidate = payload.sub2apiAccountPriority !== undefined
+ ? payload.sub2apiAccountPriority
+ : backgroundState.sub2apiAccountPriority;
+ const rawValue = String(candidate ?? '').trim();
+ if (!rawValue) {
+ return SUB2API_DEFAULT_PRIORITY;
+ }
+ const numeric = Number(rawValue);
+ if (!Number.isSafeInteger(numeric) || numeric < 1) {
+ throw new Error('SUB2API 账号优先级必须是大于等于 1 的整数。');
+ }
+ return numeric;
+}
+
function normalizeProxyId(value) {
if (value === undefined || value === null || value === '') {
return null;
@@ -568,6 +583,7 @@ async function step9_submitOpenAiCallback(payload = {}) {
const proxySelector = preferredProxyId || proxyPreference;
const proxy = proxySelector ? await resolveSub2ApiProxy(origin, token, proxySelector) : null;
const proxyId = normalizeProxyId(proxy?.id);
+ const accountPriority = resolveSub2ApiAccountPriority(payload, backgroundState);
const storedGroupIds = Array.isArray(payload.sub2apiGroupIds)
? payload.sub2apiGroupIds
: (Array.isArray(backgroundState.sub2apiGroupIds) ? backgroundState.sub2apiGroupIds : []);
@@ -627,7 +643,7 @@ async function step9_submitOpenAiCallback(payload = {}) {
type: 'oauth',
credentials,
concurrency: SUB2API_DEFAULT_CONCURRENCY,
- priority: SUB2API_DEFAULT_PRIORITY,
+ priority: accountPriority,
rate_multiplier: SUB2API_DEFAULT_RATE_MULTIPLIER,
group_ids: groupIds,
auto_pause_on_expired: true,
diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css
index 6004558..2c4fc3f 100644
--- a/sidepanel/sidepanel.css
+++ b/sidepanel/sidepanel.css
@@ -2367,6 +2367,20 @@ header {
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-input {
width: 100%;
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 1584dd9..afe062d 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -210,6 +210,10 @@
+
+ 优先级
+
+
默认代理
@@ -323,17 +327,17 @@
- 本地短信
+ 本地 OTP
- 从本机 macOS Messages helper 读取 SMS OTP
+ 从本机 helper 读取当前通道 OTP,失败后回退手动输入
- 短信接口
+ OTP 接口
@@ -1467,6 +1471,28 @@
次
+
+
+
自动白嫖复用
+
+
+
+
@@ -1487,6 +1513,14 @@
验证码
未获取
+
+ 白嫖号码
+
+ 地区:未保存
+ 未保存
+
+
+
优先号码