chore: merge latest dev into pr 208 fixes

This commit is contained in:
QLHazyCoder
2026-05-07 02:26:08 +08:00
28 changed files with 2713 additions and 44 deletions
+86 -1
View File
@@ -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,
+19
View File
@@ -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'
+1
View File
@@ -293,6 +293,7 @@
sub2apiPassword: state.sub2apiPassword,
sub2apiGroupName: groupName,
sub2apiDefaultProxyName: state.sub2apiDefaultProxyName,
sub2apiAccountPriority: state.sub2apiAccountPriority,
logStep: 7,
},
}, {
+762 -14
View File
@@ -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(
`步骤 9OpenAI 无法向号码 ${normalizedActivation.phoneNumber} 发送短信,立即更换号码。${error.message}`,
'warn'
);
await clearPhoneRuntimeCountdown();
return {
code: '',
replaceNumber: true,
reason: 'resend_phone_banned',
};
}
if (isPhoneMaxUsageExceededFlowError(error)) {
await addLog(
`步骤 9OpenAI 提示号码 ${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(
`步骤 9OpenAI 无法向号码 ${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));
+1 -2
View File
@@ -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');
+1
View File
@@ -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,
+6 -2
View File
@@ -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;
}
+74
View File
@@ -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,
+4
View File
@@ -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.');
},
+17 -1
View File
@@ -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,
+14
View File
@@ -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%;
+37 -3
View File
@@ -210,6 +210,10 @@
<button id="btn-add-sub2api-group" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
</div>
</div>
<div class="data-row" id="row-sub2api-account-priority" style="display:none;">
<span class="data-label">优先级</span>
<input type="number" id="input-sub2api-account-priority" class="data-input mono" value="1" min="1" step="1" />
</div>
<div class="data-row" id="row-sub2api-default-proxy" style="display:none;">
<span class="data-label">默认代理</span>
<input type="text" id="input-sub2api-default-proxy" class="data-input" placeholder="留空则不使用代理;或填写代理名称 / ID" />
@@ -323,17 +327,17 @@
</select>
</div>
<div class="data-row" id="row-gpc-helper-local-sms-enabled" style="display:none;">
<span class="data-label">本地短信</span>
<span class="data-label">本地 OTP</span>
<label class="toggle-switch">
<input type="checkbox" id="input-gpc-helper-local-sms-enabled" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
</label>
<span class="setting-caption">从本机 macOS Messages helper 读取 SMS OTP</span>
<span class="setting-caption">从本机 helper 读取当前通道 OTP,失败后回退手动输入</span>
</div>
<div class="data-row" id="row-gpc-helper-local-sms-url" style="display:none;">
<span class="data-label">短信接口</span>
<span class="data-label">OTP 接口</span>
<input type="text" id="input-gpc-helper-local-sms-url" class="data-input"
placeholder="http://127.0.0.1:18767" autocomplete="off" />
</div>
@@ -1467,6 +1471,28 @@
<span class="data-unit"></span>
</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>
@@ -1487,6 +1513,14 @@
<span class="hero-sms-runtime-key">验证码</span>
<span id="display-hero-sms-current-code" class="data-value mono hero-sms-runtime-value">未获取</span>
</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;">
<span class="hero-sms-runtime-key">优先号码</span>
<select id="select-hero-sms-preferred-activation" class="data-input mono hero-sms-runtime-select">
+234 -7
View File
@@ -100,6 +100,8 @@ const btnSub2ApiGroupMenu = document.getElementById('btn-sub2api-group-menu');
const sub2ApiGroupCurrent = document.getElementById('sub2api-group-current');
const sub2ApiGroupMenu = document.getElementById('sub2api-group-menu');
const btnAddSub2ApiGroup = document.getElementById('btn-add-sub2api-group');
const rowSub2ApiAccountPriority = document.getElementById('row-sub2api-account-priority');
const inputSub2ApiAccountPriority = document.getElementById('input-sub2api-account-priority');
const rowSub2ApiDefaultProxy = document.getElementById('row-sub2api-default-proxy');
const inputSub2ApiDefaultProxy = document.getElementById('input-sub2api-default-proxy');
const rowIpProxyEnabled = document.getElementById('row-ip-proxy-enabled');
@@ -406,6 +408,9 @@ const rowPhoneCodeWaitSeconds = document.getElementById('row-phone-code-wait-sec
const rowPhoneCodeTimeoutWindows = document.getElementById('row-phone-code-timeout-windows');
const rowPhoneCodePollIntervalSeconds = document.getElementById('row-phone-code-poll-interval-seconds');
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 btnToggleHeroSmsApiKey = document.getElementById('btn-toggle-hero-sms-api-key');
const inputFiveSimApiKey = document.getElementById('input-five-sim-api-key');
@@ -423,6 +428,9 @@ const inputPhoneCodeTimeoutWindows = document.getElementById('input-phone-code-t
const inputPhoneCodePollIntervalSeconds = document.getElementById('input-phone-code-poll-interval-seconds');
const inputPhoneCodePollMaxRounds = document.getElementById('input-phone-code-poll-max-rounds');
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 selectHeroSmsCountryFallback = document.getElementById('select-hero-sms-country-fallback');
const selectHeroSmsAcquirePriority = document.getElementById('select-hero-sms-acquire-priority');
@@ -454,10 +462,14 @@ const displayHeroSmsCurrentCountdown = document.getElementById('display-hero-sms
const displayHeroSmsPriceTiers = document.getElementById('display-hero-sms-price-tiers');
const displayPhoneSmsBalance = document.getElementById('display-phone-sms-balance');
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 displayFiveSimCountryFallbackOrder = document.getElementById('display-five-sim-country-fallback-order');
const displayNexSmsCountryFallbackOrder = document.getElementById('display-nex-sms-country-fallback-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 inputAccountRunHistoryHelperBaseUrl = document.getElementById('input-account-run-history-helper-base-url');
const autoStartModal = document.getElementById('auto-start-modal');
@@ -1150,6 +1162,15 @@ function normalizeSub2ApiGroupOptions(...sources) {
return normalizeEditableListValues(...sources);
}
function normalizeSub2ApiAccountPriorityValue(value) {
const rawValue = String(value ?? '').trim();
const numeric = Number(rawValue);
if (!rawValue || !Number.isSafeInteger(numeric) || numeric < 1) {
return 1;
}
return numeric;
}
function getSelectedSub2ApiGroupName() {
return String(inputSub2ApiGroup?.value || '').trim()
|| DEFAULT_SUB2API_GROUP_OPTIONS[0];
@@ -2978,6 +2999,12 @@ function collectSettingsPayload() {
const heroSmsReuseEnabledValue = typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled
? normalizeHeroSmsReuseEnabledValue(inputHeroSmsReuseEnabled.checked)
: 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'
? DEFAULT_HERO_SMS_ACQUIRE_PRIORITY
: (typeof HERO_SMS_ACQUIRE_PRIORITY_COUNTRY !== 'undefined' ? HERO_SMS_ACQUIRE_PRIORITY_COUNTRY : 'country');
@@ -3147,7 +3174,7 @@ function collectSettingsPayload() {
? selectGpcHelperOtpChannel.value
: (latestState?.gopayHelperOtpChannel || 'whatsapp')
);
const selectedGpcLocalSmsHelperEnabled = selectedGpcOtpChannel === 'sms' && Boolean(
const selectedGpcLocalSmsHelperEnabled = Boolean(
typeof inputGpcHelperLocalSmsEnabled !== 'undefined' && inputGpcHelperLocalSmsEnabled
? inputGpcHelperLocalSmsEnabled.checked
: latestState?.gopayHelperLocalSmsHelperEnabled
@@ -3181,6 +3208,12 @@ function collectSettingsPayload() {
if (sub2apiGroupNames.length === 0) {
appendSub2ApiGroupNames(['codex', 'openai-plus']);
}
const sub2apiAccountPriorityNormalizer = typeof normalizeSub2ApiAccountPriorityValue === 'function'
? normalizeSub2ApiAccountPriorityValue
: ((value) => {
const numeric = Number(String(value ?? '').trim());
return Number.isSafeInteger(numeric) && numeric >= 1 ? numeric : 1;
});
return {
...(contributionModeEnabled ? {} : {
panelMode: selectPanelMode.value,
@@ -3193,6 +3226,11 @@ function collectSettingsPayload() {
sub2apiPassword: inputSub2ApiPassword.value,
sub2apiGroupName: selectedSub2ApiGroupName,
sub2apiGroupNames,
sub2apiAccountPriority: sub2apiAccountPriorityNormalizer(
typeof inputSub2ApiAccountPriority !== 'undefined' && inputSub2ApiAccountPriority
? inputSub2ApiAccountPriority.value
: latestState?.sub2apiAccountPriority
),
sub2apiDefaultProxyName: inputSub2ApiDefaultProxy.value.trim(),
ipProxyEnabled: getSelectedIpProxyEnabledSafe(),
ipProxyService: selectedIpProxyService,
@@ -3342,6 +3380,8 @@ function collectSettingsPayload() {
nexSmsCountryOrder: nexSmsCountryOrderValue,
nexSmsServiceCode: nexSmsServiceCodeValue,
heroSmsReuseEnabled: heroSmsReuseEnabledValue,
freePhoneReuseEnabled: freePhoneReuseEnabledValue,
freePhoneReuseAutoEnabled: freePhoneReuseAutoEnabledValue,
heroSmsAcquirePriority: heroSmsAcquirePriorityValue,
heroSmsMaxPrice: heroSmsMaxPriceValue,
heroSmsPreferredPrice: heroSmsPreferredPriceValue,
@@ -5341,10 +5381,79 @@ function updateHeroSmsRuntimeDisplay(state = {}) {
const code = String(state?.currentPhoneVerificationCode ?? latestState?.currentPhoneVerificationCode ?? '').trim();
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);
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() {
const countrySelect = selectHeroSmsCountry || selectHeroSmsCountryFallback;
if (!countrySelect) {
@@ -6948,6 +7057,8 @@ function updatePhoneVerificationSettingsUI() {
typeof rowPhoneCodeTimeoutWindows !== 'undefined' ? rowPhoneCodeTimeoutWindows : null,
typeof rowPhoneCodePollIntervalSeconds !== 'undefined' ? rowPhoneCodePollIntervalSeconds : null,
typeof rowPhoneCodePollMaxRounds !== 'undefined' ? rowPhoneCodePollMaxRounds : null,
typeof rowFreePhoneReuseEnabled !== 'undefined' ? rowFreePhoneReuseEnabled : null,
typeof rowFreePhoneReuseAutoEnabled !== 'undefined' ? rowFreePhoneReuseAutoEnabled : null,
];
phoneVerificationRows.forEach((row) => {
if (row) {
@@ -6970,12 +7081,33 @@ function updatePhoneVerificationSettingsUI() {
if (rowFiveSimOperator) {
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;
[
typeof rowHeroSmsRuntimePair !== 'undefined' ? rowHeroSmsRuntimePair : null,
typeof rowHeroSmsCurrentNumber !== 'undefined' ? rowHeroSmsCurrentNumber : null,
typeof rowHeroSmsCurrentCountdown !== 'undefined' ? rowHeroSmsCurrentCountdown : null,
typeof rowHeroSmsCurrentCode !== 'undefined' ? rowHeroSmsCurrentCode : null,
typeof rowFreeReusablePhone !== 'undefined' ? rowFreeReusablePhone : null,
typeof rowHeroSmsPreferredActivation !== 'undefined' ? rowHeroSmsPreferredActivation : null,
].forEach((row) => {
if (row) {
@@ -7014,8 +7146,8 @@ function updatePlusModeUI() {
? normalizePlusPaymentMethod(selectPlusPaymentMethod.value)
: method;
const gpcRowsVisible = enabled && selectedMethod === gpcValue;
const localSmsControlsVisible = gpcRowsVisible && gpcOtpChannel === 'sms';
const effectiveLocalSmsEnabled = gpcOtpChannel === 'sms' && localSmsEnabled;
const localSmsControlsVisible = gpcRowsVisible;
const effectiveLocalSmsEnabled = localSmsEnabled;
if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) {
selectPlusPaymentMethod.value = method;
if (selectPlusPaymentMethod.style) {
@@ -7097,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) {
const normalizedEmail = String(email || '').trim() || null;
const response = await chrome.runtime.sendMessage({
@@ -7536,6 +7679,7 @@ function applyAutoRunStatus(payload = currentAutoRun) {
const settingsCardLocked = scheduled || locked;
setSettingsCardLocked(settingsCardLocked);
setFreePhoneReuseControlsLocked(settingsCardLocked);
inputRunCount.disabled = currentAutoRun.autoRunning || (
typeof shouldLockRunCountToEmailPool === 'function'
@@ -7550,6 +7694,9 @@ function applyAutoRunStatus(payload = currentAutoRun) {
if (typeof inputSignupPhone !== 'undefined' && inputSignupPhone) {
inputSignupPhone.disabled = locked;
}
if (typeof inputSub2ApiAccountPriority !== 'undefined' && inputSub2ApiAccountPriority) {
inputSub2ApiAccountPriority.disabled = locked;
}
inputAutoSkipFailures.disabled = scheduled;
const lockedRunCount = typeof getLockedRunCountFromEmailPool === 'function'
@@ -7847,6 +7994,9 @@ function applySettingsState(state) {
inputSub2ApiEmail.value = state?.sub2apiEmail || '';
inputSub2ApiPassword.value = state?.sub2apiPassword || '';
renderSub2ApiGroupOptions(state, state?.sub2apiGroupName || '');
if (typeof inputSub2ApiAccountPriority !== 'undefined' && inputSub2ApiAccountPriority) {
inputSub2ApiAccountPriority.value = String(normalizeSub2ApiAccountPriorityValue(state?.sub2apiAccountPriority));
}
inputSub2ApiDefaultProxy.value = state?.sub2apiDefaultProxyName || '';
const normalizedIpProxyService = resolveIpProxyService(state?.ipProxyService);
const normalizedIpProxyServiceProfiles = typeof normalizeIpProxyServiceProfiles === 'function'
@@ -8092,6 +8242,12 @@ function applySettingsState(state) {
if (typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled) {
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) {
selectHeroSmsAcquirePriority.value = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
}
@@ -9613,6 +9769,7 @@ function updatePanelModeUI() {
rowSub2ApiEmail.style.display = useSub2Api ? '' : 'none';
rowSub2ApiPassword.style.display = useSub2Api ? '' : 'none';
rowSub2ApiGroup.style.display = useSub2Api ? '' : 'none';
rowSub2ApiAccountPriority.style.display = useSub2Api ? '' : 'none';
rowSub2ApiDefaultProxy.style.display = useSub2Api ? '' : 'none';
rowCodex2ApiUrl.style.display = useCodex2Api ? '' : 'none';
rowCodex2ApiAdminKey.style.display = useCodex2Api ? '' : 'none';
@@ -10354,6 +10511,7 @@ const contributionModeManager = window.SidepanelContributionMode?.createContribu
rowPhoneVerificationEnabled,
rowCustomPassword,
rowLocalCpaStep9Mode,
rowSub2ApiAccountPriority,
rowSub2ApiDefaultProxy,
rowSub2ApiEmail,
rowSub2ApiGroup,
@@ -11229,9 +11387,6 @@ selectPlusPaymentMethod?.addEventListener('change', () => {
scheduleSettingsAutoSave();
});
input?.addEventListener('change', () => {
if (input === inputGpcHelperLocalSmsEnabled && input.checked && selectGpcHelperOtpChannel) {
selectGpcHelperOtpChannel.value = 'sms';
}
if (input === selectGpcHelperOtpChannel || input === inputGpcHelperLocalSmsEnabled) {
updatePlusModeUI();
}
@@ -11703,6 +11858,16 @@ inputSub2ApiGroup.addEventListener('change', () => {
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
});
inputSub2ApiAccountPriority.addEventListener('input', () => {
markSettingsDirty(true);
scheduleSettingsAutoSave();
});
inputSub2ApiAccountPriority.addEventListener('blur', () => {
inputSub2ApiAccountPriority.value = String(normalizeSub2ApiAccountPriorityValue(inputSub2ApiAccountPriority.value));
saveSettings({ silent: true }).catch(() => { });
});
btnAddSub2ApiGroup?.addEventListener('click', () => {
handleAddSub2ApiGroup().catch((error) => {
showToast(error?.message || '添加 SUB2API 分组失败。', 'error');
@@ -12422,6 +12587,59 @@ inputHeroSmsReuseEnabled?.addEventListener('change', () => {
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.value = normalizeHeroSmsAcquirePriority(selectHeroSmsAcquirePriority.value);
markSettingsDirty(true);
@@ -13246,6 +13464,14 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.payload.heroSmsReuseEnabled !== undefined && inputHeroSmsReuseEnabled) {
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) {
selectHeroSmsAcquirePriority.value = normalizeHeroSmsAcquirePriority(message.payload.heroSmsAcquirePriority);
}
@@ -13400,7 +13626,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
: latestState?.heroSmsCountryFallback
);
applyHeroSmsFallbackSelection(
[...nextPrimaryCountries, ...nextFallback],
[nextPrimary, ...nextFallback],
{ includePrimary: true }
);
updateHeroSmsPlatformDisplay();
@@ -13414,6 +13640,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|| message.payload.currentPhoneVerificationCountdownEndsAt !== undefined
|| message.payload.currentPhoneVerificationCountdownWindowIndex !== undefined
|| message.payload.currentPhoneVerificationCountdownWindowTotal !== undefined
|| message.payload.freeReusablePhoneActivation !== undefined
|| message.payload.heroSmsLastPriceTiers !== undefined
|| message.payload.heroSmsLastPriceCountryId !== undefined
|| message.payload.heroSmsLastPriceCountryLabel !== undefined
@@ -2,6 +2,47 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function extractFunctionBody(source, name) {
const start = source.indexOf(`function ${name}(`);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
for (let i = braceStart; i < source.length; i += 1) {
const ch = source[i];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
return source.slice(braceStart + 1, i);
}
}
}
throw new Error(`unterminated function ${name}`);
}
test('icloud login helper distinguishes auth-required errors from transient context errors', () => {
const source = fs.readFileSync('background.js', 'utf8');
@@ -107,3 +148,20 @@ test('icloud login helper distinguishes auth-required errors from transient cont
'icloud auto-fetch should fallback to reusable aliases when create-new fails due transient session/context issues'
);
});
test('icloud login helper does not redeclare safeActionLabel in transient branch', () => {
const source = fs.readFileSync('background.js', 'utf8');
const body = extractFunctionBody(source, 'withIcloudLoginHelp');
const declarations = body.match(/\bconst\s+safeActionLabel\b/g) || [];
assert.equal(
declarations.length,
1,
'withIcloudLoginHelp should declare safeActionLabel once to avoid temporal-dead-zone crashes'
);
assert.match(
body,
/const transientError = new Error\(`iCloud\$\{safeActionLabel\}受网络\/上下文波动影响,请稍后重试。`\);/,
'transient context errors should use the already-initialized safeActionLabel'
);
});
@@ -7,6 +7,27 @@ test('background imports message router module', () => {
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', () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = {};
@@ -15,3 +36,56 @@ test('message router module exposes a factory', () => {
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'
);
});
@@ -116,3 +116,49 @@ test('panel bridge can request cpa oauth url via management api', async () => {
globalThis.fetch = originalFetch;
}
});
test('panel bridge forwards SUB2API account priority when requesting oauth url', async () => {
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
const sentMessages = [];
const api = new Function('self', `${source}; return self.MultiPageBackgroundPanelBridge;`)({});
const bridge = api.createPanelBridge({
addLog: async () => {},
chrome: {
tabs: {
create: async () => ({ id: 72 }),
},
},
closeConflictingTabsForSource: async () => {},
ensureContentScriptReadyOnTab: async () => {},
getPanelMode: () => 'sub2api',
normalizeCodex2ApiUrl: (value) => value,
normalizeSub2ApiUrl: (value) => value,
rememberSourceLastUrl: async () => {},
sendToContentScript: async (sourceName, message, options) => {
sentMessages.push({ sourceName, message, options });
return {
oauthUrl: 'https://auth.openai.com/authorize?state=oauth-state',
sub2apiSessionId: 'session-123',
sub2apiOAuthState: 'oauth-state',
};
},
sendToContentScriptResilient: async () => ({}),
waitForTabUrlFamily: async () => ({ id: 72 }),
DEFAULT_SUB2API_GROUP_NAME: 'codex',
SUB2API_STEP1_RESPONSE_TIMEOUT_MS: 90000,
});
await bridge.requestOAuthUrlFromPanel({
panelMode: 'sub2api',
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiGroupName: 'codex',
sub2apiAccountPriority: 3,
}, { logLabel: '步骤 7' });
assert.equal(sentMessages.length, 1);
assert.equal(sentMessages[0].sourceName, 'sub2api-panel');
assert.equal(sentMessages[0].message.payload.sub2apiAccountPriority, 3);
});
@@ -272,3 +272,34 @@ test('platform verify module sends Plus visible step 13 to SUB2API panel', async
assert.equal(sentMessages[0].message.step, 13);
});
test('platform verify module forwards SUB2API account priority to panel', async () => {
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
const sentMessages = [];
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
const { deps } = createDeps({
getPanelMode: () => 'sub2api',
getTabId: async () => 91,
isTabAlive: async () => true,
sendToContentScript: async (sourceName, message, options) => {
sentMessages.push({ sourceName, message, options });
return { ok: true };
},
});
const executor = api.createStep10Executor(deps);
await executor.executeStep10({
panelMode: 'sub2api',
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiSessionId: 'session-1',
sub2apiOAuthState: 'oauth-state',
sub2apiAccountPriority: 2,
});
assert.equal(sentMessages.length, 1);
assert.equal(sentMessages[0].sourceName, 'sub2api-panel');
assert.equal(sentMessages[0].message.payload.sub2apiAccountPriority, 2);
});
+149
View File
@@ -294,3 +294,152 @@ return {
assert.equal(result.code, '556677');
});
test('icloud step8 polling finds a visible first-row code immediately', async () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('getThreadItemMetadata'),
extractFunction('buildItemSignature'),
extractFunction('extractVerificationCode'),
extractFunction('normalizePollSessionKey'),
extractFunction('getOrCreatePollSessionBaseline'),
extractFunction('persistPollSessionBaseline'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
const ICLOUD_POLL_SESSION_CACHE = new Map();
function log() {}
function throwIfStopped() {}
async function sleep() {}
async function waitForElement() { return true; }
async function refreshInbox() { return true; }
const currentThreadData = [
{
signature: 'visible-code',
sender: 'noreply@tm.openai.com',
subject: '你的 OpenAI 代码为 576773',
preview: '输入此临时验证码以继续:576773',
timestamp: '21:05',
ariaLabel: 'visible-code',
},
{
signature: 'older-code',
sender: 'noreply@tm.openai.com',
subject: '你的 OpenAI 代码为 697852',
preview: '输入此临时验证码以继续:697852',
timestamp: '21:04',
ariaLabel: 'older-code',
},
];
function collectThreadItems() {
return currentThreadData.map((entry) => ({
getAttribute(name) {
if (name === 'aria-label') return entry.ariaLabel || entry.signature;
return '';
},
querySelector(selector) {
if (selector === '.thread-participants') return { textContent: entry.sender };
if (selector === '.thread-subject') return { textContent: entry.subject };
if (selector === '.thread-preview') return { textContent: entry.preview };
if (selector === '.thread-timestamp') return { textContent: entry.timestamp };
return null;
},
}));
}
async function openMailItemAndRead(item) {
const meta = getThreadItemMetadata(item);
return {
sender: meta.sender,
recipients: '',
timestamp: meta.timestamp,
bodyText: meta.preview,
combinedText: meta.combinedText,
};
}
${bundle}
return { handlePollEmail };
`)();
const result = await api.handlePollEmail(8, {
senderFilters: ['openai', 'noreply', 'chatgpt'],
subjectFilters: ['code', '验证码', 'login'],
maxAttempts: 1,
intervalMs: 10,
excludeCodes: [],
sessionKey: '8:visible-first',
});
assert.equal(result.code, '576773');
});
test('icloud step8 visible first-row code still respects excluded codes', async () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('getThreadItemMetadata'),
extractFunction('buildItemSignature'),
extractFunction('extractVerificationCode'),
extractFunction('normalizePollSessionKey'),
extractFunction('getOrCreatePollSessionBaseline'),
extractFunction('persistPollSessionBaseline'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
const ICLOUD_POLL_SESSION_CACHE = new Map();
function log() {}
function throwIfStopped() {}
async function sleep() {}
async function waitForElement() { return true; }
async function refreshInbox() { return true; }
const currentThreadData = [
{
signature: 'visible-code',
sender: 'noreply@tm.openai.com',
subject: '你的 OpenAI 代码为 576773',
preview: '输入此临时验证码以继续:576773',
timestamp: '21:05',
ariaLabel: 'visible-code',
},
];
function collectThreadItems() {
return currentThreadData.map((entry) => ({
getAttribute(name) {
if (name === 'aria-label') return entry.ariaLabel || entry.signature;
return '';
},
querySelector(selector) {
if (selector === '.thread-participants') return { textContent: entry.sender };
if (selector === '.thread-subject') return { textContent: entry.subject };
if (selector === '.thread-preview') return { textContent: entry.preview };
if (selector === '.thread-timestamp') return { textContent: entry.timestamp };
return null;
},
}));
}
async function openMailItemAndRead(item) {
const meta = getThreadItemMetadata(item);
return {
sender: meta.sender,
recipients: '',
timestamp: meta.timestamp,
bodyText: meta.preview,
combinedText: meta.combinedText,
};
}
${bundle}
return { handlePollEmail };
`)();
await assert.rejects(
() => api.handlePollEmail(8, {
senderFilters: ['openai', 'noreply', 'chatgpt'],
subjectFilters: ['code', '验证码', 'login'],
maxAttempts: 1,
intervalMs: 10,
excludeCodes: ['576773'],
sessionKey: '8:excluded-visible-first',
}),
/仍未在 iCloud 邮箱中找到新的匹配邮件/
);
});
+49
View File
@@ -448,3 +448,52 @@ test('phone auth resend stops with PHONE_ROUTE_405_RECOVERY_FAILED instead of en
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;
}
});
+819
View File
@@ -2794,6 +2794,28 @@ test('phone verification helper immediately replaces number when page says the p
verificationResendCount: 1,
currentPhoneActivation: 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 = [
@@ -2903,6 +2925,11 @@ test('phone verification helper immediately replaces number when page says the p
'SUBMIT_PHONE_NUMBER',
'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 () => {
@@ -2913,6 +2940,28 @@ test('phone verification helper treats phone_max_usage_exceeded as used-number a
verificationResendCount: 1,
currentPhoneActivation: 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 = [
@@ -3022,6 +3071,11 @@ test('phone verification helper treats phone_max_usage_exceeded as used-number a
'SUBMIT_PHONE_NUMBER',
'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 () => {
@@ -3418,6 +3472,771 @@ test('phone verification helper keeps maxUses behavior for reused V2 activations
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 () => {
const requests = [];
const messages = [];
@@ -932,6 +932,65 @@ test('GPC billing reads OTP from local SMS helper when enabled', async () => {
assert.equal(events.completed[0].step, 7);
});
test('GPC billing can read WhatsApp OTP from local helper when enabled', async () => {
const fetchCalls = [];
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url.startsWith('http://127.0.0.1:18767/otp')) {
return {
ok: true,
status: 200,
json: async () => ({ ok: true, otp: '765432', message_id: 'wa-1' }),
};
}
if (url.endsWith('/api/gopay/otp')) {
return {
ok: true,
status: 200,
json: async () => ({ reference_id: 'ref_wa', challenge_id: 'challenge_wa' }),
};
}
if (url.endsWith('/api/gopay/pin')) {
return {
ok: true,
status: 200,
json: async () => ({ stage: 'gopay_complete' }),
};
}
throw new Error(`unexpected url: ${url}`);
},
});
await executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperReferenceId: 'ref_wa',
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
gopayHelperPin: '654321',
gopayHelperCardKey: 'card_wa',
gopayHelperOtpChannel: 'whatsapp',
gopayHelperLocalSmsHelperEnabled: true,
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
gopayHelperPhoneNumber: '+8613800138000',
});
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
assert.equal(events.states.some((state) => state.gopayHelperResolvedOtp === '765432'), true);
const helperUrl = new URL(fetchCalls[0].url);
assert.equal(helperUrl.origin + helperUrl.pathname, 'http://127.0.0.1:18767/otp');
assert.equal(helperUrl.searchParams.get('reference_id'), 'ref_wa');
assert.equal(helperUrl.searchParams.get('phone_number'), '+8613800138000');
assert.deepEqual(JSON.parse(fetchCalls[1].options.body), {
reference_id: 'ref_wa',
otp: '765432',
card_key: 'card_wa',
});
assert.equal(events.completed[0].step, 7);
});
test('GPC billing retries OTP with compatibility field after HTTP 400', async () => {
const fetchCalls = [];
let currentState = {
+2
View File
@@ -110,6 +110,7 @@ const inputAutoSkipFailures = { disabled: false };
const autoContinueBar = { style: { display: '' } };
function setSettingsCardLocked() {}
function setFreePhoneReuseControlsLocked() {}
function getAutoRunLabel() { return ''; }
function getLockedRunCountFromEmailPool() { return lockedRunCount; }
function isCustomMailProvider() { return false; }
@@ -198,6 +199,7 @@ const inputAutoSkipFailures = { disabled: false };
const autoContinueBar = { style: { display: '' } };
function setSettingsCardLocked() {}
function setFreePhoneReuseControlsLocked() {}
function getAutoRunLabel() { return ''; }
function getLockedRunCountFromEmailPool() { return 0; }
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-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-verification-resend-count"/);
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"/);
});
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', () => {
assert.match(sidepanelSource, /function getRuntimeSignupPhoneValue\(state = latestState\)/);
assert.match(sidepanelSource, /function shouldExecuteStep3WithSignupPhoneIdentity\(state = latestState\)/);
@@ -494,6 +549,9 @@ function updateSignupMethodUI() {
function syncSignupPhoneInputFromState() {
rowSignupPhone.style.display = inputPhoneVerificationEnabled.checked && latestState.signupPhoneNumber ? '' : 'none';
}
function setFreePhoneReuseControlsLocked() {}
function isAutoRunLockedPhase() { return false; }
function isAutoRunScheduledPhase() { return false; }
${extractFunction('updatePhoneVerificationSettingsUI')}
@@ -686,6 +744,8 @@ const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputPhoneVerificationEnabled = { checked: true };
const inputFreePhoneReuseEnabled = { checked: true };
const inputFreePhoneReuseAutoEnabled = { checked: true };
const selectPhoneSmsProvider = { value: 'hero-sms' };
const inputVerificationResendCount = { value: '4' };
const inputHeroSmsApiKey = { value: 'demo-key' };
@@ -832,6 +892,8 @@ return { collectSettingsPayload };
assert.deepStrictEqual(payload.nexSmsCountryOrder, [1]);
assert.equal(payload.nexSmsServiceCode, 'ot');
assert.equal(payload.heroSmsReuseEnabled, true);
assert.equal(payload.freePhoneReuseEnabled, true);
assert.equal(payload.freePhoneReuseAutoEnabled, true);
assert.equal(payload.heroSmsAcquirePriority, 'price');
assert.equal(payload.heroSmsMaxPrice, '0.12');
assert.equal(payload.heroSmsPreferredPrice, '0.0512');
+6 -10
View File
@@ -257,24 +257,20 @@ return {
assert.equal(api.rows.rowGpcHelperCardKey.style.display, '');
assert.equal(api.rows.rowGpcHelperPhone.style.display, '');
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, '');
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, 'none');
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
assert.match(api.plusPaymentMethodCaption.textContent, /GPC/);
api.selectGpcHelperOtpChannel.value = 'sms';
api.updatePlusModeUI();
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
api.inputGpcHelperLocalSmsEnabled.checked = true;
api.updatePlusModeUI();
assert.equal(api.selectGpcHelperOtpChannel.value, 'whatsapp');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, '');
api.selectGpcHelperOtpChannel.value = 'whatsapp';
api.selectGpcHelperOtpChannel.value = 'sms';
api.updatePlusModeUI();
assert.equal(api.inputGpcHelperLocalSmsEnabled.checked, false);
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, 'none');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
assert.equal(api.inputGpcHelperLocalSmsEnabled.checked, true);
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, '');
api.selectPlusPaymentMethod.value = 'gopay';
api.updatePlusModeUI();
@@ -0,0 +1,52 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
test('sidepanel exposes SUB2API account priority below group setting', () => {
assert.match(html, /id="row-sub2api-account-priority"/);
assert.match(html, /id="input-sub2api-account-priority"/);
assert.match(html, /<span class="data-label">优先级<\/span>/);
const inputTag = html.match(/<input[^>]*id="input-sub2api-account-priority"[^>]*>/)?.[0] || '';
assert.match(inputTag, /type="number"/);
assert.match(inputTag, /min="1"/);
assert.match(inputTag, /step="1"/);
assert.ok(
html.indexOf('id="row-sub2api-account-priority"') > html.indexOf('id="row-sub2api-group"'),
'priority row should be placed below the SUB2API group row'
);
assert.ok(
html.indexOf('id="row-sub2api-account-priority"') < html.indexOf('id="row-sub2api-default-proxy"'),
'priority row should remain above the SUB2API default proxy row'
);
});
test('sidepanel persists and locks SUB2API account priority setting', () => {
assert.match(
source,
/const rowSub2ApiAccountPriority = document\.getElementById\('row-sub2api-account-priority'\);/
);
assert.match(
source,
/const inputSub2ApiAccountPriority = document\.getElementById\('input-sub2api-account-priority'\);/
);
assert.match(source, /function normalizeSub2ApiAccountPriorityValue\(/);
assert.match(source, /const sub2apiAccountPriorityNormalizer = typeof normalizeSub2ApiAccountPriorityValue === 'function'/);
assert.match(source, /sub2apiAccountPriority: sub2apiAccountPriorityNormalizer\(/);
assert.match(
source,
/inputSub2ApiAccountPriority\.value = String\(normalizeSub2ApiAccountPriorityValue\(state\?\.sub2apiAccountPriority\)\);/
);
assert.match(source, /rowSub2ApiAccountPriority\.style\.display = useSub2Api \? '' : 'none';/);
assert.match(source, /inputSub2ApiAccountPriority\.disabled = locked;/);
assert.match(
source,
/inputSub2ApiAccountPriority\.addEventListener\('input', \(\) => \{[\s\S]*scheduleSettingsAutoSave\(\);[\s\S]*\}\);/
);
assert.match(
source,
/inputSub2ApiAccountPriority\.addEventListener\('blur', \(\) => \{[\s\S]*saveSettings\(\{ silent: true \}\)/
);
});
+46
View File
@@ -277,3 +277,49 @@ test('SUB2API step 10 omits proxy_id when no proxy is configured', async () => {
assert.equal(Object.hasOwn(exchangeCall.body, 'proxy_id'), false);
assert.equal(Object.hasOwn(createCall.body, 'proxy_id'), false);
});
test('SUB2API step 10 creates account with configured account priority', async () => {
const fetchCalls = [];
const context = createSub2ApiPanelContext(fetchCalls);
await vm.runInContext(`
step9_submitOpenAiCallback({
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiGroupName: 'codex',
sub2apiSessionId: 'session-1',
sub2apiOAuthState: 'oauth-state',
sub2apiGroupId: 5,
sub2apiAccountPriority: 3
})
`, context);
const createCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts');
assert.equal(createCall.body.priority, 3);
});
test('SUB2API account priority must be an integer greater than or equal to 1', async () => {
const fetchCalls = [];
const context = createSub2ApiPanelContext(fetchCalls);
await assert.rejects(
() => vm.runInContext(`
step9_submitOpenAiCallback({
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiGroupName: 'codex',
sub2apiSessionId: 'session-1',
sub2apiOAuthState: 'oauth-state',
sub2apiGroupId: 5,
sub2apiAccountPriority: 0
})
`, context),
/SUB2API 账号优先级必须是大于等于 1 的整数/
);
assert.equal(fetchCalls.some((call) => call.path === '/api/v1/admin/accounts'), false);
});
+2 -2
View File
@@ -46,7 +46,7 @@
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Ultra`、历史 `Pro` 与 legacy `v` 三个版本族,排序时固定以 `Ultra` 为最高正式系列,同时会在读取缓存后重新排序,避免历史 `Pro``v` 版本误显示为比 `Ultra` 更新
- 展示一个单独的“接码”开关、注册方式 `signupMethod` 与“接码平台”下拉;接码平台当前支持 HeroSMS / 5sim / NexSMS。普通模式下开启接码后可把注册方式切到手机号注册,并在 OAuth 登录链路命中手机号登录验证码页时继续复用同一手机号续跑短信验证
- 侧栏在接码卡内提供一个独立运行态“注册手机号”输入框,位于接码订单运行状态下方;第 2 步自动拿到号码后立即回填,用户手动接管手机号注册时也可以直接改写这一个运行态槽位。这个输入框表达账号身份,不等同于接码订单;后续自动拉短信仍依赖 `signupPhoneActivation / signupPhoneCompletedActivation`
- 展示 `Plus 模式` 开关与 Plus 支付方式配置;支付方式支持 PayPal / GoPay / GPCPayPal 展示账号池下拉与添加按钮,GoPay 展示手机号和 PIN,GPC 展示卡密、专用手机号、OTP 渠道、本地 macOS SMS helper 开关与 URL、PIN;步骤列表切换为 Plus 模式 13 步定义,普通模式的注册成功等待步骤不再显示或执行,登录验证码步骤会移动到 Plus 可见第 11 步
- 展示 `Plus 模式` 开关与 Plus 支付方式配置;支付方式支持 PayPal / GoPay / GPCPayPal 展示账号池下拉与添加按钮,GoPay 展示手机号和 PIN,GPC 展示卡密、专用手机号、OTP 渠道、本地 OTP helper 开关与 URL、PIN;步骤列表切换为 Plus 模式 13 步定义,普通模式的注册成功等待步骤不再显示或执行,登录验证码步骤会移动到 Plus 可见第 11 步
- 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作
### 2.2 Background Service Worker
@@ -549,7 +549,7 @@ Plus 模式可见步骤:
1. 第 1~5 步:沿用普通注册入口、邮箱、密码、注册验证码、资料填写链路。
2. 第 6 步 `创建 Plus Checkout`:打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session。PayPal 使用 `DE / EUR``https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`GoPay 使用 `ID / IDR``https://chatgpt.com/checkout/openai_llc/{checkout_session_id}`GPC helper 模式改为把 accessToken、卡密、手机号、`otp_channel` 等提交给 helper API 创建 GPC 订单。
3. 第 7 步 `填写账单并提交订阅`:按 `plusPaymentMethod` 选择 PayPal、GoPay 或 GPC。PayPal / GoPay 仍走 checkout 页面与 Stripe iframe 自动化;GPC helper 模式不再操作 checkout iframe,而是先提交 OTP,再提交 PIN。若侧栏启用本地 macOS SMS helper,后台会先轮询 `gopayHelperLocalSmsHelperUrl``/otp` 接口读取 SMS OTP,读取失败再回退到手动 OTP 输入弹窗。
3. 第 7 步 `填写账单并提交订阅`:按 `plusPaymentMethod` 选择 PayPal、GoPay 或 GPC。PayPal / GoPay 仍走 checkout 页面与 Stripe iframe 自动化;GPC helper 模式不再操作 checkout iframe,而是先提交 OTP,再提交 PIN。若侧栏启用本地 OTP helper,后台会先轮询 `gopayHelperLocalSmsHelperUrl``/otp` 接口读取当前 OTP 通道的验证码,读取失败再回退到手动 OTP 输入弹窗;仓库内置的 `scripts/gpc_sms_helper_macos.py` 仍是 macOS Messages 短信读取实现,其他通道需要提供兼容的本地 `/otp` 接口
4. 第 8 步按当前支付方式显示为 `PayPal 登录与授权``GoPay 手机验证与授权`,底层 step key 仍为 `paypal-approve`。PayPal 模式后台优先读取侧边栏当前选中的 PayPal 账号;GoPay 模式读取侧边栏的国家区号、手机号、可选验证码和 PIN,先在 GoPay 页面填写手机号;验证码优先用侧边栏已填值,否则弹出插件输入框让用户手动填写,提交后继续填写 PIN。
5. 第 9 步 `订阅回跳确认`:等待 PayPal / GoPay 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。
6. 第 10 步:复用原 Step 7 OAuth 登录执行器,但状态和日志按 Plus 可见第 10 步记录。
+2 -2
View File
@@ -150,7 +150,7 @@
receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱`
额外显示 `自定义号池` 文本框;当邮箱服务为 iCloud 时,额外提供目标邮箱类型与转发邮箱 provider 配置,用于选择直接从 iCloud 收件箱收
码或从 QQ / 网易 / Gmail 转发目标邮箱收码;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密
钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;接码卡内把注册方式、接码配置、接码订单运行态与“注册手机号”身份运行态分层展示,“注册手机号”位于订单运行状态下方,不嵌入当前分配/验证码网格;设置卡片新增 `Plus 模式` 开关与 PayPal 账号下拉框,右侧使用公共表单弹窗添加账号;GPC Plus 配置额外提供 OTP 渠道选择、本地 macOS SMS helper 开关和 helper URLHotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器;设置卡片还新增“授权总超时”开关,用于控制 Step 7 后链 5 分钟总预算。
钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;接码卡内把注册方式、接码配置、接码订单运行态与“注册手机号”身份运行态分层展示,“注册手机号”位于订单运行状态下方,不嵌入当前分配/验证码网格;设置卡片新增 `Plus 模式` 开关与 PayPal 账号下拉框,右侧使用公共表单弹窗添加账号;GPC Plus 配置额外提供 OTP 渠道选择、本地 OTP helper 开关和 helper URLHotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器;设置卡片还新增“授权总超时”开关,用于控制 Step 7 后链 5 分钟总预算。
- `sidepanel/paypal-manager.js`:侧边栏 PayPal 账号管理器,负责 Plus 模式下的账号下拉框渲染、添加账号弹窗、保存账号与切换当前账号。
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装
配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 账号记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启
@@ -215,7 +215,7 @@
- `tests/paypal-approve-detection.test.js`:测试 Plus 第 8 步后台执行器对 PayPal 标签页发现、分离式账号/密码页识别、联合登录页识别,以及登录后直接离开 PayPal 页的分支判断。
- `tests/paypal-flow-content.test.js`:测试 PayPal 内容脚本对可见邮箱/密码输入框的识别,并覆盖邮箱页即使已预填相同账号也会先清空再重填后继续下一步。
- `tests/gpc-sms-helper-script.test.js`:测试 GPC macOS 本地短信 helper 在非 macOS 环境下会提示平台与 iPhone 短信转发要求。
- `tests/plus-checkout-billing-tab-resolution.test.js`:测试 Plus 第 7 步可直接接管当前 checkout 标签页,并把 PayPal、账单地址、Google 地址推荐和订阅按钮操作路由到对应 Stripe iframe;当前也覆盖 GPC helper 的本地 SMS OTP 自动读取分支。
- `tests/plus-checkout-billing-tab-resolution.test.js`:测试 Plus 第 7 步可直接接管当前 checkout 标签页,并把 PayPal、账单地址、Google 地址推荐和订阅按钮操作路由到对应 Stripe iframe;当前也覆盖 GPC helper 的本地 OTP 自动读取分支。
- `tests/cloudflare-temp-email-provider.test.js`:测试 Cloudflare Temp Email provider 的轮询与目标邮箱选择逻辑。
- `tests/cloudflare-temp-email-utils.test.js`:测试 Cloudflare Temp Email 工具层的 URL、域名、邮件解析逻辑。
- `tests/content-utils.test.js`:测试内容脚本公共工具层对 `mail.126.com` 等网页邮箱来源的识别逻辑。