fix(flow): harden proxy phone and checkout recovery paths
This commit is contained in:
+86
-19
@@ -246,6 +246,18 @@ const IP_PROXY_FETCH_TIMEOUT_MS = 20000;
|
||||
const IP_PROXY_SETTINGS_SCOPE = 'regular';
|
||||
const IP_PROXY_BYPASS_LIST = ['<local>', 'localhost', '127.0.0.1'];
|
||||
const IP_PROXY_ROUTE_ALL_TRAFFIC = true;
|
||||
const IP_PROXY_FORCE_DIRECT_HOST_PATTERNS = [
|
||||
'pm-redirects.stripe.com',
|
||||
'*.pm-redirects.stripe.com',
|
||||
'hwork.pro',
|
||||
'*.hwork.pro',
|
||||
'auth.openai.com',
|
||||
'auth0.openai.com',
|
||||
'accounts.openai.com',
|
||||
'luckyous.com',
|
||||
'*.luckyous.com',
|
||||
];
|
||||
const IP_PROXY_FORCE_DIRECT_FALLBACK = 'PROXY 127.0.0.1:7897';
|
||||
const IP_PROXY_ACCOUNT_LIST_ENABLED = false;
|
||||
const IP_PROXY_INIT_ENABLE_EXIT_PROBE = false;
|
||||
const IP_PROXY_INIT_SUPPRESS_AUTH_REBIND = true;
|
||||
@@ -346,8 +358,12 @@ const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const FIVE_SIM_COUNTRY_ID = 'vietnam';
|
||||
const FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
|
||||
const FIVE_SIM_SUPPORTED_COUNTRY_IDS = ['indonesia', 'thailand', 'vietnam'];
|
||||
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(FIVE_SIM_SUPPORTED_COUNTRY_IDS);
|
||||
const HERO_SMS_SUPPORTED_COUNTRY_IDS = [6, 52, 10];
|
||||
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(HERO_SMS_SUPPORTED_COUNTRY_IDS.map(String));
|
||||
const FIVE_SIM_OPERATOR = 'any';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
@@ -571,6 +587,9 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
luckmailBaseUrl: DEFAULT_LUCKMAIL_BASE_URL,
|
||||
luckmailEmailType: DEFAULT_LUCKMAIL_EMAIL_TYPE,
|
||||
luckmailDomain: '',
|
||||
luckmailUsedPurchases: {},
|
||||
luckmailPreserveTagId: 0,
|
||||
luckmailPreserveTagName: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
|
||||
cloudflareDomain: '',
|
||||
cloudflareDomains: [],
|
||||
cloudflareTempEmailBaseUrl: '',
|
||||
@@ -596,6 +615,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
fiveSimCountryId: FIVE_SIM_COUNTRY_ID,
|
||||
fiveSimCountryLabel: FIVE_SIM_COUNTRY_LABEL,
|
||||
fiveSimCountryFallback: [],
|
||||
fiveSimCountryOrder: [FIVE_SIM_COUNTRY_ID],
|
||||
fiveSimMaxPrice: '',
|
||||
fiveSimOperator: FIVE_SIM_OPERATOR,
|
||||
};
|
||||
@@ -944,7 +964,7 @@ function normalizeHeroSmsCountryFallback(value = []) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isFinite(countryId) || countryId <= 0 || seenIds.has(countryId)) {
|
||||
if (!Number.isFinite(countryId) || countryId <= 0 || !HERO_SMS_SUPPORTED_COUNTRY_ID_SET.has(String(countryId)) || seenIds.has(countryId)) {
|
||||
continue;
|
||||
}
|
||||
seenIds.add(countryId);
|
||||
@@ -984,11 +1004,19 @@ function normalizePlusPaymentMethod(value = '') {
|
||||
|
||||
function normalizeFiveSimCountryId(value, fallback = FIVE_SIM_COUNTRY_ID) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryId) {
|
||||
return rootScope.PhoneSmsFiveSimProvider.normalizeFiveSimCountryId(value, fallback);
|
||||
const rawNormalized = rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryId
|
||||
? rootScope.PhoneSmsFiveSimProvider.normalizeFiveSimCountryId(value, '')
|
||||
: String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||
const normalized = String(rawNormalized || '').trim().toLowerCase();
|
||||
if (FIVE_SIM_SUPPORTED_COUNTRY_ID_SET.has(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||
return normalized || fallback;
|
||||
const fallbackSource = fallback === undefined || fallback === null ? FIVE_SIM_COUNTRY_ID : fallback;
|
||||
const normalizedFallback = String(fallbackSource).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||
if (!normalizedFallback) {
|
||||
return '';
|
||||
}
|
||||
return FIVE_SIM_SUPPORTED_COUNTRY_ID_SET.has(normalizedFallback) ? normalizedFallback : FIVE_SIM_COUNTRY_ID;
|
||||
}
|
||||
|
||||
function normalizeFiveSimCountryLabel(value = '', fallback = FIVE_SIM_COUNTRY_LABEL) {
|
||||
@@ -1925,6 +1953,12 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return normalizeLuckmailEmailType(value);
|
||||
case 'luckmailDomain':
|
||||
return String(value || '').trim();
|
||||
case 'luckmailUsedPurchases':
|
||||
return normalizeLuckmailUsedPurchases(value);
|
||||
case 'luckmailPreserveTagId':
|
||||
return Number(value) || 0;
|
||||
case 'luckmailPreserveTagName':
|
||||
return String(value || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME;
|
||||
case 'cloudflareDomain':
|
||||
return normalizeCloudflareDomain(value);
|
||||
case 'cloudflareDomains':
|
||||
@@ -1956,10 +1990,13 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return normalizeHeroSmsAcquirePriority(value);
|
||||
case 'heroSmsMaxPrice':
|
||||
return normalizeHeroSmsMaxPrice(value);
|
||||
case 'heroSmsPreferredPrice':
|
||||
return normalizeHeroSmsMaxPrice(value);
|
||||
case 'heroSmsCountryId':
|
||||
return Math.max(0, Math.floor(Number(value) || 0));
|
||||
case 'heroSmsCountryId': {
|
||||
const parsed = Math.floor(Number(value));
|
||||
if (Number.isFinite(parsed) && HERO_SMS_SUPPORTED_COUNTRY_ID_SET.has(String(parsed))) {
|
||||
return parsed;
|
||||
}
|
||||
return HERO_SMS_COUNTRY_ID;
|
||||
}
|
||||
case 'heroSmsCountryLabel':
|
||||
return String(value || HERO_SMS_COUNTRY_LABEL).trim() || HERO_SMS_COUNTRY_LABEL;
|
||||
case 'heroSmsCountryFallback':
|
||||
@@ -1972,6 +2009,10 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return normalizeFiveSimCountryLabel(value);
|
||||
case 'fiveSimCountryFallback':
|
||||
return normalizeFiveSimCountryFallback(value);
|
||||
case 'fiveSimCountryOrder':
|
||||
return normalizeFiveSimCountryFallback(value)
|
||||
.map((entry) => entry.id)
|
||||
.filter(Boolean);
|
||||
case 'fiveSimMaxPrice':
|
||||
return normalizeFiveSimMaxPrice(value);
|
||||
case 'fiveSimOperator':
|
||||
@@ -2318,6 +2359,7 @@ function getLuckmailPreserveTagInfo(state = {}) {
|
||||
|
||||
async function setLuckmailUsedPurchasesState(usedPurchases) {
|
||||
const normalizedUsedPurchases = normalizeLuckmailUsedPurchases(usedPurchases);
|
||||
await setPersistentSettings({ luckmailUsedPurchases: normalizedUsedPurchases });
|
||||
await setState({ luckmailUsedPurchases: normalizedUsedPurchases });
|
||||
broadcastDataUpdate({ luckmailUsedPurchases: normalizedUsedPurchases });
|
||||
return normalizedUsedPurchases;
|
||||
@@ -2354,6 +2396,7 @@ async function setLuckmailPreserveTagInfo(tag) {
|
||||
luckmailPreserveTagId: Number(normalizedTag.id) || 0,
|
||||
luckmailPreserveTagName: String(normalizedTag.name || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
|
||||
};
|
||||
await setPersistentSettings(updates);
|
||||
await setState(updates);
|
||||
broadcastDataUpdate(updates);
|
||||
return updates;
|
||||
@@ -4877,6 +4920,7 @@ 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;
|
||||
@@ -6728,6 +6772,16 @@ function isSignupUserAlreadyExistsFailure(error) {
|
||||
return /SIGNUP_USER_ALREADY_EXISTS::|user_already_exists/i.test(message);
|
||||
}
|
||||
|
||||
function isStep4Route405RecoveryLimitFailure(error) {
|
||||
const message = getErrorMessage(error);
|
||||
return /STEP4_405_RECOVERY_LIMIT::|步骤\s*4:检测到\s*405\s*错误页面,已连续点击“重试”恢复/i.test(message);
|
||||
}
|
||||
|
||||
function isPhoneSmsPlatformRateLimitFailure(error) {
|
||||
const message = getErrorMessage(error);
|
||||
return /FIVE_SIM_RATE_LIMIT::|5sim[\s\S]*(?:限流|rate\s*limit)/i.test(message);
|
||||
}
|
||||
|
||||
function isPlusCheckoutNonFreeTrialFailure(error) {
|
||||
const message = getErrorMessage(error);
|
||||
return /PLUS_CHECKOUT_NON_FREE_TRIAL::|今日应付金额不是\s*0|没有免费试用资格/i.test(message);
|
||||
@@ -8871,8 +8925,10 @@ const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoR
|
||||
getStopRequested: () => stopRequested,
|
||||
hasSavedProgress,
|
||||
isAddPhoneAuthFailure,
|
||||
isPhoneSmsPlatformRateLimitFailure,
|
||||
isPlusCheckoutNonFreeTrialFailure,
|
||||
isRestartCurrentAttemptError,
|
||||
isStep4Route405RecoveryLimitFailure,
|
||||
isSignupUserAlreadyExistsFailure,
|
||||
isStopError,
|
||||
launchAutoRunTimerPlan,
|
||||
@@ -9771,6 +9827,7 @@ const plusCheckoutBillingExecutor = self.MultiPageBackgroundPlusCheckoutBilling?
|
||||
sleepWithStop,
|
||||
waitForTabCompleteUntilStopped,
|
||||
waitForTabUrlMatchUntilStopped,
|
||||
probeIpProxyExit,
|
||||
});
|
||||
const goPayManualConfirmExecutor = self.MultiPageBackgroundGoPayManualConfirm?.createGoPayManualConfirmExecutor({
|
||||
addLog,
|
||||
@@ -10514,17 +10571,16 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
||||
};
|
||||
const isPlatformVerifyTransientRetryError = (errorMessage = '') => {
|
||||
const normalizedMessage = String(errorMessage || '');
|
||||
const mentionsTokenExchange = /auth\.openai\.com\/oauth\/token/i.test(normalizedMessage);
|
||||
const mentionsTokenExchange = /auth\.openai\.com\/oauth\/token|token\s*exchange|token_exchange_user_error/i.test(normalizedMessage);
|
||||
const hasTransientNetworkSignal = /connect:\s*connection refused|failed to fetch|i\/o timeout|context deadline exceeded|eof|connection reset by peer/i.test(normalizedMessage);
|
||||
const hasTransientExchangeUserSignal = /token_exchange_user_error|invalid\s+request\.\s+please\s+try\s+again\s+later/i.test(normalizedMessage);
|
||||
const hasTokenExchangeFailureSignal = /token\s+exchange\s+failed|oauth\/token/i.test(normalizedMessage);
|
||||
if (hasTransientExchangeUserSignal && hasTokenExchangeFailureSignal) {
|
||||
return true;
|
||||
}
|
||||
return mentionsTokenExchange && hasTransientNetworkSignal;
|
||||
const hasTransientTokenExchangeSignal = /token_exchange_user_error|invalid request\.?\s*please try again later/i.test(normalizedMessage);
|
||||
return mentionsTokenExchange && (hasTransientNetworkSignal || hasTransientTokenExchangeSignal);
|
||||
};
|
||||
const isPhoneVerificationLocalFailure = (errorMessage = '') => {
|
||||
const normalizedMessage = String(errorMessage || '');
|
||||
if (isPhoneSmsPlatformRateLimitFailure(normalizedMessage)) {
|
||||
return false;
|
||||
}
|
||||
return /HeroSMS|phone verification did not succeed|number replacements|sms_timeout_after_resend|phone number is already linked|add-phone keeps rejecting current number|接码|手机号|手机验证码|步骤\s*9.*(?:手机号|验证码)|Step\s*9.*phone verification/i.test(normalizedMessage);
|
||||
};
|
||||
|
||||
@@ -10546,6 +10602,17 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
||||
&& confirmOauthStep < normalizedStep
|
||||
&& isPlatformVerifyTransientRetryError(errorMessage);
|
||||
const restartAnchorStep = shouldRetryFromConfirmStep ? confirmOauthStep : authChainStartStep;
|
||||
if (isPhoneSmsPlatformRateLimitFailure(errorMessage)) {
|
||||
return {
|
||||
shouldRestart: false,
|
||||
blockedByAddPhone: false,
|
||||
forcedByPhoneVerificationTimeout: false,
|
||||
restartStep: authChainStartStep,
|
||||
errorMessage,
|
||||
authState: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (!Number.isFinite(normalizedStep) || normalizedStep < authChainStartStep || normalizedStep > lastStepId) {
|
||||
return {
|
||||
shouldRestart: false,
|
||||
@@ -10603,7 +10670,7 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
||||
});
|
||||
}
|
||||
|
||||
if (isAddPhoneAuthState(authState)) {
|
||||
if (isAddPhoneAuthState(authState) && !isPhoneSmsPlatformRateLimitFailure(errorMessage)) {
|
||||
return {
|
||||
shouldRestart: false,
|
||||
blockedByAddPhone: true,
|
||||
|
||||
@@ -23,8 +23,10 @@
|
||||
getState,
|
||||
hasSavedProgress,
|
||||
isAddPhoneAuthFailure,
|
||||
isPhoneSmsPlatformRateLimitFailure,
|
||||
isPlusCheckoutNonFreeTrialFailure,
|
||||
isRestartCurrentAttemptError,
|
||||
isStep4Route405RecoveryLimitFailure,
|
||||
isSignupUserAlreadyExistsFailure,
|
||||
isStopError,
|
||||
launchAutoRunTimerPlan,
|
||||
@@ -124,6 +126,18 @@
|
||||
&& state.customMailProviderPool.length > 0;
|
||||
}
|
||||
|
||||
function isPhoneNumberSupplyExhaustedFailure(error) {
|
||||
const text = String(
|
||||
typeof getErrorMessage === 'function'
|
||||
? getErrorMessage(error)
|
||||
: (error?.message || error || '')
|
||||
).trim();
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
return /no\s+numbers\s+available\s+across|all provider candidates failed to acquire number|no\s+free\s+phones|numbers?\s+not\s+found|no\s+numbers\s+within\s+maxprice|countries\s+are\s+empty|均无可用号码|暂无可用号码|无可用号码|接码号池暂无|\bNO_NUMBERS\b/i.test(text);
|
||||
}
|
||||
|
||||
async function logAutoRunFinalSummary(totalRuns, roundSummaries = []) {
|
||||
const summaries = buildAutoRunRoundSummaries(totalRuns, roundSummaries);
|
||||
const successRounds = summaries.filter((item) => item.status === 'success');
|
||||
@@ -502,13 +516,27 @@
|
||||
|
||||
const reason = getErrorMessage(err);
|
||||
roundSummary.failureReasons.push(reason);
|
||||
const blockedByAddPhone = typeof isAddPhoneAuthFailure === 'function' && isAddPhoneAuthFailure(err);
|
||||
const blockedByPhoneSmsRateLimit = typeof isPhoneSmsPlatformRateLimitFailure === 'function'
|
||||
&& isPhoneSmsPlatformRateLimitFailure(err);
|
||||
const blockedByPhoneNoSupply = !blockedByPhoneSmsRateLimit
|
||||
&& isPhoneNumberSupplyExhaustedFailure(err);
|
||||
const blockedByAddPhone = !blockedByPhoneSmsRateLimit
|
||||
&& !blockedByPhoneNoSupply
|
||||
&& typeof isAddPhoneAuthFailure === 'function'
|
||||
&& isAddPhoneAuthFailure(err);
|
||||
const blockedByPlusNonFreeTrial = typeof isPlusCheckoutNonFreeTrialFailure === 'function'
|
||||
&& isPlusCheckoutNonFreeTrialFailure(err);
|
||||
const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function'
|
||||
&& !keepSameEmailUntilAddPhone
|
||||
&& isSignupUserAlreadyExistsFailure(err);
|
||||
const canRetry = !blockedByAddPhone && !blockedByPlusNonFreeTrial && !blockedBySignupUserAlreadyExists && autoRunSkipFailures && attemptRun < maxAttemptsForRound;
|
||||
const blockedByStep4Route405 = typeof isStep4Route405RecoveryLimitFailure === 'function'
|
||||
&& isStep4Route405RecoveryLimitFailure(err);
|
||||
const canRetry = !blockedByAddPhone
|
||||
&& !blockedByPhoneNoSupply
|
||||
&& !blockedByPlusNonFreeTrial
|
||||
&& !blockedBySignupUserAlreadyExists
|
||||
&& autoRunSkipFailures
|
||||
&& attemptRun < maxAttemptsForRound;
|
||||
|
||||
await setState({
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||
@@ -549,6 +577,41 @@
|
||||
break;
|
||||
}
|
||||
|
||||
if (blockedByPhoneNoSupply) {
|
||||
roundSummary.status = 'failed';
|
||||
roundSummary.finalFailureReason = reason;
|
||||
await setState({
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||
});
|
||||
await appendRoundRecordIfNeeded('failed', reason);
|
||||
cancelPendingCommands('当前轮因接码号池暂无可用号码已终止。');
|
||||
await broadcastStopToContentScripts();
|
||||
if (!autoRunSkipFailures) {
|
||||
await addLog(
|
||||
`第 ${targetRun}/${totalRuns} 轮接码号池暂无可用号码,自动重试未开启,当前自动运行将停止。`,
|
||||
'warn'
|
||||
);
|
||||
stoppedEarly = true;
|
||||
await broadcastAutoRunStatus('stopped', {
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun,
|
||||
sessionId: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
await addLog(`第 ${targetRun}/${totalRuns} 轮接码号池暂无可用号码,本轮将直接失败并跳过剩余重试。`, 'warn');
|
||||
await addLog(
|
||||
targetRun < totalRuns
|
||||
? `第 ${targetRun}/${totalRuns} 轮因接码号池暂无可用号码提前结束,自动流程将继续下一轮。`
|
||||
: `第 ${targetRun}/${totalRuns} 轮因接码号池暂无可用号码提前结束,已无后续轮次,本次自动运行结束。`,
|
||||
'warn'
|
||||
);
|
||||
forceFreshTabsNextRun = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (blockedByPlusNonFreeTrial) {
|
||||
roundSummary.status = 'failed';
|
||||
roundSummary.finalFailureReason = reason;
|
||||
@@ -619,18 +682,18 @@
|
||||
break;
|
||||
}
|
||||
|
||||
if (blockedByPhoneSupplyExhausted) {
|
||||
if (blockedByStep4Route405) {
|
||||
roundSummary.status = 'failed';
|
||||
roundSummary.finalFailureReason = reason;
|
||||
await setState({
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||
});
|
||||
await appendRoundRecordIfNeeded('failed', reason);
|
||||
cancelPendingCommands('当前轮因接码号池不可用已终止。');
|
||||
cancelPendingCommands('当前轮因步骤 4 连续 405 错误已终止。');
|
||||
await broadcastStopToContentScripts();
|
||||
if (!autoRunSkipFailures) {
|
||||
await addLog(
|
||||
`第 ${targetRun}/${totalRuns} 轮触发接码号池不可用,自动重试未开启,当前自动运行将停止。`,
|
||||
`第 ${targetRun}/${totalRuns} 轮步骤 4 连续 405 恢复失败,自动重试未开启,当前自动运行将停止。`,
|
||||
'warn'
|
||||
);
|
||||
stoppedEarly = true;
|
||||
@@ -643,11 +706,11 @@
|
||||
break;
|
||||
}
|
||||
|
||||
await addLog(`第 ${targetRun}/${totalRuns} 轮接码号池暂不可用,本轮将直接失败并跳过剩余重试。`, 'warn');
|
||||
await addLog(`第 ${targetRun}/${totalRuns} 轮步骤 4 连续 405 恢复失败,本轮将直接失败并跳过剩余重试。`, 'warn');
|
||||
await addLog(
|
||||
targetRun < totalRuns
|
||||
? `第 ${targetRun}/${totalRuns} 轮因接码号池不可用提前结束,自动流程将继续下一轮。`
|
||||
: `第 ${targetRun}/${totalRuns} 轮因接码号池不可用提前结束,已无后续轮次,本次自动运行结束。`,
|
||||
? `第 ${targetRun}/${totalRuns} 轮因步骤 4 连续 405 提前结束,自动流程将继续下一轮。`
|
||||
: `第 ${targetRun}/${totalRuns} 轮因步骤 4 连续 405 提前结束,已无后续轮次,本次自动运行结束。`,
|
||||
'warn'
|
||||
);
|
||||
forceFreshTabsNextRun = true;
|
||||
|
||||
+291
-5
@@ -48,6 +48,11 @@ const IP_PROXY_EXIT_PROBE_ENDPOINTS_711_STICKY = [
|
||||
// 与 curl 口径对齐,保持单端点,避免多站点探测引入额外波动与耗时。
|
||||
'https://ipinfo.io/json',
|
||||
];
|
||||
const IP_PROXY_TARGET_REACHABILITY_ENDPOINTS = [
|
||||
// Step 1 的真实目标站点;出口 IP 可用不代表该站点的 CONNECT/TLS 链路可用。
|
||||
'https://chatgpt.com/',
|
||||
];
|
||||
const IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS = 8000;
|
||||
const IP_PROXY_BACKGROUND_PROBE_MAX_ENDPOINTS = 4;
|
||||
const IP_PROXY_BACKGROUND_PROBE_PER_ENDPOINT_TIMEOUT_MS = 3500;
|
||||
const IP_PROXY_PAGE_CONTEXT_PROBE_URL = 'https://example.com/';
|
||||
@@ -1270,6 +1275,7 @@ function buildIpProxyRoutingStatePatch(status = {}) {
|
||||
const exitDetecting = Boolean(status?.exitDetecting);
|
||||
const exitError = String(status?.exitError || '').trim();
|
||||
const exitSource = String(status?.exitSource || '').trim().toLowerCase();
|
||||
const exitEndpoint = String(status?.exitEndpoint || status?.endpoint || '').trim();
|
||||
return {
|
||||
ipProxyApplied: applied,
|
||||
ipProxyAppliedReason: reason,
|
||||
@@ -1286,6 +1292,7 @@ function buildIpProxyRoutingStatePatch(status = {}) {
|
||||
ipProxyAppliedExitDetecting: exitDetecting,
|
||||
ipProxyAppliedExitError: exitError,
|
||||
ipProxyAppliedExitSource: exitSource,
|
||||
ipProxyAppliedExitEndpoint: exitEndpoint,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1317,6 +1324,23 @@ async function setIpProxyLeakGuardEnabled(enabled) {
|
||||
}).catch(() => { });
|
||||
}
|
||||
|
||||
function shouldEnableIpProxyLeakGuardForStatus(status = {}) {
|
||||
if (!status?.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (status?.applied) {
|
||||
return false;
|
||||
}
|
||||
const reason = String(status?.reason || '').trim().toLowerCase();
|
||||
// connectivity_failed 表示 PAC/代理接管已经下发,只是探测或目标站点失败。
|
||||
// 此时继续启用 DNR 会把 chatgpt.com 变成 ERR_BLOCKED_BY_CLIENT,掩盖真实代理链路结果。
|
||||
return reason !== 'connectivity_failed';
|
||||
}
|
||||
|
||||
async function syncIpProxyLeakGuardForStatus(status = {}) {
|
||||
await setIpProxyLeakGuardEnabled(shouldEnableIpProxyLeakGuardForStatus(status));
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url, options = {}, timeoutMs = IP_PROXY_FETCH_TIMEOUT_MS) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || IP_PROXY_FETCH_TIMEOUT_MS));
|
||||
@@ -1495,6 +1519,15 @@ function resolveExitProbeEndpoints(options = {}) {
|
||||
return IP_PROXY_EXIT_PROBE_ENDPOINTS.slice();
|
||||
}
|
||||
|
||||
function resolveTargetReachabilityEndpoints(options = {}) {
|
||||
const configured = Array.isArray(options?.targetReachabilityEndpoints)
|
||||
? options.targetReachabilityEndpoints.map((item) => String(item || '').trim()).filter(Boolean)
|
||||
: [];
|
||||
return configured.length
|
||||
? configured
|
||||
: IP_PROXY_TARGET_REACHABILITY_ENDPOINTS.slice();
|
||||
}
|
||||
|
||||
function applyExitRegionExpectation(status = {}, expectedRegion = '') {
|
||||
const exitIp = String(status?.exitIp || '').trim();
|
||||
const exitError = String(status?.exitError || '').trim();
|
||||
@@ -1656,6 +1689,48 @@ function applyExitBaselineExpectation(status = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function shouldVerifyIpProxyTargetReachability(status = {}) {
|
||||
if (!String(status?.exitIp || '').trim()) {
|
||||
return false;
|
||||
}
|
||||
if (status?.applied === false && String(status?.reason || '').trim().toLowerCase() === 'connectivity_failed') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildTargetReachabilityFailureMessage(status = {}, reachability = {}) {
|
||||
const exitIp = String(status?.exitIp || '').trim();
|
||||
const exitRegion = String(status?.exitRegion || '').trim();
|
||||
const endpoint = String(reachability?.endpoint || reachability?.url || IP_PROXY_TARGET_REACHABILITY_ENDPOINTS[0] || '').trim();
|
||||
const targetHost = extractProbeHostFromTabUrl(endpoint) || endpoint || 'chatgpt.com';
|
||||
const diagnostic = String(reachability?.error || reachability?.diagnostics || '').trim();
|
||||
const diagnosticSuffix = diagnostic ? ` 诊断:${diagnostic}` : '';
|
||||
const exitRegionSuffix = exitRegion ? ` [${exitRegion}]` : '';
|
||||
return `已检测到出口 IP ${exitIp}${exitRegionSuffix},但真实目标 ${targetHost} 不可达。`
|
||||
+ `这说明代理只通过了 IP 探测,无法打开步骤 1 的 ChatGPT 页面;请更换支持 chatgpt.com CONNECT/TLS 的节点。`
|
||||
+ diagnosticSuffix;
|
||||
}
|
||||
|
||||
function applyTargetReachabilityExpectation(status = {}, reachability = {}) {
|
||||
if (!shouldVerifyIpProxyTargetReachability(status)) {
|
||||
return status;
|
||||
}
|
||||
if (reachability?.reachable === true) {
|
||||
return status;
|
||||
}
|
||||
if (reachability?.skipped === true) {
|
||||
return status;
|
||||
}
|
||||
return {
|
||||
...status,
|
||||
applied: false,
|
||||
reason: 'connectivity_failed',
|
||||
warning: '',
|
||||
error: buildTargetReachabilityFailureMessage(status, reachability),
|
||||
};
|
||||
}
|
||||
|
||||
async function detectProxyExitInfoByBackgroundFetch(options = {}) {
|
||||
const timeoutMs = Number(options?.timeoutMs) > 0 ? Number(options.timeoutMs) : 10000;
|
||||
const errors = Array.isArray(options?.errors) ? options.errors : [];
|
||||
@@ -1954,6 +2029,35 @@ async function readExitProbeFromTabDocument(tabId) {
|
||||
return executionResults?.[0]?.result || null;
|
||||
}
|
||||
|
||||
function appendProbeCacheBuster(rawUrl = '', key = '_multipage_proxy_probe') {
|
||||
const text = String(rawUrl || '').trim();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(text);
|
||||
parsed.searchParams.set(key, String(Date.now()));
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
const separator = text.includes('?') ? '&' : '?';
|
||||
return `${text}${separator}${key}=${Date.now()}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function readTargetReachabilityFromTabDocument(tabId) {
|
||||
const executionResults = await chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
world: 'ISOLATED',
|
||||
func: () => ({
|
||||
href: String(location.href || ''),
|
||||
title: String(document.title || ''),
|
||||
readyState: String(document.readyState || ''),
|
||||
bodyLength: Number(document.body?.innerText?.length || document.documentElement?.innerText?.length || 0),
|
||||
}),
|
||||
});
|
||||
return executionResults?.[0]?.result || null;
|
||||
}
|
||||
|
||||
async function probeExitInfoByTabNavigation(tabId, timeoutMs = 10000, errors = []) {
|
||||
return probeExitInfoByTabNavigationWithEndpoints(tabId, timeoutMs, errors, []);
|
||||
}
|
||||
@@ -2208,6 +2312,101 @@ async function probeExitInfoViaExecuteScript(tabId, timeoutMs = 10000, endpoints
|
||||
return executionResults?.[0]?.result || null;
|
||||
}
|
||||
|
||||
async function detectIpProxyTargetReachabilityByPageContext(options = {}) {
|
||||
const timeoutMs = Number(options?.timeoutMs) > 0
|
||||
? Number(options.timeoutMs)
|
||||
: IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS;
|
||||
const errors = Array.isArray(options?.errors) ? options.errors : [];
|
||||
const endpoints = resolveTargetReachabilityEndpoints(options);
|
||||
if (!endpoints.length) {
|
||||
return { reachable: true, skipped: true, source: 'target_page_context', endpoint: '' };
|
||||
}
|
||||
if (!chrome.tabs?.create || !chrome.tabs?.update || !chrome.scripting?.executeScript) {
|
||||
errors.push('target:page_context:unavailable');
|
||||
return {
|
||||
reachable: false,
|
||||
endpoint: endpoints[0],
|
||||
source: 'target_page_context_unavailable',
|
||||
error: 'target:page_context:unavailable',
|
||||
};
|
||||
}
|
||||
|
||||
let tabId = null;
|
||||
let createdTabId = null;
|
||||
const navigationErrorTracker = createProbeNavigationErrorTracker(null);
|
||||
const perEndpointTimeoutMs = Math.max(2500, Math.min(IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS, timeoutMs));
|
||||
|
||||
try {
|
||||
for (let index = 0; index < endpoints.length; index += 1) {
|
||||
const endpoint = String(endpoints[index] || '').trim();
|
||||
if (!endpoint) {
|
||||
continue;
|
||||
}
|
||||
const targetUrl = appendProbeCacheBuster(endpoint, '_multipage_proxy_target');
|
||||
try {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
const tab = await chrome.tabs.create({
|
||||
url: targetUrl,
|
||||
active: false,
|
||||
});
|
||||
tabId = Number(tab?.id) || null;
|
||||
createdTabId = tabId;
|
||||
navigationErrorTracker.setTabId(tabId);
|
||||
} else {
|
||||
await chrome.tabs.update(tabId, {
|
||||
url: targetUrl,
|
||||
active: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (!Number.isInteger(tabId)) {
|
||||
errors.push(`target:page_context:${endpoint}:no_tab`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ready = await waitForPageContextProbeTabReady(tabId, perEndpointTimeoutMs);
|
||||
if (!ready) {
|
||||
errors.push(`target:page_context:${endpoint}:not_ready`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const documentResult = await readTargetReachabilityFromTabDocument(tabId);
|
||||
const href = String(documentResult?.href || '').trim();
|
||||
const host = extractProbeHostFromTabUrl(href);
|
||||
const expectedHost = extractProbeHostFromTabUrl(endpoint);
|
||||
if (host && expectedHost && (host === expectedHost || host.endsWith(`.${expectedHost}`))) {
|
||||
return {
|
||||
reachable: true,
|
||||
endpoint,
|
||||
url: href,
|
||||
source: 'target_page_context',
|
||||
};
|
||||
}
|
||||
errors.push(`target:page_context:${endpoint}:unexpected_url:${href || 'unknown'}`);
|
||||
} catch (error) {
|
||||
errors.push(`target:page_context:${endpoint}:${error?.message || error}`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
navigationErrorTracker.appendDiagnostics(errors, 3);
|
||||
navigationErrorTracker.dispose();
|
||||
if (Number.isInteger(createdTabId)) {
|
||||
try {
|
||||
await chrome.tabs.remove(createdTabId);
|
||||
} catch {
|
||||
// ignore tab close failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
reachable: false,
|
||||
endpoint: endpoints[0],
|
||||
source: 'target_page_context',
|
||||
error: buildProbeDiagnosticsSummary(errors, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS),
|
||||
};
|
||||
}
|
||||
|
||||
async function detectProxyExitInfoByPageContext(options = {}) {
|
||||
const timeoutMs = Number(options?.timeoutMs) > 0 ? Number(options.timeoutMs) : 10000;
|
||||
const errors = Array.isArray(options?.errors) ? options.errors : [];
|
||||
@@ -2675,6 +2874,16 @@ function buildIpProxyPacScript(entry) {
|
||||
}
|
||||
const targetPatterns = IP_PROXY_TARGET_HOST_PATTERNS.map((pattern) => `'${String(pattern).replace(/'/g, "\\'")}'`).join(', ');
|
||||
const bypassList = IP_PROXY_BYPASS_LIST.map((pattern) => `'${String(pattern).replace(/'/g, "\\'")}'`).join(', ');
|
||||
const forceDirectPatterns = (typeof IP_PROXY_FORCE_DIRECT_HOST_PATTERNS !== 'undefined' && Array.isArray(IP_PROXY_FORCE_DIRECT_HOST_PATTERNS)
|
||||
? IP_PROXY_FORCE_DIRECT_HOST_PATTERNS
|
||||
: [])
|
||||
.map((pattern) => `'${String(pattern).replace(/'/g, "\\'")}'`)
|
||||
.join(', ');
|
||||
const forceDirectFallback = String(
|
||||
typeof IP_PROXY_FORCE_DIRECT_FALLBACK !== 'undefined' && IP_PROXY_FORCE_DIRECT_FALLBACK
|
||||
? IP_PROXY_FORCE_DIRECT_FALLBACK
|
||||
: 'DIRECT'
|
||||
).replace(/"/g, '\\"');
|
||||
const proxyEndpoint = `${pacScheme} ${host}:${port}`;
|
||||
const routeAllLiteral = (typeof IP_PROXY_ROUTE_ALL_TRAFFIC !== 'undefined' && Boolean(IP_PROXY_ROUTE_ALL_TRAFFIC))
|
||||
? 'true'
|
||||
@@ -2690,6 +2899,22 @@ function FindProxyForURL(url, host) {
|
||||
}
|
||||
}
|
||||
|
||||
var forceDirectPatterns = [${forceDirectPatterns}];
|
||||
for (var fd = 0; fd < forceDirectPatterns.length; fd++) {
|
||||
var directPattern = forceDirectPatterns[fd];
|
||||
if (directPattern.indexOf('*.') === 0) {
|
||||
var directSuffix = directPattern.substring(1);
|
||||
var directHost = directPattern.substring(2);
|
||||
if (dnsDomainIs(host, directSuffix) || host === directHost) {
|
||||
return "${forceDirectFallback}";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (host === directPattern || dnsDomainIs(host, '.' + directPattern)) {
|
||||
return "${forceDirectFallback}";
|
||||
}
|
||||
}
|
||||
|
||||
var routeAllTraffic = ${routeAllLiteral};
|
||||
if (routeAllTraffic) {
|
||||
return "${proxyEndpoint}";
|
||||
@@ -3109,9 +3334,25 @@ async function applyIpProxySettingsFromState(state = {}, options = {}) {
|
||||
const expectedRegion = String(entry?.region || '').trim();
|
||||
let normalizedExitStatus = applyExitRegionExpectation(exitStatus, expectedRegion);
|
||||
normalizedExitStatus = applyExitBaselineExpectation(normalizedExitStatus);
|
||||
if (normalizedExitStatus.reason === 'connectivity_failed') {
|
||||
await setIpProxyLeakGuardEnabled(true);
|
||||
if (shouldVerifyIpProxyTargetReachability(normalizedExitStatus)) {
|
||||
const targetDiagnostics = [];
|
||||
const reachability = await detectIpProxyTargetReachabilityByPageContext({
|
||||
timeoutMs: IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS,
|
||||
errors: targetDiagnostics,
|
||||
}).catch((error) => ({
|
||||
reachable: false,
|
||||
endpoint: IP_PROXY_TARGET_REACHABILITY_ENDPOINTS[0],
|
||||
source: 'target_page_context',
|
||||
error: error?.message || String(error || 'target reachability failed'),
|
||||
}));
|
||||
normalizedExitStatus = applyTargetReachabilityExpectation(normalizedExitStatus, {
|
||||
...reachability,
|
||||
error: reachability?.reachable
|
||||
? ''
|
||||
: (reachability?.error || buildProbeDiagnosticsSummary(targetDiagnostics, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS)),
|
||||
});
|
||||
}
|
||||
await syncIpProxyLeakGuardForStatus(normalizedExitStatus);
|
||||
await updateIpProxyRuntimeStatus(normalizedExitStatus);
|
||||
return normalizedExitStatus;
|
||||
}
|
||||
@@ -3425,6 +3666,32 @@ async function probeIpProxyExit(options = {}) {
|
||||
const probePromise = (async () => {
|
||||
const state = options.state || await getState();
|
||||
if (!state?.ipProxyEnabled) {
|
||||
if (options?.detectWhenDisabled) {
|
||||
const diagnostics = [];
|
||||
const exit = await detectProxyExitInfo({
|
||||
timeoutMs: Number(options?.timeoutMs) || 10000,
|
||||
errors: diagnostics,
|
||||
provider: normalizeIpProxyProviderValue(state?.ipProxyService),
|
||||
username: String(state?.ipProxyUsername || '').trim(),
|
||||
preferPageContext: true,
|
||||
allowBackgroundFallback: true,
|
||||
}).catch(() => ({ ip: '', region: '', endpoint: '', source: '' }));
|
||||
const status = {
|
||||
enabled: false,
|
||||
applied: false,
|
||||
reason: 'disabled_probe_only',
|
||||
provider: normalizeIpProxyProviderValue(state?.ipProxyService),
|
||||
exitDetecting: false,
|
||||
exitIp: String(exit?.ip || '').trim(),
|
||||
exitRegion: String(exit?.region || '').trim(),
|
||||
exitBaselineIp: String(exit?.baselineIp || '').trim(),
|
||||
exitEndpoint: String(exit?.endpoint || '').trim(),
|
||||
exitSource: String(exit?.source || '').trim().toLowerCase(),
|
||||
exitError: exit?.ip ? '' : buildProbeDiagnosticsSummary(diagnostics, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS),
|
||||
error: '',
|
||||
};
|
||||
return { proxyRouting: status };
|
||||
}
|
||||
const status = {
|
||||
enabled: false,
|
||||
applied: false,
|
||||
@@ -3488,6 +3755,7 @@ async function probeIpProxyExit(options = {}) {
|
||||
exitDetecting: true,
|
||||
exitIp: '',
|
||||
exitRegion: '',
|
||||
exitEndpoint: '',
|
||||
exitError: '',
|
||||
exitSource: '',
|
||||
};
|
||||
@@ -3519,6 +3787,7 @@ async function probeIpProxyExit(options = {}) {
|
||||
exitIp: String(exit?.ip || '').trim(),
|
||||
exitRegion: String(exit?.region || '').trim(),
|
||||
exitBaselineIp: String(exit?.baselineIp || '').trim(),
|
||||
exitEndpoint: String(exit?.endpoint || '').trim(),
|
||||
exitError: exit?.ip ? '' : buildProbeDiagnosticsSummary(diagnostics, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS),
|
||||
exitSource: String(exit?.source || '').trim().toLowerCase(),
|
||||
authDiagnostics: statusSeed?.hasAuth ? getIpProxyAuthDiagnosticsSummary() : '',
|
||||
@@ -3526,6 +3795,24 @@ async function probeIpProxyExit(options = {}) {
|
||||
const expectedRegion = String(statusSeed?.region || '').trim();
|
||||
let normalized = applyExitRegionExpectation(finalStatus, expectedRegion);
|
||||
normalized = applyExitBaselineExpectation(normalized);
|
||||
if (shouldVerifyIpProxyTargetReachability(normalized)) {
|
||||
const targetDiagnostics = [];
|
||||
const reachability = await detectIpProxyTargetReachabilityByPageContext({
|
||||
timeoutMs: IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS,
|
||||
errors: targetDiagnostics,
|
||||
}).catch((error) => ({
|
||||
reachable: false,
|
||||
endpoint: IP_PROXY_TARGET_REACHABILITY_ENDPOINTS[0],
|
||||
source: 'target_page_context',
|
||||
error: error?.message || String(error || 'target reachability failed'),
|
||||
}));
|
||||
normalized = applyTargetReachabilityExpectation(normalized, {
|
||||
...reachability,
|
||||
error: reachability?.reachable
|
||||
? ''
|
||||
: (reachability?.error || buildProbeDiagnosticsSummary(targetDiagnostics, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS)),
|
||||
});
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
|
||||
@@ -3584,6 +3871,7 @@ async function probeIpProxyExit(options = {}) {
|
||||
exitDetecting: true,
|
||||
exitIp: '',
|
||||
exitRegion: '',
|
||||
exitEndpoint: '',
|
||||
exitError: '',
|
||||
exitSource: '',
|
||||
};
|
||||
@@ -3612,9 +3900,7 @@ async function probeIpProxyExit(options = {}) {
|
||||
normalizedFinalStatus = recovered.status;
|
||||
}
|
||||
}
|
||||
if (normalizedFinalStatus.reason === 'connectivity_failed') {
|
||||
await setIpProxyLeakGuardEnabled(true);
|
||||
}
|
||||
await syncIpProxyLeakGuardForStatus(normalizedFinalStatus);
|
||||
await updateIpProxyRuntimeStatus(normalizedFinalStatus);
|
||||
return { proxyRouting: normalizedFinalStatus };
|
||||
})();
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
const HERO_SMS_LAST_PRICE_COUNTRY_LABEL_KEY = 'heroSmsLastPriceCountryLabel';
|
||||
const HERO_SMS_LAST_PRICE_USER_LIMIT_KEY = 'heroSmsLastPriceUserLimit';
|
||||
const HERO_SMS_LAST_PRICE_AT_KEY = 'heroSmsLastPriceAt';
|
||||
const FIVE_SIM_RATE_LIMIT_ERROR_PREFIX = 'FIVE_SIM_RATE_LIMIT::';
|
||||
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||||
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||||
@@ -207,11 +208,23 @@
|
||||
}
|
||||
|
||||
function resolveFiveSimCountryCandidates(state = {}) {
|
||||
const codes = normalizeFiveSimCountryOrder(state?.fiveSimCountryOrder);
|
||||
let codes = normalizeFiveSimCountryOrder(state?.fiveSimCountryOrder);
|
||||
if (!codes.length) {
|
||||
const legacyPrimary = normalizeFiveSimCountryCode(state?.fiveSimCountryId, '');
|
||||
const legacyFallback = normalizeFiveSimCountryOrder(state?.fiveSimCountryFallback);
|
||||
codes = normalizeFiveSimCountryOrder([
|
||||
...(legacyPrimary ? [legacyPrimary] : []),
|
||||
...legacyFallback,
|
||||
]);
|
||||
}
|
||||
return codes.map((code) => ({
|
||||
code,
|
||||
id: code,
|
||||
label: code,
|
||||
label: (
|
||||
code === normalizeFiveSimCountryCode(state?.fiveSimCountryId, '')
|
||||
? normalizeCountryLabel(state?.fiveSimCountryLabel, code)
|
||||
: code
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -571,10 +584,12 @@
|
||||
(Array.isArray(payloads) ? payloads : [])
|
||||
.flatMap((payload) => collectHeroSmsPriceCandidatesIncludingZeroStock(payload, []))
|
||||
);
|
||||
const mergedCandidates = buildSortedUniquePriceCandidates([
|
||||
...inStockCandidates,
|
||||
...allCatalogCandidates,
|
||||
]);
|
||||
const mergedCandidates = inStockCandidates.length
|
||||
? buildSortedUniquePriceCandidates([
|
||||
...inStockCandidates,
|
||||
...allCatalogCandidates,
|
||||
])
|
||||
: [];
|
||||
const minCatalogPrice = allCatalogCandidates.length
|
||||
? allCatalogCandidates[0]
|
||||
: (mergedCandidates.length ? mergedCandidates[0] : null);
|
||||
@@ -1127,6 +1142,15 @@
|
||||
);
|
||||
}
|
||||
|
||||
function buildPhoneReplacementLimitError(maxNumberReplacementAttempts, reason = '') {
|
||||
const safeMax = Math.max(0, Math.floor(Number(maxNumberReplacementAttempts) || 0));
|
||||
const safeReason = String(reason || 'unknown').trim() || 'unknown';
|
||||
return new Error(
|
||||
`步骤 9:更换 ${safeMax} 次号码后手机号验证仍未成功。最后原因:${safeReason}. `
|
||||
+ `Step 9: phone verification did not succeed after ${safeMax} number replacements. Last reason: ${safeReason}.`
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizePhoneCodeTimeoutError(error) {
|
||||
const message = String(error?.message || '');
|
||||
if (!message.startsWith(PHONE_CODE_TIMEOUT_ERROR_PREFIX)) {
|
||||
@@ -1357,12 +1381,16 @@
|
||||
if (!apiKey) {
|
||||
throw new Error('5sim API key is missing. Save it in the side panel before running the phone flow.');
|
||||
}
|
||||
const configuredMaxPrice = normalizeHeroSmsPriceLimit(state.fiveSimMaxPrice);
|
||||
return {
|
||||
provider,
|
||||
apiKey,
|
||||
baseUrl: normalizeUrl(state.fiveSimBaseUrl, DEFAULT_FIVE_SIM_BASE_URL).replace(/\/+$/, ''),
|
||||
operator: normalizeFiveSimCountryCode(state.fiveSimOperator, DEFAULT_FIVE_SIM_OPERATOR),
|
||||
product: normalizeFiveSimCountryCode(state.fiveSimProduct, DEFAULT_FIVE_SIM_PRODUCT),
|
||||
maxPriceLimit: configuredMaxPrice !== null
|
||||
? configuredMaxPrice
|
||||
: normalizeHeroSmsPriceLimit(state.heroSmsMaxPrice),
|
||||
countryCandidates: resolveFiveSimCountryCandidates(state),
|
||||
};
|
||||
}
|
||||
@@ -1468,9 +1496,15 @@
|
||||
}
|
||||
|
||||
function resolveHeroSmsStockState(payload = {}) {
|
||||
const physicalCount = Number(payload.physicalCount);
|
||||
if (Number.isFinite(physicalCount)) {
|
||||
return {
|
||||
hasStockField: true,
|
||||
stockCount: physicalCount,
|
||||
};
|
||||
}
|
||||
const stockCandidates = [
|
||||
payload.count,
|
||||
payload.physicalCount,
|
||||
payload.stock,
|
||||
payload.available,
|
||||
payload.quantity,
|
||||
@@ -1869,6 +1903,26 @@
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function collectFiveSimProductPriceCandidates(payload, product = DEFAULT_FIVE_SIM_PRODUCT, candidates = []) {
|
||||
if (Array.isArray(payload)) {
|
||||
payload.forEach((entry) => collectFiveSimProductPriceCandidates(entry, product, candidates));
|
||||
return candidates;
|
||||
}
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return candidates;
|
||||
}
|
||||
const productPayload = payload[product] || payload[String(product || '').toLowerCase()];
|
||||
if (productPayload && typeof productPayload === 'object') {
|
||||
const price = Number(productPayload.Price ?? productPayload.price ?? productPayload.cost);
|
||||
const qty = Number(productPayload.Qty ?? productPayload.qty ?? productPayload.count);
|
||||
if (Number.isFinite(price) && price > 0 && (!Number.isFinite(qty) || qty > 0)) {
|
||||
candidates.push(Math.round(price * 10000) / 10000);
|
||||
}
|
||||
}
|
||||
Object.values(payload).forEach((entry) => collectFiveSimProductPriceCandidates(entry, product, candidates));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function findLowestFiveSimPrice(payload, product = DEFAULT_FIVE_SIM_PRODUCT, countryCode = '') {
|
||||
const normalizedProduct = normalizeFiveSimCountryCode(product, DEFAULT_FIVE_SIM_PRODUCT);
|
||||
const normalizedCountryCode = normalizeFiveSimCountryCode(countryCode, '');
|
||||
@@ -1892,6 +1946,21 @@
|
||||
return /no\s+free\s+phones|no\s+phones\s+available|no\s+numbers\s+available/i.test(text);
|
||||
}
|
||||
|
||||
function isFiveSimRateLimitError(payloadOrMessage, status = 0) {
|
||||
if (Number(status) === 429) {
|
||||
return true;
|
||||
}
|
||||
const text = describeFiveSimPayload(payloadOrMessage);
|
||||
return /rate\s*limit|too\s*many\s*requests|request\s*limit|429/i.test(text);
|
||||
}
|
||||
|
||||
function buildFiveSimRateLimitError(details = []) {
|
||||
const suffix = Array.isArray(details) && details.length
|
||||
? `:${details.join(' | ')}。`
|
||||
: '。';
|
||||
return new Error(`${FIVE_SIM_RATE_LIMIT_ERROR_PREFIX}5sim 购买接口触发限流,请稍后再试${suffix}`);
|
||||
}
|
||||
|
||||
function isFiveSimTerminalError(payloadOrMessage, status = 0) {
|
||||
if (Number(status) === 401 || Number(status) === 403) {
|
||||
return true;
|
||||
@@ -1995,7 +2064,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
const maxPriceLimit = normalizeHeroSmsPriceLimit(state.heroSmsMaxPrice);
|
||||
const maxPriceLimit = config.maxPriceLimit === undefined
|
||||
? normalizeHeroSmsPriceLimit(state.heroSmsMaxPrice)
|
||||
: config.maxPriceLimit;
|
||||
const acquirePriority = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
|
||||
const preferredPriceTier = normalizeHeroSmsPriceLimit(state?.heroSmsPreferredPrice);
|
||||
const countryPriceFloorByCountryCode = normalizeCountryPriceFloorMap(
|
||||
@@ -2007,6 +2078,7 @@
|
||||
const retryDelayMs = normalizePhoneActivationRetryDelayMs(state?.heroSmsActivationRetryDelayMs);
|
||||
|
||||
let finalNoNumbersByCountry = [];
|
||||
let finalRateLimitByCountry = [];
|
||||
let finalLastError = null;
|
||||
|
||||
for (let round = 1; round <= maxAcquireRounds; round += 1) {
|
||||
@@ -2017,6 +2089,7 @@
|
||||
);
|
||||
}
|
||||
const noNumbersByCountry = [];
|
||||
const rateLimitByCountry = [];
|
||||
const retryableNoNumberCountries = [];
|
||||
let lastError = null;
|
||||
|
||||
@@ -2068,7 +2141,20 @@
|
||||
const countryLabel = String(countryConfig.label || countryCode).trim() || countryCode;
|
||||
const countryPriceFloor = countryPriceFloorByCountryCode.get(countryCode) ?? null;
|
||||
try {
|
||||
const explicitFiveSimMaxPriceLimit = normalizeHeroSmsPriceLimit(state.fiveSimMaxPrice);
|
||||
let guestPricesPayload = null;
|
||||
let productPricesPayload = null;
|
||||
if (explicitFiveSimMaxPriceLimit !== null) {
|
||||
try {
|
||||
productPricesPayload = await fetchFiveSimPayload(
|
||||
config,
|
||||
`/guest/products/${countryCode}/${config.operator}`,
|
||||
'5sim guest products'
|
||||
);
|
||||
} catch (_) {
|
||||
productPricesPayload = null;
|
||||
}
|
||||
}
|
||||
try {
|
||||
guestPricesPayload = await fetchFiveSimPayload(
|
||||
config,
|
||||
@@ -2086,16 +2172,23 @@
|
||||
}
|
||||
|
||||
const rawPriceCandidates = buildSortedUniquePriceCandidates(
|
||||
collectFiveSimPriceCandidates(
|
||||
(
|
||||
guestPricesPayload
|
||||
&& typeof guestPricesPayload === 'object'
|
||||
&& !Array.isArray(guestPricesPayload)
|
||||
? (guestPricesPayload?.[config.product]?.[countryCode] || guestPricesPayload?.[countryCode] || guestPricesPayload)
|
||||
: guestPricesPayload
|
||||
[
|
||||
...(
|
||||
normalizeHeroSmsPriceLimit(state.fiveSimMaxPrice) !== null
|
||||
? collectFiveSimProductPriceCandidates(productPricesPayload, config.product, [])
|
||||
: []
|
||||
),
|
||||
[]
|
||||
)
|
||||
...collectFiveSimPriceCandidates(
|
||||
(
|
||||
guestPricesPayload
|
||||
&& typeof guestPricesPayload === 'object'
|
||||
&& !Array.isArray(guestPricesPayload)
|
||||
? (guestPricesPayload?.[config.product]?.[countryCode] || guestPricesPayload?.[countryCode] || guestPricesPayload)
|
||||
: guestPricesPayload
|
||||
),
|
||||
[]
|
||||
),
|
||||
]
|
||||
);
|
||||
const boundedPriceCandidates = maxPriceLimit === null
|
||||
? rawPriceCandidates
|
||||
@@ -2106,7 +2199,14 @@
|
||||
preferredPriceTier
|
||||
);
|
||||
const orderedPrices = orderedPricesFromCatalog.length
|
||||
? orderedPricesFromCatalog
|
||||
? (
|
||||
explicitFiveSimMaxPriceLimit !== null
|
||||
? [
|
||||
explicitFiveSimMaxPriceLimit,
|
||||
...orderedPricesFromCatalog.filter((price) => Number(price) !== Number(explicitFiveSimMaxPriceLimit)),
|
||||
]
|
||||
: orderedPricesFromCatalog
|
||||
)
|
||||
: (maxPriceLimit !== null ? [maxPriceLimit] : [null]);
|
||||
const floorFilteredPrices = filterPriceCandidatesAboveFloor(orderedPrices, countryPriceFloor);
|
||||
const hasCountryPriceFloor = (
|
||||
@@ -2181,6 +2281,10 @@
|
||||
break;
|
||||
}
|
||||
const payloadText = describeFiveSimPayload(payload);
|
||||
if (isFiveSimRateLimitError(payload)) {
|
||||
countryNoNumbersText = payloadText || countryNoNumbersText || 'rate limit';
|
||||
continue;
|
||||
}
|
||||
if (isFiveSimNoNumbersError(payload)) {
|
||||
countryNoNumbersText = payloadText || countryNoNumbersText || 'no free phones';
|
||||
continue;
|
||||
@@ -2190,6 +2294,10 @@
|
||||
}
|
||||
lastError = new Error(`5sim buy activation failed: ${payloadText || 'empty response'}`);
|
||||
} catch (error) {
|
||||
if (isFiveSimRateLimitError(error?.payload || error?.message, error?.status)) {
|
||||
countryNoNumbersText = describeFiveSimPayload(error?.payload || error?.message) || countryNoNumbersText || 'rate limit';
|
||||
continue;
|
||||
}
|
||||
if (isFiveSimTerminalError(error?.payload || error?.message, error?.status)) {
|
||||
throw new Error(`5sim buy activation failed: ${describeFiveSimPayload(error?.payload || error?.message) || 'unknown terminal error'}`);
|
||||
}
|
||||
@@ -2210,12 +2318,18 @@
|
||||
noNumbersByCountry.push(
|
||||
`${countryLabel}: no numbers within maxPrice=${maxPriceLimit}; lowest listed=${lowestPrice}`
|
||||
);
|
||||
} else if (isFiveSimRateLimitError(countryNoNumbersText)) {
|
||||
rateLimitByCountry.push(`${countryLabel}: ${countryNoNumbersText || 'rate limit'}`);
|
||||
} else {
|
||||
noNumbersByCountry.push(`${countryLabel}: ${countryNoNumbersText || 'no free phones'}`);
|
||||
retryableNoNumberCountries.push(countryLabel);
|
||||
}
|
||||
continue;
|
||||
} catch (error) {
|
||||
if (isFiveSimRateLimitError(error?.payload || error?.message, error?.status)) {
|
||||
rateLimitByCountry.push(`${countryLabel}: ${describeFiveSimPayload(error?.payload || error?.message) || 'rate limit'}`);
|
||||
continue;
|
||||
}
|
||||
if (isFiveSimTerminalError(error?.payload || error?.message, error?.status)) {
|
||||
throw new Error(`5sim buy activation failed: ${describeFiveSimPayload(error?.payload || error?.message) || 'unknown terminal error'}`);
|
||||
}
|
||||
@@ -2236,8 +2350,13 @@
|
||||
}
|
||||
|
||||
finalNoNumbersByCountry = noNumbersByCountry;
|
||||
finalRateLimitByCountry = rateLimitByCountry;
|
||||
finalLastError = lastError;
|
||||
|
||||
if (rateLimitByCountry.length) {
|
||||
throw buildFiveSimRateLimitError(rateLimitByCountry);
|
||||
}
|
||||
|
||||
if (
|
||||
noNumbersByCountry.length
|
||||
&& round < maxAcquireRounds
|
||||
@@ -2259,6 +2378,9 @@
|
||||
`5sim no numbers available across ${countryCandidates.length} country candidate(s): ${finalNoNumbersByCountry.join(' | ')}.`
|
||||
);
|
||||
}
|
||||
if (finalRateLimitByCountry.length) {
|
||||
throw buildFiveSimRateLimitError(finalRateLimitByCountry);
|
||||
}
|
||||
if (finalLastError) {
|
||||
throw finalLastError;
|
||||
}
|
||||
@@ -2714,6 +2836,10 @@
|
||||
`步骤 9:HeroSMS 正在获取手机号(第 ${round}/${maxAcquireRounds} 轮)...`,
|
||||
'info'
|
||||
);
|
||||
await addLog(
|
||||
`步骤 9:HeroSMS 正在获取手机号(第 ${round}/${maxAcquireRounds} 轮)...`,
|
||||
'info'
|
||||
);
|
||||
}
|
||||
|
||||
const countryAttempts = countryCandidates.map((countryConfig, index) => ({
|
||||
@@ -2935,6 +3061,10 @@
|
||||
`步骤 9:HeroSMS 暂无可用号码(第 ${round}/${maxAcquireRounds} 轮);${Math.ceil(retryDelayMs / 1000)} 秒后重试。国家:${retryableNoNumberCountries.join(', ')}。`,
|
||||
'warn'
|
||||
);
|
||||
await addLog(
|
||||
`步骤 9:HeroSMS 暂无可用号码(第 ${round}/${maxAcquireRounds} 轮),${Math.ceil(retryDelayMs / 1000)} 秒后重试。国家:${retryableNoNumberCountries.join(', ')}。`,
|
||||
'warn'
|
||||
);
|
||||
await sleepWithStop(retryDelayMs);
|
||||
continue;
|
||||
}
|
||||
@@ -2945,6 +3075,7 @@
|
||||
if (finalNoNumbersByCountry.length) {
|
||||
throw new Error(
|
||||
`HeroSMS 已尝试 ${countryCandidates.length} 个候选国家,均无可用号码:${finalNoNumbersByCountry.join(' | ')}。`
|
||||
+ ` HeroSMS no numbers available across ${countryCandidates.length} country candidate(s): ${finalNoNumbersByCountry.join(' | ')}.`
|
||||
);
|
||||
}
|
||||
if (finalLastError) {
|
||||
@@ -3606,7 +3737,9 @@
|
||||
const successfulUses = normalizedActivation.successfulUses + 1;
|
||||
const nextReusableActivation = {
|
||||
...normalizedActivation,
|
||||
successfulUses,
|
||||
successfulUses: normalizedActivation.provider === PHONE_SMS_PROVIDER_5SIM
|
||||
? 1
|
||||
: successfulUses,
|
||||
};
|
||||
await upsertReusableActivationPool(nextReusableActivation, { state });
|
||||
if (!normalizeHeroSmsReuseEnabled(state?.heroSmsReuseEnabled)) {
|
||||
@@ -3907,10 +4040,147 @@
|
||||
countryPriceFloorByKey.delete(countryKey);
|
||||
};
|
||||
|
||||
const getBlockedCountryIds = () => Array.from(countrySmsFailureCounts.entries())
|
||||
.filter(([, count]) => Number(count) >= PHONE_SMS_FAILURE_SKIP_THRESHOLD)
|
||||
.map(([countryId]) => countryId)
|
||||
.filter(Boolean);
|
||||
const getBlockedCountryIds = () => {
|
||||
const activeProvider = normalizePhoneSmsProvider(
|
||||
state?.phoneSmsProvider || activation?.provider || DEFAULT_PHONE_SMS_PROVIDER
|
||||
);
|
||||
return Array.from(countrySmsFailureCounts.entries())
|
||||
.filter(([, count]) => Number(count) >= PHONE_SMS_FAILURE_SKIP_THRESHOLD)
|
||||
.map(([countryKey]) => splitCountryFailureKey(countryKey, activeProvider))
|
||||
.filter((entry) => entry.provider === activeProvider)
|
||||
.map((entry) => String(entry.countryKey || '').trim())
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const getCountryPriceFloorById = () => {
|
||||
const activeProvider = normalizePhoneSmsProvider(
|
||||
state?.phoneSmsProvider || activation?.provider || DEFAULT_PHONE_SMS_PROVIDER
|
||||
);
|
||||
const floorById = {};
|
||||
countryPriceFloorByKey.forEach((price, compoundCountryKey) => {
|
||||
const numeric = normalizeHeroSmsPrice(price);
|
||||
if (numeric === null || numeric <= 0) {
|
||||
return;
|
||||
}
|
||||
const parsed = splitCountryFailureKey(compoundCountryKey, activeProvider);
|
||||
if (parsed.provider !== activeProvider) {
|
||||
return;
|
||||
}
|
||||
const keyPart = String(parsed.countryKey || '').trim();
|
||||
if (!keyPart) {
|
||||
return;
|
||||
}
|
||||
floorById[keyPart] = Math.round(numeric * 10000) / 10000;
|
||||
});
|
||||
return floorById;
|
||||
};
|
||||
|
||||
const setCountryPriceFloorFromActivation = async (activationCandidate, reason = '') => {
|
||||
const normalizedActivation = normalizeActivation(activationCandidate);
|
||||
if (!normalizedActivation) {
|
||||
return;
|
||||
}
|
||||
const countryKey = normalizeCountryFailureKey(
|
||||
normalizedActivation.countryId,
|
||||
normalizedActivation.provider
|
||||
);
|
||||
if (!countryKey) {
|
||||
return;
|
||||
}
|
||||
const floorPrice = normalizeHeroSmsPrice(
|
||||
normalizedActivation.price
|
||||
?? normalizedActivation.maxPrice
|
||||
?? normalizedActivation.selectedPrice
|
||||
?? getActivationAcquiredPriceHint(normalizedActivation)
|
||||
);
|
||||
if (floorPrice === null || floorPrice <= 0) {
|
||||
return;
|
||||
}
|
||||
const currentFloor = normalizeHeroSmsPrice(countryPriceFloorByKey.get(countryKey));
|
||||
if (currentFloor !== null && currentFloor >= floorPrice) {
|
||||
return;
|
||||
}
|
||||
const normalizedFloor = Math.round(floorPrice * 10000) / 10000;
|
||||
countryPriceFloorByKey.set(countryKey, normalizedFloor);
|
||||
const countryLabel = resolveCountryLabelByFailureKey(countryKey, normalizedActivation.provider);
|
||||
await addLog(
|
||||
`Step 9: ${countryLabel} will try a higher price tier (> ${normalizedFloor}) due to ${reason || 'sms timeout'}.`,
|
||||
'warn'
|
||||
);
|
||||
};
|
||||
|
||||
const isPreferredActivation = (activationCandidate, stateSnapshot = {}) => (
|
||||
isSameActivation(
|
||||
stateSnapshot?.[PREFERRED_PHONE_ACTIVATION_STATE_KEY],
|
||||
activationCandidate
|
||||
)
|
||||
);
|
||||
|
||||
const markPreferredActivationExhausted = async (reason = '') => {
|
||||
if (preferredActivationExhausted || !activation || !isPreferredActivation(activation, state)) {
|
||||
return;
|
||||
}
|
||||
preferredActivationExhausted = true;
|
||||
await addLog(
|
||||
`Step 9: preferred number ${activation.phoneNumber} failed (${reason || 'unknown reason'}), falling back to a new number.`,
|
||||
'warn'
|
||||
);
|
||||
};
|
||||
|
||||
const rotateActivationAfterAddPhoneFailure = async (failureReason, failureCode, submitState = {}) => {
|
||||
await markPreferredActivationExhausted(failureCode || failureReason);
|
||||
usedNumberReplacementAttempts += 1;
|
||||
if (usedNumberReplacementAttempts > maxNumberReplacementAttempts) {
|
||||
throw buildPhoneReplacementLimitError(maxNumberReplacementAttempts, failureCode || 'add_phone_rejected');
|
||||
}
|
||||
await addLog(
|
||||
`Step 9: replacing number after add-phone failure (${failureReason}) (${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts}).`,
|
||||
'warn'
|
||||
);
|
||||
if (shouldCancelActivation && activation) {
|
||||
await cancelPhoneActivation(state, activation);
|
||||
}
|
||||
await clearCurrentActivation();
|
||||
activation = null;
|
||||
shouldCancelActivation = false;
|
||||
preferReuseExistingActivationOnAddPhone = false;
|
||||
addPhoneReentryWithSameActivation = 0;
|
||||
let addPhoneSnapshot = {
|
||||
...pageState,
|
||||
...submitState,
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
};
|
||||
try {
|
||||
const returned = await returnToAddPhone(tabId);
|
||||
addPhoneSnapshot = {
|
||||
...addPhoneSnapshot,
|
||||
...returned,
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
};
|
||||
} catch (returnError) {
|
||||
await addLog(
|
||||
`Step 9: failed to return to add-phone page after rejection, will continue with best-effort state. ${returnError.message}`,
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
try {
|
||||
const verified = await ensureAddPhonePageBeforeSubmit('after add-phone rejection');
|
||||
addPhoneSnapshot = {
|
||||
...addPhoneSnapshot,
|
||||
...verified,
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
};
|
||||
} catch (verifyError) {
|
||||
await addLog(
|
||||
`Step 9: failed to verify add-phone state after rejection. ${verifyError.message}`,
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
pageState = addPhoneSnapshot;
|
||||
};
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
@@ -3941,9 +4211,7 @@
|
||||
if (addPhoneReentryWithSameActivation > 1) {
|
||||
usedNumberReplacementAttempts += 1;
|
||||
if (usedNumberReplacementAttempts > maxNumberReplacementAttempts) {
|
||||
throw new Error(
|
||||
`步骤 9:更换 ${maxNumberReplacementAttempts} 次号码后手机号验证仍未成功。最后原因:${formatStep9Reason('returned_to_add_phone_loop')}。`
|
||||
);
|
||||
throw buildPhoneReplacementLimitError(maxNumberReplacementAttempts, 'returned_to_add_phone_loop');
|
||||
}
|
||||
await addLog(
|
||||
`步骤 9:当前号码 ${activation.phoneNumber} 反复返回添加手机号页,正在更换号码(${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`,
|
||||
@@ -4191,9 +4459,7 @@
|
||||
|
||||
usedNumberReplacementAttempts += 1;
|
||||
if (usedNumberReplacementAttempts > maxNumberReplacementAttempts) {
|
||||
throw new Error(
|
||||
`步骤 9:更换 ${maxNumberReplacementAttempts} 次号码后手机号验证仍未成功。最后原因:${formatStep9Reason(replaceReason)}。`
|
||||
);
|
||||
throw buildPhoneReplacementLimitError(maxNumberReplacementAttempts, replaceReason || 'unknown');
|
||||
}
|
||||
|
||||
if (shouldCancelActivation && activation) {
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
setState,
|
||||
sleepWithStop,
|
||||
waitForTabCompleteUntilStopped,
|
||||
probeIpProxyExit = null,
|
||||
} = deps;
|
||||
|
||||
function isPlusCheckoutUrl(url = '') {
|
||||
@@ -115,11 +116,31 @@
|
||||
);
|
||||
}
|
||||
|
||||
function normalizePostalCodeForCountry(countryCode, rawPostalCode = '', fallbackPostalCode = '') {
|
||||
const normalizedCountry = resolveMeiguodizhiCountryCode(countryCode) || normalizeText(countryCode).toUpperCase();
|
||||
const postalCode = normalizeText(rawPostalCode);
|
||||
const fallback = normalizeText(fallbackPostalCode);
|
||||
if (normalizedCountry !== 'KR') {
|
||||
return postalCode;
|
||||
}
|
||||
if (/^\d{5}$/.test(postalCode)) {
|
||||
return postalCode;
|
||||
}
|
||||
if (/^\d{5}$/.test(fallback)) {
|
||||
return fallback;
|
||||
}
|
||||
return '04524';
|
||||
}
|
||||
|
||||
function buildDirectAddressSeed(countryCode, apiAddress, fallbackSeed) {
|
||||
const address1 = normalizeText(apiAddress?.Trans_Address || apiAddress?.Address);
|
||||
const city = normalizeText(apiAddress?.City);
|
||||
const region = normalizeText(apiAddress?.State_Full || apiAddress?.State);
|
||||
const postalCode = normalizeText(apiAddress?.Zip_Code);
|
||||
const postalCode = normalizePostalCodeForCountry(
|
||||
countryCode,
|
||||
apiAddress?.Zip_Code,
|
||||
fallbackSeed?.fallback?.postalCode
|
||||
);
|
||||
if (!address1 || !city || !postalCode) {
|
||||
return null;
|
||||
}
|
||||
@@ -197,11 +218,47 @@
|
||||
};
|
||||
}
|
||||
|
||||
function resolveBillingAddressCountry(state = {}, countryOverride = '', paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const normalizedPaymentMethod = normalizePlusPaymentMethod(paymentMethod || state?.plusPaymentMethod);
|
||||
const checkoutCountry = resolveMeiguodizhiCountryCode(countryOverride);
|
||||
const savedCheckoutCountry = resolveMeiguodizhiCountryCode(state.plusCheckoutCountry);
|
||||
const exitCountry = resolveMeiguodizhiCountryCode(
|
||||
state.ipProxyAppliedExitRegion
|
||||
|| state.ipProxyExitRegion
|
||||
|| ''
|
||||
);
|
||||
|
||||
if (normalizedPaymentMethod === PLUS_PAYMENT_METHOD_GOPAY) {
|
||||
const countryCode = exitCountry || checkoutCountry || savedCheckoutCountry || 'ID';
|
||||
return {
|
||||
countryCode,
|
||||
requestedCountry: exitCountry
|
||||
|| normalizeText(countryOverride)
|
||||
|| normalizeText(state.plusCheckoutCountry)
|
||||
|| 'ID',
|
||||
source: exitCountry ? 'proxy_exit' : (checkoutCountry ? 'checkout_page' : (savedCheckoutCountry ? 'checkout_state' : 'gopay_fallback')),
|
||||
};
|
||||
}
|
||||
|
||||
const countryCode = checkoutCountry || savedCheckoutCountry || exitCountry || 'DE';
|
||||
return {
|
||||
countryCode,
|
||||
requestedCountry: normalizeText(countryOverride)
|
||||
|| normalizeText(state.plusCheckoutCountry)
|
||||
|| exitCountry
|
||||
|| 'DE',
|
||||
source: checkoutCountry ? 'checkout_page' : (savedCheckoutCountry ? 'checkout_state' : (exitCountry ? 'proxy_exit' : 'paypal_fallback')),
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveBillingAddressSeed(state = {}, countryOverride = '', options = {}) {
|
||||
const paymentMethod = normalizePlusPaymentMethod(options.paymentMethod || state?.plusPaymentMethod);
|
||||
const forcedCountry = paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'ID' : '';
|
||||
const requestedCountry = normalizeText(forcedCountry || countryOverride || state.plusCheckoutCountry || 'DE');
|
||||
const countryCode = forcedCountry || resolveMeiguodizhiCountryCode(requestedCountry) || 'DE';
|
||||
const countryResolution = resolveBillingAddressCountry(state, countryOverride, paymentMethod);
|
||||
const countryCode = countryResolution.countryCode;
|
||||
const requestedCountry = countryResolution.requestedCountry;
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY && countryResolution.source === 'proxy_exit') {
|
||||
await addLog(`步骤 7:GoPay 账单地址将按当前代理出口地区 ${countryCode} 填写。`, 'info');
|
||||
}
|
||||
const localSeed = getLocalAddressSeed(countryCode);
|
||||
const lookupSeed = localSeed || buildMeiguodizhiLookupSeed(countryCode);
|
||||
if (!lookupSeed) {
|
||||
@@ -610,7 +667,67 @@
|
||||
await addLog(`步骤 7:账单地址位于 checkout iframe(frameId=${billingFrame.frameId}),将改为在该 frame 内填写。`, 'info');
|
||||
}
|
||||
|
||||
const addressSeed = await resolveBillingAddressSeed(state, billingFrame.countryText, { paymentMethod });
|
||||
let billingState = state;
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY && typeof probeIpProxyExit === 'function') {
|
||||
const staleExitRegion = normalizeText(
|
||||
state?.ipProxyAppliedExitRegion
|
||||
|| state?.ipProxyExitRegion
|
||||
|| ''
|
||||
);
|
||||
try {
|
||||
await addLog('步骤 7:GoPay 账单地址准备按代理出口填写,正在重新检测当前出口地区...', 'info');
|
||||
const probeResult = await probeIpProxyExit({
|
||||
state,
|
||||
timeoutMs: 12000,
|
||||
authRebindRetry: true,
|
||||
detectWhenDisabled: true,
|
||||
});
|
||||
const routing = probeResult?.proxyRouting || {};
|
||||
const probedExitRegion = normalizeText(routing.exitRegion || '');
|
||||
const probedExitIp = normalizeText(routing.exitIp || '');
|
||||
const probedExitSource = normalizeText(routing.exitSource || '');
|
||||
const probeEndpoint = normalizeText(routing.endpoint || routing.exitEndpoint || '');
|
||||
const probeReason = normalizeText(routing.reason || '');
|
||||
const probeError = normalizeText(routing.exitError || routing.error || '');
|
||||
if (probedExitRegion) {
|
||||
billingState = {
|
||||
...(state || {}),
|
||||
ipProxyAppliedExitRegion: probedExitRegion,
|
||||
ipProxyExitRegion: probedExitRegion,
|
||||
ipProxyAppliedExitIp: probedExitIp,
|
||||
ipProxyAppliedExitSource: probedExitSource,
|
||||
};
|
||||
const sourceSuffix = probedExitSource ? `,来源 ${probedExitSource}` : '';
|
||||
const endpointSuffix = probeEndpoint ? `,检测地址 ${probeEndpoint}` : '';
|
||||
await addLog(`步骤 7:当前代理出口复测结果:${probedExitRegion}${probedExitIp ? ` / ${probedExitIp}` : ''}${sourceSuffix}${endpointSuffix}。`, 'info');
|
||||
} else {
|
||||
billingState = {
|
||||
...(state || {}),
|
||||
ipProxyAppliedExitRegion: '',
|
||||
ipProxyExitRegion: '',
|
||||
ipProxyAppliedExitIp: probedExitIp,
|
||||
ipProxyAppliedExitSource: probedExitSource,
|
||||
};
|
||||
await addLog(
|
||||
`步骤 7:代理出口复测没有返回国家/地区代码,已清空旧出口地区${staleExitRegion ? ` ${staleExitRegion}` : ''},不会继续沿用旧地区。${probeReason ? `状态:${probeReason}。` : ''}${probeError ? `诊断:${probeError}` : ''}`,
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
billingState = {
|
||||
...(state || {}),
|
||||
ipProxyAppliedExitRegion: '',
|
||||
ipProxyExitRegion: '',
|
||||
};
|
||||
await addLog(`步骤 7:代理出口复测失败,已清空旧出口地区${staleExitRegion ? ` ${staleExitRegion}` : ''},不会继续沿用旧地区:${error?.message || String(error || '未知错误')}`, 'warn');
|
||||
}
|
||||
}
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY
|
||||
&& typeof probeIpProxyExit === 'function'
|
||||
&& !resolveMeiguodizhiCountryCode(billingState?.ipProxyAppliedExitRegion || billingState?.ipProxyExitRegion || '')) {
|
||||
throw new Error('步骤 7:GoPay 账单地址需要当前代理出口国家/地区,但本次复测没有拿到国家码;已停止填写,避免误用旧的 KR/ID 地区。请先点 IP 代理“检测出口”,确认显示 JP 后再继续。');
|
||||
}
|
||||
const addressSeed = await resolveBillingAddressSeed(billingState, billingFrame.countryText, { paymentMethod });
|
||||
if (!addressSeed) {
|
||||
throw new Error('步骤 7:未找到可用的本地账单地址种子。');
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
const GOPAY_INJECT_FILES = ['content/utils.js', 'content/gopay-flow.js'];
|
||||
const GOPAY_WAIT_TIMEOUT_MS = 120000;
|
||||
const GOPAY_POLL_INTERVAL_MS = 1000;
|
||||
const GOPAY_LINKING_RETRY_WAIT_MS = 15000;
|
||||
const GOPAY_LINKING_STABLE_WAIT_MS = 60000;
|
||||
const GOPAY_OTP_FRAME_URL_PATTERN = /\/linking\/otp\b|gopayapi\.com\/linking\/otp/i;
|
||||
const GOPAY_PIN_FRAME_URL_PATTERN = /pin-web-client\.gopayapi\.com\/auth\/pin|\/auth\/pin\/verify|linking-validate-pin|merchants-gws-app\.gopayapi\.com\/payment\/validate-pin|\/payment\/validate-pin/i;
|
||||
const GOPAY_PAYMENT_FRAME_URL_PATTERN = /merchants-gws-app\.gopayapi\.com\/(?:payment\/details|app\/challenge)|\/gopay-tokenization\/pay/i;
|
||||
@@ -41,6 +43,37 @@
|
||||
return `${terminalMessage}${rawSuffix}`;
|
||||
}
|
||||
|
||||
function createGoPayStableStateTracker() {
|
||||
let signature = '';
|
||||
let firstSeenAt = 0;
|
||||
return {
|
||||
update(pageState = {}, currentUrl = '') {
|
||||
const nextSignature = [
|
||||
pageState.url || currentUrl || '',
|
||||
pageState.hasPhoneInput ? 'phone' : '',
|
||||
pageState.hasOtpInput ? 'otp' : '',
|
||||
pageState.hasPinInput ? 'pin' : '',
|
||||
pageState.hasPayNowButton ? 'pay' : '',
|
||||
pageState.hasContinueButton ? 'continue' : '',
|
||||
normalizeText(pageState.textPreview || '').slice(0, 700),
|
||||
].join('::');
|
||||
const now = Date.now();
|
||||
if (nextSignature !== signature) {
|
||||
signature = nextSignature;
|
||||
firstSeenAt = now;
|
||||
}
|
||||
return {
|
||||
signature,
|
||||
stableMs: firstSeenAt ? now - firstSeenAt : 0,
|
||||
};
|
||||
},
|
||||
reset() {
|
||||
signature = '';
|
||||
firstSeenAt = 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function restartGoPayCheckoutFromStep6(tabId, reason = '') {
|
||||
const message = normalizeText(reason || 'GoPay 支付页已失效或点击后没有进入下一步。');
|
||||
await addLog(`步骤 8:${message} 正在关闭当前 GoPay/Checkout 页面,并回到步骤 6 重新创建 Plus Checkout。`, 'warn');
|
||||
@@ -196,14 +229,16 @@
|
||||
el.inputMode,
|
||||
].filter(Boolean).join(' ')));
|
||||
const hasTerminalError = /waktunya\s+habis|ulang(?:i)?\s+prosesnya\s+dari\s+awal|time(?:'s|\s+is)?\s+(?:out|expired)|session\s+expired|expired|technical\s+error|terjadi\s+kesalahan|payment\s+failed|pembayaran\s+gagal|transaksi\s+gagal|declined|failed/i.test(text);
|
||||
const hasPinInput = /pin|6\s*digit|masukkin\s+pin|ketik\s+6\s+digit/i.test(text)
|
||||
|| inputHints.some((hint) => /pin-input|pin|password|numeric/i.test(hint));
|
||||
const isPinPage = /pin|6\s*digit|masukkin\s+pin|masukkan\s+pin|ketik\s+6\s+digit|enter\s+pin|支付密码/i.test(text)
|
||||
|| /pin-web-client\.gopayapi\.com|\/auth\/pin|\/payment\/validate-pin|linking-validate-pin/i.test(location.href || '');
|
||||
const hasPinInput = isPinPage
|
||||
|| inputHints.some((hint) => /pin-input|(?:^|[\s_-])pin(?:$|[\s_-])|password|numeric|支付密码/i.test(hint));
|
||||
const hasOtpInput = !hasPinInput && (/otp|one[-\s]*time|kode|verification|whatsapp|验证码|短信/i.test(text)
|
||||
|| inputHints.some((hint) => /otp|code|kode|verification|whatsapp/i.test(hint)));
|
||||
const hasPayNowButton = controlTexts.some((item) => /^\s*pay\s+now\s*$/i.test(item)
|
||||
|| /^\s*bayar(?:\s+sekarang)?(?:\s*rp[\s\S]*)?\s*$/i.test(item)
|
||||
|| /(?:^|\s)pay-button(?:\s|$)/i.test(item));
|
||||
const hasContinueButton = controlTexts.some((item) => /continue|next|submit|verify|confirm|authorize|allow|lanjut|berikut|kirim|konfirmasi|link|继续|下一步|提交|验证|确认|授权|绑定|关联/i.test(item));
|
||||
const hasContinueButton = controlTexts.some((item) => /continue|next|submit|verify|confirm|authorize|allow|lanjut|lanjutkan|berikut|kirim|konfirmasi|hubungkan|sambungkan|tautkan|setuju|izinkan|link|继续|下一步|提交|验证|确认|授权|绑定|关联/i.test(item));
|
||||
return {
|
||||
url: location.href,
|
||||
hasTerminalError,
|
||||
@@ -320,10 +355,10 @@
|
||||
.filter((el) => visible(el) && enabled(el));
|
||||
const controlTexts = controls.map(getText);
|
||||
const inputHints = inputs.map(getText);
|
||||
const isPinPage = /pin|password|passcode|security|sandi|6\\s*digit|masukkin\\s+pin|ketik\\s+6\\s+digit/i.test(bodyText)
|
||||
|| /pin-web-client\\.gopayapi\\.com|\\/auth\\/pin|\\/payment\\/validate-pin/i.test(location.href || '');
|
||||
const isPinPage = /pin|password|passcode|security|sandi|6\\s*digit|masukkin\\s+pin|masukkan\\s+pin|ketik\\s+6\\s+digit|enter\\s+pin|支付密码/i.test(bodyText)
|
||||
|| /pin-web-client\\.gopayapi\\.com|\\/auth\\/pin|\\/payment\\/validate-pin|linking-validate-pin/i.test(location.href || '');
|
||||
const hasTerminalError = /waktunya\\s+habis|ulang(?:i)?\\s+prosesnya\\s+dari\\s+awal|time(?:'s|\\s+is)?\\s+(?:out|expired)|session\\s+expired|expired|kedaluwarsa|technical\\s+error|terjadi\\s+kesalahan|error\\s+teknis|kendala\\s+teknis|gak\\s+bisa\\s+diproses|coba\\s+lagi\\s+nanti|payment\\s+failed|pembayaran\\s+gagal|transaksi\\s+gagal|ditolak|declined|failed/i.test(bodyText);
|
||||
const hasPinInput = isPinPage || inputHints.some((hint) => /pin-input|pin|password|numeric/i.test(hint));
|
||||
const hasPinInput = isPinPage || inputHints.some((hint) => /pin-input|(?:^|[\\s_-])pin(?:$|[\\s_-])|password|numeric|支付密码/i.test(hint));
|
||||
const hasOtpInput = !hasPinInput && (/otp|one[-\\s]*time|kode|verification|whatsapp|验证码|短信/i.test(bodyText)
|
||||
|| inputHints.some((hint) => /otp|code|kode|verification|whatsapp/i.test(hint)));
|
||||
const hasPhoneInput = !hasOtpInput && !hasPinInput && inputs.some((input) => {
|
||||
@@ -335,7 +370,7 @@
|
||||
const hasPayNowButton = controlTexts.some((item) => /^\\s*pay\\s+now\\s*$/i.test(item)
|
||||
|| /^\\s*bayar(?:\\s+sekarang)?(?:\\s*rp[\\s\\S]*)?\\s*$/i.test(item)
|
||||
|| /(?:^|\\s)pay-button(?:\\s|$)/i.test(item));
|
||||
const hasContinueButton = !hasPayNowButton && !hasPhoneInput && controlTexts.some((item) => /continue|next|submit|verify|confirm|authorize|allow|lanjut|berikut|kirim|konfirmasi|link|继续|下一步|提交|验证|确认|授权|绑定|关联/i.test(item));
|
||||
const hasContinueButton = !hasPayNowButton && !hasPhoneInput && controlTexts.some((item) => /continue|next|submit|verify|confirm|authorize|allow|lanjut|lanjutkan|berikut|kirim|konfirmasi|hubungkan|sambungkan|tautkan|setuju|izinkan|link|继续|下一步|提交|验证|确认|授权|绑定|关联/i.test(item));
|
||||
const completed = /success|successful|completed|selesai|berhasil|approved|authorized|支付成功|绑定成功|已授权/i.test(bodyText)
|
||||
&& !hasPhoneInput
|
||||
&& !hasOtpInput
|
||||
@@ -454,7 +489,7 @@
|
||||
if (/^\\s*pay\\s+now\\s*$/i.test(text) || /^\\s*bayar(?:\\s+sekarang)?(?:\\s*rp[\\s\\S]*)?\\s*$/i.test(text)) {
|
||||
return false;
|
||||
}
|
||||
return /continue|next|submit|verify|confirm|authorize|allow|lanjut|berikut|kirim|konfirmasi|link|继续|下一步|提交|验证|确认|授权|绑定|关联/i.test(text);
|
||||
return /continue|next|submit|verify|confirm|authorize|allow|lanjut|lanjutkan|berikut|kirim|konfirmasi|hubungkan|sambungkan|tautkan|setuju|izinkan|link|继续|下一步|提交|验证|确认|授权|绑定|关联/i.test(text);
|
||||
});
|
||||
if (!target) {
|
||||
return { clicked: false, reason: 'target_not_found', url: location.href, textPreview: normalize(document.body?.innerText || '').slice(0, 240) };
|
||||
@@ -539,7 +574,7 @@
|
||||
&& (!style || (style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity) !== 0));
|
||||
};
|
||||
const inputs = Array.from(document.querySelectorAll('input, textarea')).filter((el) => visible(el) && !el.disabled);
|
||||
const target = inputs.find((el) => /otp|one[-\\s]*time|kode|verification|whatsapp|code|pin-input-field/i.test([
|
||||
const target = inputs.find((el) => /otp|one[-\\s]*time|kode|verification|whatsapp|code/i.test([
|
||||
el.getAttribute?.('data-testid'),
|
||||
el.getAttribute?.('aria-label'),
|
||||
el.getAttribute?.('placeholder'),
|
||||
@@ -1005,6 +1040,38 @@
|
||||
return { timeout: true };
|
||||
}
|
||||
|
||||
async function clickGoPayContinueBestEffort(tabId) {
|
||||
const actionFrame = await findGoPayActionFrame(tabId);
|
||||
const actionFrameId = actionFrame.frameId;
|
||||
const actionTargetId = actionFrame.targetId;
|
||||
if (Number.isInteger(actionFrameId)) {
|
||||
await ensureGoPayOtpFrameReady(tabId, actionFrameId);
|
||||
}
|
||||
|
||||
try {
|
||||
if (actionTargetId) {
|
||||
const result = await sendGoPayDebuggerTargetCommand(actionTargetId, 'GOPAY_CLICK_CONTINUE', {});
|
||||
if (result?.clicked) {
|
||||
return result;
|
||||
}
|
||||
} else if (Number.isInteger(actionFrameId)) {
|
||||
const result = await sendGoPayFrameCommand(tabId, actionFrameId, 'GOPAY_CLICK_CONTINUE', {});
|
||||
if (result?.clicked) {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
const result = await sendGoPayCommand(tabId, 'GOPAY_CLICK_CONTINUE', {});
|
||||
if (result?.clicked) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Fall through to a real debugger click below.
|
||||
}
|
||||
|
||||
return clickGoPayContinueWithDebugger(tabId, actionFrameId);
|
||||
}
|
||||
|
||||
function normalizeGoPayCountryCode(value = '') {
|
||||
const normalized = String(value || '').trim().replace(/[^\d+]/g, '');
|
||||
const digits = normalized.replace(/\D/g, '');
|
||||
@@ -1062,6 +1129,7 @@
|
||||
lastContinueClickSignature = '';
|
||||
payNowClickAttempts = 0;
|
||||
lastPayNowClickSignature = '';
|
||||
stableStateTracker?.reset?.();
|
||||
}
|
||||
|
||||
let phoneSubmitted = false;
|
||||
@@ -1072,6 +1140,7 @@
|
||||
let lastContinueClickSignature = '';
|
||||
let payNowClickAttempts = 0;
|
||||
let lastPayNowClickSignature = '';
|
||||
const stableStateTracker = createGoPayStableStateTracker();
|
||||
|
||||
while (true) {
|
||||
throwIfStopped?.();
|
||||
@@ -1094,6 +1163,7 @@
|
||||
: (Number.isInteger(actionFrameId)
|
||||
? await sendGoPayFrameCommand(tabId, actionFrameId, 'GOPAY_GET_STATE', {})
|
||||
: await getGoPayState(tabId));
|
||||
const stableState = stableStateTracker.update(pageState, currentUrl);
|
||||
await handleGoPayTerminalError(pageState, tabId);
|
||||
|
||||
if (pageState.completed) {
|
||||
@@ -1194,27 +1264,6 @@
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pageState.hasOtpInput && !otpSubmitted) {
|
||||
const code = await requestManualGoPayOtp(credentials.otp);
|
||||
credentials.otp = code;
|
||||
await addLog('步骤 8:正在填写 GoPay 验证码...', 'info');
|
||||
if (actionTargetId) {
|
||||
await sendGoPayDebuggerTargetCommand(actionTargetId, 'GOPAY_SUBMIT_OTP', { code });
|
||||
} else if (Number.isInteger(actionFrameId)) {
|
||||
await ensureGoPayOtpFrameReady(tabId, actionFrameId);
|
||||
await sendGoPayFrameCommand(tabId, actionFrameId, 'GOPAY_SUBMIT_OTP', { code });
|
||||
} else {
|
||||
await ensureGoPayReady(tabId, '步骤 8:已获取 GoPay 验证码,等待 GoPay 页面就绪...');
|
||||
await sendGoPayCommand(tabId, 'GOPAY_SUBMIT_OTP', { code });
|
||||
}
|
||||
otpSubmitted = true;
|
||||
continueClickAttempts = 0;
|
||||
lastContinueClickSignature = '';
|
||||
loggedWaiting = false;
|
||||
await sleepWithStop(1500);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pageState.hasPinInput && !pinSubmitted) {
|
||||
await addLog('步骤 8:正在填写 GoPay PIN...', 'info');
|
||||
if (actionTargetId) {
|
||||
@@ -1247,6 +1296,27 @@
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pageState.hasOtpInput && !pageState.hasPinInput && !otpSubmitted) {
|
||||
const code = await requestManualGoPayOtp(credentials.otp);
|
||||
credentials.otp = code;
|
||||
await addLog('步骤 8:正在填写 GoPay 验证码...', 'info');
|
||||
if (actionTargetId) {
|
||||
await sendGoPayDebuggerTargetCommand(actionTargetId, 'GOPAY_SUBMIT_OTP', { code });
|
||||
} else if (Number.isInteger(actionFrameId)) {
|
||||
await ensureGoPayOtpFrameReady(tabId, actionFrameId);
|
||||
await sendGoPayFrameCommand(tabId, actionFrameId, 'GOPAY_SUBMIT_OTP', { code });
|
||||
} else {
|
||||
await ensureGoPayReady(tabId, '步骤 8:已获取 GoPay 验证码,等待 GoPay 页面就绪...');
|
||||
await sendGoPayCommand(tabId, 'GOPAY_SUBMIT_OTP', { code });
|
||||
}
|
||||
otpSubmitted = true;
|
||||
continueClickAttempts = 0;
|
||||
lastContinueClickSignature = '';
|
||||
loggedWaiting = false;
|
||||
await sleepWithStop(1500);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pageState.hasContinueButton) {
|
||||
const continueSignature = `${pageState.url || currentUrl || ''}::${pageState.textPreview || ''}`.slice(0, 700);
|
||||
if (continueSignature === lastContinueClickSignature) {
|
||||
@@ -1256,7 +1326,8 @@
|
||||
continueClickAttempts = 1;
|
||||
}
|
||||
if (continueClickAttempts > 2) {
|
||||
await addLog('步骤 8:GoPay 确认按钮点击后页面仍未变化,已暂停自动重复点击。请手动点击页面上的确认按钮,插件会继续等待后续页面。', 'warn');
|
||||
const stableBeforeRetrySeconds = Math.round(stableState.stableMs / 1000);
|
||||
await addLog(`步骤 8:GoPay 确认按钮点击后页面仍未变化,先等待 linking 页面加载/跳转(已稳定 ${stableBeforeRetrySeconds}s)。`, 'warn');
|
||||
const decision = await waitForGoPayState(tabId, (nextState) => (
|
||||
nextState.hasTerminalError
|
||||
|| nextState.hasOtpInput
|
||||
@@ -1264,7 +1335,7 @@
|
||||
|| nextState.hasPayNowButton
|
||||
|| nextState.completed
|
||||
|| !nextState.hasContinueButton
|
||||
), { timeoutMs: 30000 });
|
||||
), { timeoutMs: GOPAY_LINKING_RETRY_WAIT_MS });
|
||||
await handleGoPayTerminalError(decision.pageState, tabId);
|
||||
if (decision.returned) {
|
||||
await addLog('步骤 8:GoPay 已跳转回 ChatGPT / OpenAI 页面,准备进入回跳确认。', 'ok');
|
||||
@@ -1273,10 +1344,46 @@
|
||||
if (!decision.timeout) {
|
||||
continueClickAttempts = 0;
|
||||
lastContinueClickSignature = '';
|
||||
stableStateTracker.reset();
|
||||
loggedWaiting = false;
|
||||
continue;
|
||||
}
|
||||
throw new Error('步骤 8:GoPay 确认按钮自动点击无效,请手动点击后重新执行或继续当前步骤。');
|
||||
const refreshedState = decision.pageState || pageState;
|
||||
const refreshedStableState = stableStateTracker.update(refreshedState, currentUrl);
|
||||
const stableSeconds = Math.round(refreshedStableState.stableMs / 1000);
|
||||
if (stableSeconds < Math.round(GOPAY_LINKING_STABLE_WAIT_MS / 1000)) {
|
||||
await addLog(`步骤 8:GoPay linking 页面还在同一状态(${stableSeconds}s),改用兜底点击 Hubungkan/确认按钮后继续等待。`, 'info');
|
||||
const retryResult = await clickGoPayContinueBestEffort(tabId);
|
||||
if (retryResult?.clickTarget) {
|
||||
await addLog(`步骤 8:已兜底点击 GoPay 控件:${retryResult.clickTarget}`, 'info');
|
||||
}
|
||||
continueClickAttempts = 2;
|
||||
await sleepWithStop(2500);
|
||||
loggedWaiting = false;
|
||||
continue;
|
||||
}
|
||||
await addLog('步骤 8:GoPay linking 页面长时间没有变化,已暂停自动重复点击。请手动点击页面上的 Hubungkan/确认按钮,插件会继续等待后续页面。', 'warn');
|
||||
const manualDecision = await waitForGoPayState(tabId, (nextState) => (
|
||||
nextState.hasTerminalError
|
||||
|| nextState.hasOtpInput
|
||||
|| nextState.hasPinInput
|
||||
|| nextState.hasPayNowButton
|
||||
|| nextState.completed
|
||||
|| !nextState.hasContinueButton
|
||||
), { timeoutMs: GOPAY_WAIT_TIMEOUT_MS });
|
||||
await handleGoPayTerminalError(manualDecision.pageState, tabId);
|
||||
if (manualDecision.returned) {
|
||||
await addLog('步骤 8:GoPay 已跳转回 ChatGPT / OpenAI 页面,准备进入回跳确认。', 'ok');
|
||||
break;
|
||||
}
|
||||
if (!manualDecision.timeout) {
|
||||
continueClickAttempts = 0;
|
||||
lastContinueClickSignature = '';
|
||||
stableStateTracker.reset();
|
||||
loggedWaiting = false;
|
||||
continue;
|
||||
}
|
||||
throw new Error('步骤 8:GoPay linking 页面长时间无变化,请手动点击 Hubungkan/确认按钮后重新执行或继续当前步骤。');
|
||||
}
|
||||
await addLog(`步骤 8:检测到 GoPay 继续/确认按钮,正在点击${continueClickAttempts > 1 ? `(第 ${continueClickAttempts} 次)` : ''}...`, 'info');
|
||||
const clickResult = continueClickAttempts === 1
|
||||
|
||||
@@ -306,7 +306,8 @@
|
||||
}
|
||||
|
||||
async function executeSub2ApiStep10(state) {
|
||||
const visibleStep = resolvePlatformVerifyStep(state);
|
||||
const platformVerifyStep = resolvePlatformVerifyStep(state);
|
||||
const visibleStep = platformVerifyStep;
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
||||
}
|
||||
@@ -351,7 +352,7 @@
|
||||
await addLog(`步骤 ${visibleStep}:正在向 SUB2API 提交回调并创建账号...`);
|
||||
const requestMessage = {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 10,
|
||||
step: platformVerifyStep,
|
||||
source: 'background',
|
||||
payload: {
|
||||
localhostUrl: state.localhostUrl,
|
||||
|
||||
+58
-17
@@ -75,9 +75,25 @@ async function waitUntil(predicate, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForDocumentComplete() {
|
||||
await waitUntil(() => document.readyState === 'complete', { intervalMs: 200 });
|
||||
await sleep(800);
|
||||
async function waitForDocumentComplete(options = {}) {
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 8000));
|
||||
const settleMs = Math.max(0, Math.floor(Number(options.settleMs) || 500));
|
||||
try {
|
||||
await waitUntil(() => {
|
||||
const readyState = String(document.readyState || '').toLowerCase();
|
||||
return readyState === 'complete'
|
||||
|| readyState === 'interactive'
|
||||
|| Boolean(document.querySelector?.('button, input, textarea, a, [role="button"]'))
|
||||
|| Boolean(normalizeText(document.body?.innerText || document.body?.textContent || ''));
|
||||
}, {
|
||||
intervalMs: 200,
|
||||
timeoutMs,
|
||||
label: 'GoPay DOM',
|
||||
});
|
||||
} catch (_) {
|
||||
// GoPay linking 页面有时长时间保持 loading;后续定位控件本身还有等待/重试。
|
||||
}
|
||||
await sleep(settleMs);
|
||||
}
|
||||
|
||||
function isVisibleElement(el) {
|
||||
@@ -114,6 +130,7 @@ function getActionText(el) {
|
||||
return normalizeText([
|
||||
el?.textContent,
|
||||
el?.value,
|
||||
el?.getAttribute?.('data-testid'),
|
||||
el?.getAttribute?.('aria-label'),
|
||||
el?.getAttribute?.('title'),
|
||||
el?.getAttribute?.('placeholder'),
|
||||
@@ -171,14 +188,33 @@ function isGoPayOtpPageText() {
|
||||
if (isGoPayPinPageText()) {
|
||||
return false;
|
||||
}
|
||||
if (getVisibleTextInputs().some((input) => isGoPayPinInput(input))) {
|
||||
return false;
|
||||
}
|
||||
const text = getPageBodyText();
|
||||
return /otp|one[-\s]*time|kode|verification|whatsapp|验证码|短信/i.test(text);
|
||||
}
|
||||
|
||||
function isGoPayPinPageText() {
|
||||
const text = getPageBodyText();
|
||||
return /pin|password|passcode|security|sandi|6\s*digit/i.test(text)
|
||||
|| /pin-web-client\.gopayapi\.com|\/auth\/pin/i.test(location.href || '');
|
||||
return /pin|password|passcode|security|sandi|6\s*digit|masukkin\s+pin|masukkan\s+pin|ketik\s+6\s+digit|enter\s+pin|支付密码/i.test(text)
|
||||
|| /pin-web-client\.gopayapi\.com|\/auth\/pin|\/payment\/validate-pin|linking-validate-pin/i.test(location.href || '');
|
||||
}
|
||||
|
||||
function isGoPayPinInput(input) {
|
||||
if (!input || isCountrySearchInput(input)) {
|
||||
return false;
|
||||
}
|
||||
const text = getCombinedElementText(input);
|
||||
const testId = String(input.getAttribute?.('data-testid') || '').trim();
|
||||
const type = String(input.getAttribute?.('type') || input.type || '').trim().toLowerCase();
|
||||
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
|
||||
const pinPage = isGoPayPinPageText();
|
||||
const strongPinHint = /password|passcode|security|sandi|支付密码|密码/i.test(text);
|
||||
const ambiguousPinWidget = /^pin-input/i.test(testId)
|
||||
|| /pin-input(?:-field)?|(?:^|[\s_-])pin(?:$|[\s_-])/i.test(text);
|
||||
return strongPinHint
|
||||
|| (pinPage && (ambiguousPinWidget || type === 'password' || maxLength === 1));
|
||||
}
|
||||
|
||||
function detectGoPayTerminalError(text = getPageBodyText()) {
|
||||
@@ -213,31 +249,33 @@ function detectGoPayTerminalError(text = getPageBodyText()) {
|
||||
}
|
||||
|
||||
function findOtpInput() {
|
||||
if (isGoPayPinPageText()) {
|
||||
return null;
|
||||
}
|
||||
const input = findInputByPatterns([
|
||||
/otp|one[-\s]*time|verification|verify|code|kode|whatsapp|wa|pin-input-field/i,
|
||||
/otp|one[-\s]*time|verification|verify|code|kode|whatsapp|wa/i,
|
||||
/验证码|短信|代码/i,
|
||||
]);
|
||||
if (input && !isCountrySearchInput(input)) {
|
||||
if (input && !isCountrySearchInput(input) && !isGoPayPinInput(input)) {
|
||||
return input;
|
||||
}
|
||||
if (isGoPayOtpPageText()) {
|
||||
return getVisibleTextInputs().find((candidate) => !isCountrySearchInput(candidate)) || null;
|
||||
return getVisibleTextInputs().find((candidate) => !isCountrySearchInput(candidate) && !isGoPayPinInput(candidate)) || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getGoPayPinInputs() {
|
||||
return getVisibleTextInputs().filter((candidate) => {
|
||||
if (isCountrySearchInput(candidate)) return false;
|
||||
const text = getCombinedElementText(candidate);
|
||||
const maxLength = Number(candidate.getAttribute?.('maxlength') || candidate.maxLength || 0);
|
||||
return /pin|password|passcode|security|sandi|pin-input/i.test(text)
|
||||
|| candidate.getAttribute?.('data-testid')?.startsWith?.('pin-input')
|
||||
|| (isGoPayPinPageText() && maxLength === 1);
|
||||
return isGoPayPinInput(candidate);
|
||||
});
|
||||
}
|
||||
|
||||
function findPinInput() {
|
||||
const pinInputs = getGoPayPinInputs();
|
||||
if (pinInputs[0]) {
|
||||
return pinInputs[0];
|
||||
}
|
||||
if (isGoPayOtpPageText()) {
|
||||
return null;
|
||||
}
|
||||
@@ -248,8 +286,7 @@ function findPinInput() {
|
||||
if (input && !isCountrySearchInput(input)) {
|
||||
return input;
|
||||
}
|
||||
return getGoPayPinInputs()[0]
|
||||
|| getVisibleControls('input[type="password"]').find((candidate) => isEnabledControl(candidate) && !isCountrySearchInput(candidate))
|
||||
return getVisibleControls('input[type="password"]').find((candidate) => isEnabledControl(candidate) && !isCountrySearchInput(candidate))
|
||||
|| null;
|
||||
}
|
||||
|
||||
@@ -274,7 +311,7 @@ function findPayNowButton() {
|
||||
|
||||
function findContinueButton() {
|
||||
return findClickableByText([
|
||||
/continue|next|submit|verify|confirm|pay|authorize|allow|lanjut|berikut|kirim|bayar|konfirmasi|link/i,
|
||||
/continue|next|submit|verify|confirm|pay|authorize|allow|lanjut|lanjutkan|berikut|kirim|bayar|konfirmasi|hubungkan|sambungkan|tautkan|setuju|izinkan|link/i,
|
||||
/继续|下一步|提交|验证|确认|支付|授权|绑定|关联/i,
|
||||
]);
|
||||
}
|
||||
@@ -556,9 +593,13 @@ function fillVisiblePinInputs(pin = '') {
|
||||
function fillVisibleOtpInputs(code = '') {
|
||||
const normalizedCode = normalizeOtp(code);
|
||||
if (!normalizedCode) return false;
|
||||
if (isGoPayPinPageText() || getVisibleTextInputs().some((input) => isGoPayPinInput(input))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const otpInputs = getVisibleTextInputs()
|
||||
.filter((input) => {
|
||||
if (isGoPayPinInput(input)) return false;
|
||||
const text = getActionText(input);
|
||||
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
|
||||
return /otp|code|kode|verification|验证码|短信/i.test(text)
|
||||
|
||||
@@ -791,6 +791,17 @@ function hasBillingAddressFields() {
|
||||
});
|
||||
}
|
||||
|
||||
function hasPaymentMethodSelectionMarker(el) {
|
||||
if (!el) return false;
|
||||
const className = typeof el.className === 'string' ? el.className : el.getAttribute?.('class') || '';
|
||||
return el.checked === true
|
||||
|| el.getAttribute?.('aria-checked') === 'true'
|
||||
|| el.getAttribute?.('aria-selected') === 'true'
|
||||
|| el.getAttribute?.('data-state') === 'checked'
|
||||
|| el.getAttribute?.('data-selected') === 'true'
|
||||
|| /\b(selected|checked|active)\b/i.test(className);
|
||||
}
|
||||
|
||||
function hasSelectedPaymentMethodControl(method = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const config = getPaymentMethodConfig(method);
|
||||
const candidates = getPaymentMethodSearchCandidates(config.id);
|
||||
@@ -798,15 +809,16 @@ function hasSelectedPaymentMethodControl(method = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
let current = candidate;
|
||||
for (let depth = 0; current && depth < 6; depth += 1, current = current.parentElement) {
|
||||
if (isDocumentLevelContainer(current)) break;
|
||||
if (!config.patterns.some((pattern) => pattern.test(getCombinedSearchText(current)))) continue;
|
||||
const className = typeof current.className === 'string' ? current.className : current.getAttribute?.('class') || '';
|
||||
const currentMatchesPayment = config.patterns.some((pattern) => pattern.test(getCombinedSearchText(current)));
|
||||
if (currentMatchesPayment && hasPaymentMethodSelectionMarker(current)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const radio = current.querySelector?.('input[type="radio"], [role="radio"]');
|
||||
if (
|
||||
current.checked === true
|
||||
|| current.getAttribute?.('aria-checked') === 'true'
|
||||
|| current.getAttribute?.('aria-selected') === 'true'
|
||||
|| current.getAttribute?.('data-state') === 'checked'
|
||||
|| current.getAttribute?.('data-selected') === 'true'
|
||||
|| /\b(selected|checked|active)\b/i.test(className)
|
||||
radio
|
||||
&& config.patterns.some((pattern) => pattern.test(getCombinedSearchText(current) || getCombinedSearchText(radio)))
|
||||
&& hasPaymentMethodSelectionMarker(radio)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
+98
-1
@@ -280,13 +280,104 @@ function is405MethodNotAllowedPage() {
|
||||
return AUTH_ROUTE_ERROR_PATTERN.test(pageText);
|
||||
}
|
||||
|
||||
function getStep405RecoveryStateKey(step) {
|
||||
return `__MULTIPAGE_STEP_${Number(step) || '?'}_405_RECOVERY_COUNT__`;
|
||||
}
|
||||
|
||||
function getStep405StorageScope() {
|
||||
if (typeof window !== 'undefined' && window) {
|
||||
return window;
|
||||
}
|
||||
if (typeof globalThis !== 'undefined' && globalThis) {
|
||||
return globalThis;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function getStep405RecoveryLimit(step) {
|
||||
if (Number(step) !== 4) {
|
||||
return 0;
|
||||
}
|
||||
return typeof STEP4_405_RECOVERY_LIMIT !== 'undefined'
|
||||
? STEP4_405_RECOVERY_LIMIT
|
||||
: 3;
|
||||
}
|
||||
|
||||
function getStep405RecoveryErrorPrefix(step) {
|
||||
if (Number(step) !== 4) {
|
||||
return '';
|
||||
}
|
||||
return typeof STEP4_405_RECOVERY_ERROR_PREFIX !== 'undefined'
|
||||
? STEP4_405_RECOVERY_ERROR_PREFIX
|
||||
: 'STEP4_405_RECOVERY_LIMIT::';
|
||||
}
|
||||
|
||||
function getStep405RecoveryCount(step) {
|
||||
const key = getStep405RecoveryStateKey(step);
|
||||
let value = '';
|
||||
try {
|
||||
if (typeof sessionStorage !== 'undefined' && sessionStorage?.getItem) {
|
||||
value = sessionStorage.getItem(key) || '';
|
||||
}
|
||||
} catch {}
|
||||
if (!value) {
|
||||
value = getStep405StorageScope()[key];
|
||||
}
|
||||
return Math.max(0, Math.floor(Number(value) || 0));
|
||||
}
|
||||
|
||||
function setStep405RecoveryCount(step, count) {
|
||||
const key = getStep405RecoveryStateKey(step);
|
||||
const value = String(Math.max(0, Math.floor(Number(count) || 0)));
|
||||
try {
|
||||
if (typeof sessionStorage !== 'undefined' && sessionStorage?.setItem) {
|
||||
sessionStorage.setItem(key, value);
|
||||
}
|
||||
} catch {}
|
||||
getStep405StorageScope()[key] = value;
|
||||
}
|
||||
|
||||
function clearStep405RecoveryCount(step) {
|
||||
const key = getStep405RecoveryStateKey(step);
|
||||
try {
|
||||
if (typeof sessionStorage !== 'undefined' && sessionStorage?.removeItem) {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
} catch {}
|
||||
try {
|
||||
delete getStep405StorageScope()[key];
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function createStep405RecoveryLimitError(step, count) {
|
||||
const normalizedStep = Number(step) || step || '?';
|
||||
const limit = getStep405RecoveryLimit(normalizedStep) || count;
|
||||
const message = `步骤 ${normalizedStep}:检测到 405 错误页面,已连续点击“重试”恢复 ${count}/${limit} 次仍未恢复,当前轮将结束并进入下一轮。URL: ${location.href}`;
|
||||
return new Error(`${getStep405RecoveryErrorPrefix(normalizedStep)}${message}`);
|
||||
}
|
||||
|
||||
async function handle405ResendError(step, remainingTimeout = 30000) {
|
||||
const currentCount = getStep405RecoveryCount(step);
|
||||
if (Number(step) === 4 && currentCount >= getStep405RecoveryLimit(step)) {
|
||||
throw createStep405RecoveryLimitError(step, currentCount);
|
||||
}
|
||||
|
||||
const nextCount = currentCount + 1;
|
||||
setStep405RecoveryCount(step, nextCount);
|
||||
const maxClickAttempts = Number(step) === 4 ? 1 : 5;
|
||||
await recoverCurrentAuthRetryPage({
|
||||
logLabel: `步骤 ${step}:检测到 405 错误页面,正在点击“重试”恢复`,
|
||||
logLabel: Number(step) === 4
|
||||
? `步骤 ${step}:检测到 405 错误页面,正在点击“重试”恢复(总计 ${nextCount}/${getStep405RecoveryLimit(step)})`
|
||||
: `步骤 ${step}:检测到 405 错误页面,正在点击“重试”恢复`,
|
||||
maxClickAttempts,
|
||||
pathPatterns: [],
|
||||
step,
|
||||
timeoutMs: Math.max(1000, remainingTimeout),
|
||||
});
|
||||
if (is405MethodNotAllowedPage()) {
|
||||
throw createStep405RecoveryLimitError(step, nextCount);
|
||||
}
|
||||
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
|
||||
log(`步骤 ${step}:405 错误已恢复,页面已返回验证码页面。`);
|
||||
}
|
||||
|
||||
@@ -1038,6 +1129,8 @@ const STEP5_SUBMIT_ERROR_PATTERN = /无法根据该信息创建帐户|请重试|
|
||||
const AUTH_TIMEOUT_ERROR_TITLE_PATTERN = /糟糕,出错了|something\s+went\s+wrong|oops/i;
|
||||
const AUTH_TIMEOUT_ERROR_DETAIL_PATTERN = /operation\s+timed\s+out|timed\s+out|请求超时|操作超时|failed\s+to\s+fetch|network\s+error|fetch\s+failed/i;
|
||||
const AUTH_ROUTE_ERROR_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+["']?\/email-verification/i;
|
||||
const STEP4_405_RECOVERY_ERROR_PREFIX = 'STEP4_405_RECOVERY_LIMIT::';
|
||||
const STEP4_405_RECOVERY_LIMIT = 3;
|
||||
const SIGNUP_USER_ALREADY_EXISTS_ERROR_PREFIX = 'SIGNUP_USER_ALREADY_EXISTS::';
|
||||
const SIGNUP_EMAIL_EXISTS_PATTERN = /与此电子邮件地址相关联的帐户已存在|account\s+associated\s+with\s+this\s+email\s+address\s+already\s+exists|email\s+address.*already\s+exists/i;
|
||||
|
||||
@@ -2546,6 +2639,7 @@ async function fillVerificationCode(step, payload) {
|
||||
if (step === 4) {
|
||||
const postVerificationState = getStep4PostVerificationState();
|
||||
if (postVerificationState?.state === 'logged_in_home') {
|
||||
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
|
||||
log(`步骤 ${step}:检测到页面已进入 ChatGPT 已登录态,本次验证码提交按成功处理。`, 'ok');
|
||||
return {
|
||||
success: true,
|
||||
@@ -2556,6 +2650,7 @@ async function fillVerificationCode(step, payload) {
|
||||
};
|
||||
}
|
||||
if (postVerificationState?.state === 'step5') {
|
||||
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
|
||||
log(`步骤 ${step}:检测到页面已进入下一阶段,本次验证码提交按成功处理。`, 'ok');
|
||||
return { success: true, assumed: true, alreadyAdvanced: true };
|
||||
}
|
||||
@@ -2664,6 +2759,7 @@ async function fillVerificationCode(step, payload) {
|
||||
} else if (outcome.addPhonePage) {
|
||||
log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn');
|
||||
} else {
|
||||
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
|
||||
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok');
|
||||
}
|
||||
if (combinedSignupProfilePage && !outcome.invalidCode) {
|
||||
@@ -2698,6 +2794,7 @@ async function fillVerificationCode(step, payload) {
|
||||
} else if (outcome.addPhonePage) {
|
||||
log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn');
|
||||
} else {
|
||||
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
|
||||
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok');
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
FR: ['fr', 'fra', 'france', '法国'],
|
||||
ID: ['id', 'indonesia', '印度尼西亚', '印尼'],
|
||||
JP: ['jp', 'jpn', 'japan', '日本', '日本国'],
|
||||
KR: ['kr', 'kor', 'korea', 'south korea', '韩国', '대한민국'],
|
||||
US: ['us', 'usa', 'united states', 'united states of america', 'america', '美国'],
|
||||
};
|
||||
|
||||
@@ -121,6 +122,28 @@
|
||||
},
|
||||
},
|
||||
],
|
||||
KR: [
|
||||
{
|
||||
query: 'Seoul Jung-gu',
|
||||
suggestionIndex: 1,
|
||||
fallback: {
|
||||
address1: 'Sejong-daero 110',
|
||||
city: 'Jung-gu',
|
||||
region: 'Seoul',
|
||||
postalCode: '04524',
|
||||
},
|
||||
},
|
||||
{
|
||||
query: 'Seoul Gangnam-gu',
|
||||
suggestionIndex: 1,
|
||||
fallback: {
|
||||
address1: 'Teheran-ro 152',
|
||||
city: 'Gangnam-gu',
|
||||
region: 'Seoul',
|
||||
postalCode: '06236',
|
||||
},
|
||||
},
|
||||
],
|
||||
US: [
|
||||
{
|
||||
query: 'New York NY',
|
||||
|
||||
@@ -30,8 +30,21 @@
|
||||
{ id: 13, order: 130, key: 'platform-verify', title: '平台回调验证' },
|
||||
];
|
||||
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_STEP_KEY = 'paypal-approve';
|
||||
const PLUS_GOPAY_STEP_DEFINITIONS = [
|
||||
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网' },
|
||||
{ id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱' },
|
||||
{ id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' },
|
||||
{ id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码' },
|
||||
{ id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日' },
|
||||
{ id: 6, order: 60, key: 'plus-checkout-create', title: '打开 GoPay 订阅页' },
|
||||
{ id: 7, order: 70, key: 'plus-checkout-billing', title: '填写 GoPay 账单并提交订阅' },
|
||||
{ id: 8, order: 80, key: 'paypal-approve', title: 'GoPay 手机验证与授权' },
|
||||
{ id: 9, order: 90, key: 'plus-checkout-return', title: '订阅回跳确认' },
|
||||
{ id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录' },
|
||||
{ id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码' },
|
||||
{ id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth' },
|
||||
{ id: 13, order: 130, key: 'platform-verify', title: '平台回调验证' },
|
||||
];
|
||||
|
||||
function isPlusModeEnabled(options = {}) {
|
||||
return Boolean(options?.plusModeEnabled || options?.plusMode);
|
||||
@@ -107,6 +120,17 @@
|
||||
return match ? cloneSteps([match], options)[0] : null;
|
||||
}
|
||||
|
||||
function getPlusPaymentStepTitle(options = {}) {
|
||||
if (!isPlusModeEnabled(options)) {
|
||||
return '';
|
||||
}
|
||||
const paymentStep = getModeStepDefinitions({
|
||||
...options,
|
||||
plusModeEnabled: true,
|
||||
}).find((step) => step.key === 'paypal-approve');
|
||||
return paymentStep?.title || '';
|
||||
}
|
||||
|
||||
return {
|
||||
STEP_DEFINITIONS: NORMAL_STEP_DEFINITIONS,
|
||||
NORMAL_STEP_DEFINITIONS,
|
||||
|
||||
@@ -344,7 +344,7 @@ OAuth 登录流程打开新标签页,避免复用旧页面导致状态污染
|
||||
|
||||
- 默认仍为 `HeroSMS`,老配置无需迁移。
|
||||
- 新增 `5sim` 平台,下拉框在接码设置卡片中直接可见。
|
||||
- 国家/地区列表、API Key、价格上限、余额、买号、查码、完成、取消、ban、复用均按当前 `phoneSmsProvider` 分发。
|
||||
- 国家/地区列表、API Key、价格上限、余额、买号、查码、完成、取消、ban、复用均按当前 `phoneSmsProvider` 分发。当前侧边栏只开放三个常用地区:印度尼西亚、泰国、越南。
|
||||
- HeroSMS 继续使用原 `heroSms*` 字段;5sim 使用独立 `fiveSim*` 字段,切换平台时不会互相覆盖草稿。
|
||||
|
||||
### 关键文件
|
||||
@@ -386,7 +386,7 @@ OAuth 登录流程打开新标签页,避免复用旧页面导致状态污染
|
||||
|
||||
1. 不要把真实 5sim key 写入代码、测试或文档;只允许用户在侧边栏输入。
|
||||
2. 上游同步时如果改了 Step 9 手机号验证,必须保留 `currentPhoneActivation.provider` 驱动的后续分发。
|
||||
3. 上游同步时如果改了 sidepanel 接码区域,必须保留 `select-phone-sms-provider`,且地区列表按平台加载:HeroSMS 数字国家 ID,5sim 字符串 country slug。
|
||||
3. 上游同步时如果改了 sidepanel 接码区域,必须保留 `select-phone-sms-provider`,且地区列表按平台加载:HeroSMS 数字国家 ID,5sim 字符串 country slug;当前只展示并保存印度尼西亚、泰国、越南,避免旧配置中的英国/美国等地区回写。
|
||||
4. 5sim 产品码当前固定为 `openai`,operator 默认 `any`;价格上限为空时按价格目录最低可用价尝试。
|
||||
|
||||
### 验证
|
||||
|
||||
@@ -6,11 +6,18 @@
|
||||
const DEFAULT_BASE_URL = 'https://5sim.net';
|
||||
const DEFAULT_PRODUCT = 'openai';
|
||||
const DEFAULT_OPERATOR = 'any';
|
||||
const DEFAULT_COUNTRY_ID = 'england';
|
||||
const DEFAULT_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_COUNTRY_ID = 'vietnam';
|
||||
const DEFAULT_COUNTRY_LABEL = '越南 (Vietnam)';
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 20000;
|
||||
const DEFAULT_MAX_USES = 3;
|
||||
const FIVE_SIM_RATE_LIMIT_ERROR_PREFIX = 'FIVE_SIM_RATE_LIMIT::';
|
||||
const MAX_PRICE_CANDIDATES = 8;
|
||||
const SUPPORTED_COUNTRY_ITEMS = Object.freeze([
|
||||
{ id: 'indonesia', label: '印度尼西亚 (Indonesia)' },
|
||||
{ id: 'thailand', label: '泰国 (Thailand)' },
|
||||
{ id: 'vietnam', label: '越南 (Vietnam)' },
|
||||
]);
|
||||
const SUPPORTED_COUNTRY_ID_SET = new Set(SUPPORTED_COUNTRY_ITEMS.map((item) => item.id));
|
||||
const COUNTRY_CN_BY_ID = Object.freeze({
|
||||
afghanistan: '阿富汗',
|
||||
albania: '阿尔巴尼亚',
|
||||
@@ -108,7 +115,15 @@
|
||||
|
||||
function normalizeFiveSimCountryId(value, fallback = DEFAULT_COUNTRY_ID) {
|
||||
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||
return normalized || fallback;
|
||||
if (SUPPORTED_COUNTRY_ID_SET.has(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
const fallbackSource = fallback === undefined || fallback === null ? DEFAULT_COUNTRY_ID : fallback;
|
||||
const normalizedFallback = String(fallbackSource).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||
if (!normalizedFallback) {
|
||||
return '';
|
||||
}
|
||||
return SUPPORTED_COUNTRY_ID_SET.has(normalizedFallback) ? normalizedFallback : DEFAULT_COUNTRY_ID;
|
||||
}
|
||||
|
||||
function getCountryIdFromPayload(record = {}, fallback = DEFAULT_COUNTRY_ID) {
|
||||
@@ -135,8 +150,13 @@
|
||||
const countryId = normalizeFiveSimCountryId(id, '');
|
||||
const english = normalizeFiveSimCountryLabel(englishValue || countryId || fallback, fallback);
|
||||
const chinese = COUNTRY_CN_BY_ID[countryId] || '';
|
||||
if (chinese && english && chinese.toLowerCase() !== english.toLowerCase() && !String(english).includes(chinese)) {
|
||||
return `${chinese} (${english})`;
|
||||
if (chinese && english) {
|
||||
if (String(english).includes(chinese)) {
|
||||
return english;
|
||||
}
|
||||
if (chinese.toLowerCase() !== english.toLowerCase()) {
|
||||
return `${chinese} (${english})`;
|
||||
}
|
||||
}
|
||||
return chinese || english;
|
||||
}
|
||||
@@ -323,9 +343,11 @@
|
||||
}
|
||||
|
||||
function resolveCountryConfig(state = {}) {
|
||||
const rawCountryId = normalizeFiveSimCountryId(state.fiveSimCountryId, '');
|
||||
const id = rawCountryId || DEFAULT_COUNTRY_ID;
|
||||
return {
|
||||
id: normalizeFiveSimCountryId(state.fiveSimCountryId),
|
||||
label: normalizeFiveSimCountryLabel(state.fiveSimCountryLabel),
|
||||
id,
|
||||
label: formatFiveSimCountryLabel(id, state.fiveSimCountryLabel || id, DEFAULT_COUNTRY_LABEL),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -343,7 +365,7 @@
|
||||
seen.add(nextId);
|
||||
candidates.push({
|
||||
id: nextId,
|
||||
label: normalizeFiveSimCountryLabel(entry.label, nextId),
|
||||
label: formatFiveSimCountryLabel(nextId, entry.label || nextId, nextId),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -392,8 +414,12 @@
|
||||
].filter(Boolean).join(' '),
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((left, right) => left.label.localeCompare(right.label));
|
||||
.filter((entry) => entry && SUPPORTED_COUNTRY_ID_SET.has(String(entry.id)))
|
||||
.sort((left, right) => {
|
||||
const leftIndex = SUPPORTED_COUNTRY_ITEMS.findIndex((item) => item.id === String(left.id));
|
||||
const rightIndex = SUPPORTED_COUNTRY_ITEMS.findIndex((item) => item.id === String(right.id));
|
||||
return leftIndex - rightIndex;
|
||||
});
|
||||
}
|
||||
|
||||
function collectPriceEntries(payload, entries = []) {
|
||||
@@ -531,6 +557,18 @@
|
||||
return /no\s+free\s+phones|no\s+numbers|not\s+found/i.test(text);
|
||||
}
|
||||
|
||||
function isRateLimitPayload(payloadOrMessage) {
|
||||
const text = describePayload(payloadOrMessage);
|
||||
return /rate\s*limit|too\s*many\s*requests|request\s*limit|429/i.test(text);
|
||||
}
|
||||
|
||||
function buildFiveSimRateLimitError(details = []) {
|
||||
const suffix = Array.isArray(details) && details.length
|
||||
? `:${details.join(' | ')}。`
|
||||
: '。';
|
||||
return new Error(`${FIVE_SIM_RATE_LIMIT_ERROR_PREFIX}5sim 购买接口触发限流,请稍后再试${suffix}`);
|
||||
}
|
||||
|
||||
function isTerminalError(payloadOrMessage) {
|
||||
const text = describePayload(payloadOrMessage);
|
||||
return /not\s+enough\s+(?:user\s+)?balance|not\s+enough\s+rating|unauthorized|invalid\s+token|banned|bad\s+(?:country|operator)|no\s+product|server\s+offline/i.test(text);
|
||||
@@ -554,11 +592,17 @@
|
||||
actionLabel: '5sim 购买手机号',
|
||||
}
|
||||
);
|
||||
return normalizeActivation(payload, {
|
||||
const activation = normalizeActivation(payload, {
|
||||
countryId: countryConfig.id,
|
||||
countryLabel: countryConfig.label,
|
||||
operator,
|
||||
});
|
||||
if (!activation) {
|
||||
const error = new Error(`5sim 购买手机号返回不可用响应:${describePayload(payload) || '空响应'}`);
|
||||
error.payload = payload;
|
||||
throw error;
|
||||
}
|
||||
return activation;
|
||||
}
|
||||
|
||||
async function requestActivation(state = {}, options = {}, deps = {}) {
|
||||
@@ -614,10 +658,13 @@
|
||||
}
|
||||
|
||||
const noNumbersByCountry = [];
|
||||
const rateLimitByCountry = [];
|
||||
let lastError = null;
|
||||
let lastFailureText = '';
|
||||
let sawOnlyRetryableFailures = true;
|
||||
for (const attempt of countryAttempts) {
|
||||
const countryConfig = attempt.countryConfig;
|
||||
const countryFailures = [];
|
||||
const pricePlan = attempt.pricePlan || await resolvePricePlan(state, countryConfig, deps);
|
||||
for (const maxPrice of pricePlan.prices) {
|
||||
try {
|
||||
@@ -631,18 +678,37 @@
|
||||
if (isTerminalError(payloadOrMessage)) {
|
||||
throw new Error(`5sim 购买手机号失败:${describePayload(payloadOrMessage) || '空响应'}`);
|
||||
}
|
||||
if (isNoNumbersPayload(payloadOrMessage)) {
|
||||
lastFailureText = describePayload(payloadOrMessage) || lastFailureText;
|
||||
const failureText = describePayload(payloadOrMessage) || lastFailureText;
|
||||
if (isRateLimitPayload(payloadOrMessage)) {
|
||||
lastFailureText = failureText;
|
||||
countryFailures.push(failureText || 'rate limit');
|
||||
continue;
|
||||
}
|
||||
if (isNoNumbersPayload(payloadOrMessage)) {
|
||||
lastFailureText = failureText;
|
||||
countryFailures.push(failureText || '无可用号码');
|
||||
continue;
|
||||
}
|
||||
sawOnlyRetryableFailures = false;
|
||||
lastError = error;
|
||||
lastFailureText = describePayload(payloadOrMessage) || lastFailureText;
|
||||
lastFailureText = failureText;
|
||||
}
|
||||
}
|
||||
noNumbersByCountry.push(`${countryConfig.label}: ${lastFailureText || '无可用号码'}`);
|
||||
const countryFailureText = countryFailures.length
|
||||
? Array.from(new Set(countryFailures)).join(', ')
|
||||
: (lastFailureText || '无可用号码');
|
||||
if (countryFailures.some((text) => isRateLimitPayload(text))) {
|
||||
rateLimitByCountry.push(`${countryConfig.label}: ${countryFailureText}`);
|
||||
} else {
|
||||
noNumbersByCountry.push(`${countryConfig.label}: ${countryFailureText}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (noNumbersByCountry.length) {
|
||||
if (rateLimitByCountry.length) {
|
||||
throw buildFiveSimRateLimitError(rateLimitByCountry);
|
||||
}
|
||||
|
||||
if (noNumbersByCountry.length && sawOnlyRetryableFailures) {
|
||||
throw new Error(`5sim 已尝试 ${countryCandidates.length} 个候选国家,均无可用号码:${noNumbersByCountry.join(' | ')}。`);
|
||||
}
|
||||
if (lastError) {
|
||||
@@ -782,6 +848,7 @@
|
||||
label: '5sim',
|
||||
defaultCountryId: DEFAULT_COUNTRY_ID,
|
||||
defaultCountryLabel: DEFAULT_COUNTRY_LABEL,
|
||||
supportedCountries: SUPPORTED_COUNTRY_ITEMS,
|
||||
defaultProduct: DEFAULT_PRODUCT,
|
||||
defaultOperator: DEFAULT_OPERATOR,
|
||||
normalizeCountryId: normalizeFiveSimCountryId,
|
||||
@@ -807,10 +874,12 @@
|
||||
|
||||
return {
|
||||
PROVIDER_ID,
|
||||
FIVE_SIM_RATE_LIMIT_ERROR_PREFIX,
|
||||
DEFAULT_COUNTRY_ID,
|
||||
DEFAULT_COUNTRY_LABEL,
|
||||
DEFAULT_OPERATOR,
|
||||
DEFAULT_PRODUCT,
|
||||
SUPPORTED_COUNTRY_ITEMS,
|
||||
createProvider,
|
||||
normalizeFiveSimCountryFallback,
|
||||
normalizeFiveSimCountryId,
|
||||
|
||||
+20
-12
@@ -1164,9 +1164,13 @@ function formatIpProxyRuntimeStatus(state = latestState) {
|
||||
}
|
||||
if (reason === 'connectivity_failed') {
|
||||
const prefix = endpointSummary ? `当前代理:${endpointSummary}${accountSourceTag}` : '当前代理:未知';
|
||||
const targetUnreachable = /真实目标|chatgpt\.com 不可达|target:page_context/i.test(errorText || details);
|
||||
const exitPart = exitIp ? `;当前出口:${exitSummary}` : '';
|
||||
return {
|
||||
stateClass: 'state-error',
|
||||
text: `${prefix};连通性失败,请切换节点或重试。`,
|
||||
text: targetUnreachable && exitIp
|
||||
? `${prefix}${exitPart};ChatGPT 目标不可达,请切换支持 ChatGPT 的节点。`
|
||||
: `${prefix}${exitPart};连通性失败,请切换节点或重试。`,
|
||||
details: errorText || details,
|
||||
hideCurrentDisplay: true,
|
||||
};
|
||||
@@ -1303,11 +1307,13 @@ function updateIpProxyUI(state = latestState) {
|
||||
if (rowIpProxyPoolTargetCount) {
|
||||
rowIpProxyPoolTargetCount.style.display = showSettings ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyAutoSyncEnabled) {
|
||||
rowIpProxyAutoSyncEnabled.style.display = showSettings ? '' : 'none';
|
||||
const autoSyncEnabledRow = typeof rowIpProxyAutoSyncEnabled !== 'undefined' ? rowIpProxyAutoSyncEnabled : null;
|
||||
const autoSyncIntervalRow = typeof rowIpProxyAutoSyncInterval !== 'undefined' ? rowIpProxyAutoSyncInterval : null;
|
||||
if (autoSyncEnabledRow) {
|
||||
autoSyncEnabledRow.style.display = showSettings ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyAutoSyncInterval) {
|
||||
rowIpProxyAutoSyncInterval.style.display = showSettings ? '' : 'none';
|
||||
if (autoSyncIntervalRow) {
|
||||
autoSyncIntervalRow.style.display = showSettings ? '' : 'none';
|
||||
}
|
||||
if (rowIpProxyHost) {
|
||||
rowIpProxyHost.style.display = showSettings && isAccountMode ? '' : 'none';
|
||||
@@ -1402,15 +1408,17 @@ function updateIpProxyUI(state = latestState) {
|
||||
if (inputIpProxyAccountList) {
|
||||
inputIpProxyAccountList.disabled = !enabled || !isAccountMode || !accountListAvailable;
|
||||
}
|
||||
if (inputIpProxyAutoSyncEnabled) {
|
||||
inputIpProxyAutoSyncEnabled.disabled = !enabled;
|
||||
const autoSyncEnabledInput = typeof inputIpProxyAutoSyncEnabled !== 'undefined' ? inputIpProxyAutoSyncEnabled : null;
|
||||
const autoSyncIntervalInput = typeof inputIpProxyAutoSyncIntervalMinutes !== 'undefined' ? inputIpProxyAutoSyncIntervalMinutes : null;
|
||||
if (autoSyncEnabledInput) {
|
||||
autoSyncEnabledInput.disabled = !enabled;
|
||||
}
|
||||
if (inputIpProxyAutoSyncIntervalMinutes) {
|
||||
const autoSyncEnabled = Boolean(inputIpProxyAutoSyncEnabled?.checked);
|
||||
if (!Number.isFinite(Number.parseInt(String(inputIpProxyAutoSyncIntervalMinutes.value || '').trim(), 10))) {
|
||||
inputIpProxyAutoSyncIntervalMinutes.value = '15';
|
||||
if (autoSyncIntervalInput) {
|
||||
const autoSyncEnabled = Boolean(autoSyncEnabledInput?.checked);
|
||||
if (!Number.isFinite(Number.parseInt(String(autoSyncIntervalInput.value || '').trim(), 10))) {
|
||||
autoSyncIntervalInput.value = '15';
|
||||
}
|
||||
inputIpProxyAutoSyncIntervalMinutes.disabled = !enabled || !autoSyncEnabled;
|
||||
autoSyncIntervalInput.disabled = !enabled || !autoSyncEnabled;
|
||||
}
|
||||
|
||||
const runtimeStatus = formatIpProxyRuntimeStatus(runtimeState);
|
||||
|
||||
+331
-61
@@ -424,8 +424,13 @@ const btnAutoStartRestart = document.getElementById('btn-auto-start-restart');
|
||||
const btnAutoStartContinue = document.getElementById('btn-auto-start-continue');
|
||||
const autoHintText = document.querySelector('.auto-hint');
|
||||
const stepsList = document.querySelector('.steps-list');
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
let currentPlusModeEnabled = false;
|
||||
let currentPlusPaymentMethod = 'paypal';
|
||||
let currentPlusPaymentMethod = DEFAULT_PLUS_PAYMENT_METHOD;
|
||||
let activePlusManualConfirmationRequestId = '';
|
||||
let plusManualConfirmationDialogInFlight = false;
|
||||
let heroSmsCountrySelectionOrder = [];
|
||||
let phoneSmsProviderOrderSelection = [];
|
||||
let heroSmsCountryMenuSearchKeyword = '';
|
||||
@@ -492,14 +497,13 @@ const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH = 'price_high';
|
||||
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||
const HERO_SMS_FALLBACK_COUNTRY_ITEMS = Object.freeze([
|
||||
const HERO_SMS_SUPPORTED_COUNTRY_ITEMS = Object.freeze([
|
||||
{ id: 6, chn: '印度尼西亚', eng: 'Indonesia' },
|
||||
{ id: 52, chn: '泰国', eng: 'Thailand' },
|
||||
{ id: 187, chn: '美国(物理)', eng: 'USA' },
|
||||
{ id: 16, chn: '英国', eng: 'United Kingdom' },
|
||||
{ id: 151, chn: '日本', eng: 'Japan' },
|
||||
{ id: 43, chn: '德国', eng: 'Germany' },
|
||||
{ id: 73, chn: '法国', eng: 'France' },
|
||||
{ id: 10, chn: '越南', eng: 'Vietnam' },
|
||||
]);
|
||||
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(HERO_SMS_SUPPORTED_COUNTRY_ITEMS.map((item) => String(item.id)));
|
||||
const HERO_SMS_FALLBACK_COUNTRY_ITEMS = HERO_SMS_SUPPORTED_COUNTRY_ITEMS;
|
||||
const FIVE_SIM_COUNTRY_CN_BY_ID = Object.freeze({
|
||||
afghanistan: '阿富汗',
|
||||
albania: '阿尔巴尼亚',
|
||||
@@ -645,9 +649,13 @@ const PLUS_CONTRIBUTION_PROMPT_LEDGER_STORAGE_KEY = 'multipage-plus-contribution
|
||||
const PHONE_VERIFICATION_SECTION_EXPANDED_STORAGE_KEY = 'multipage-phone-verification-section-expanded';
|
||||
|
||||
function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
|
||||
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal';
|
||||
const rawPaymentMethod = typeof options === 'string'
|
||||
? options
|
||||
: (options.plusPaymentMethod || currentPlusPaymentMethod || defaultMethod);
|
||||
return (window.MultiPageStepDefinitions?.getSteps?.({
|
||||
plusModeEnabled,
|
||||
plusPaymentMethod: options.plusPaymentMethod || selectPlusPaymentMethod?.value || 'paypal',
|
||||
plusPaymentMethod: normalizePlusPaymentMethod(rawPaymentMethod),
|
||||
}) || [])
|
||||
.sort((left, right) => {
|
||||
const leftOrder = Number.isFinite(left.order) ? left.order : left.id;
|
||||
@@ -659,7 +667,12 @@ function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
|
||||
|
||||
function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
|
||||
currentPlusModeEnabled = Boolean(plusModeEnabled);
|
||||
stepDefinitions = getStepDefinitionsForMode(currentPlusModeEnabled, options);
|
||||
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal';
|
||||
const rawPaymentMethod = typeof options === 'string'
|
||||
? options
|
||||
: (options.plusPaymentMethod || currentPlusPaymentMethod || defaultMethod);
|
||||
currentPlusPaymentMethod = normalizePlusPaymentMethod(rawPaymentMethod);
|
||||
stepDefinitions = getStepDefinitionsForMode(currentPlusModeEnabled, currentPlusPaymentMethod);
|
||||
STEP_IDS = stepDefinitions.map((step) => Number(step.id)).filter(Number.isFinite);
|
||||
STEP_DEFAULT_STATUSES = Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending']));
|
||||
SKIPPABLE_STEPS = new Set(STEP_IDS);
|
||||
@@ -688,12 +701,15 @@ const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
|
||||
const FIVE_SIM_SUPPORTED_COUNTRY_ITEMS = Object.freeze([
|
||||
{ id: 'indonesia', chn: '印度尼西亚', eng: 'Indonesia', searchText: 'indonesia 印度尼西亚 印尼 Indonesia ID +62' },
|
||||
{ id: 'thailand', chn: '泰国', eng: 'Thailand', searchText: 'thailand 泰国 Thailand TH +66' },
|
||||
{ id: 'vietnam', chn: '越南', eng: 'Vietnam', searchText: 'vietnam 越南 Vietnam VN +84' },
|
||||
]);
|
||||
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(FIVE_SIM_SUPPORTED_COUNTRY_ITEMS.map((item) => item.id));
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
const DEFAULT_IP_PROXY_SERVICE = '711proxy';
|
||||
const SUPPORTED_IP_PROXY_SERVICES = ['711proxy', 'lumiproxy', 'iproyal', 'omegaproxy'];
|
||||
const IP_PROXY_ENABLED_SERVICES = ['711proxy'];
|
||||
@@ -1897,19 +1913,23 @@ function syncLatestState(nextState) {
|
||||
}
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
if (window.GoPayUtils?.normalizePlusPaymentMethod) {
|
||||
return window.GoPayUtils.normalizePlusPaymentMethod(value);
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizePlusPaymentMethod) {
|
||||
return rootScope.GoPayUtils.normalizePlusPaymentMethod(value);
|
||||
}
|
||||
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY
|
||||
? PLUS_PAYMENT_METHOD_GOPAY
|
||||
: PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay';
|
||||
const paypalValue = typeof PLUS_PAYMENT_METHOD_PAYPAL !== 'undefined' ? PLUS_PAYMENT_METHOD_PAYPAL : 'paypal';
|
||||
return String(value || '').trim().toLowerCase() === gopayValue
|
||||
? gopayValue
|
||||
: paypalValue;
|
||||
}
|
||||
|
||||
function getSelectedPlusPaymentMethod(state = latestState) {
|
||||
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal';
|
||||
if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod?.value) {
|
||||
return normalizePlusPaymentMethod(selectPlusPaymentMethod.value);
|
||||
}
|
||||
return normalizePlusPaymentMethod(state?.plusPaymentMethod || DEFAULT_PLUS_PAYMENT_METHOD);
|
||||
return normalizePlusPaymentMethod(state?.plusPaymentMethod || currentPlusPaymentMethod || defaultMethod);
|
||||
}
|
||||
|
||||
function hasOwnStateValue(source, key) {
|
||||
@@ -1979,6 +1999,18 @@ function isAutoRunScheduledPhase() {
|
||||
return currentAutoRun.phase === 'scheduled';
|
||||
}
|
||||
|
||||
function isAutoRunSourceSyncPhase(phase) {
|
||||
return ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase);
|
||||
}
|
||||
|
||||
function shouldSyncRunCountFromAutoRunSource(source = {}) {
|
||||
const phase = source.autoRunPhase ?? source.phase ?? currentAutoRun.phase;
|
||||
const autoRunning = source.autoRunning !== undefined
|
||||
? Boolean(source.autoRunning)
|
||||
: isAutoRunSourceSyncPhase(phase);
|
||||
return autoRunning || isAutoRunSourceSyncPhase(phase);
|
||||
}
|
||||
|
||||
function getAutoRunLabel(payload = currentAutoRun) {
|
||||
if ((payload.phase ?? currentAutoRun.phase) === 'scheduled') {
|
||||
return (payload.totalRuns || 1) > 1 ? ` (${payload.totalRuns}轮)` : '';
|
||||
@@ -2174,7 +2206,7 @@ function getLockedRunCountFromEmailPool(provider = selectMailProvider.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function shouldLockRunCountToEmailPool(provider = selectMailProvider.value) {
|
||||
function shouldLockRunCountToEmailPool(provider = (typeof selectMailProvider !== 'undefined' ? selectMailProvider?.value : undefined)) {
|
||||
return getLockedRunCountFromEmailPool(provider) > 0;
|
||||
}
|
||||
|
||||
@@ -3007,6 +3039,12 @@ function collectSettingsPayload() {
|
||||
fiveSimCountryId: fiveSimCountry.id,
|
||||
fiveSimCountryLabel: fiveSimCountry.label,
|
||||
fiveSimCountryFallback,
|
||||
fiveSimCountryOrder: [
|
||||
fiveSimCountry,
|
||||
...fiveSimCountryFallback,
|
||||
]
|
||||
.map((country) => normalizeFiveSimCountryId(country?.id, ''))
|
||||
.filter(Boolean),
|
||||
fiveSimMaxPrice: fiveSimMaxPriceValue,
|
||||
fiveSimOperator: fiveSimOperatorValue,
|
||||
};
|
||||
@@ -3124,11 +3162,22 @@ function normalizePhoneSmsMaxPriceValue(value = '', provider = getSelectedPhoneS
|
||||
}
|
||||
|
||||
function normalizeFiveSimCountryId(value, fallback = DEFAULT_FIVE_SIM_COUNTRY_ID) {
|
||||
if (typeof window !== 'undefined' && window.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryId) {
|
||||
return window.PhoneSmsFiveSimProvider.normalizeFiveSimCountryId(value, fallback);
|
||||
const supportedCountryIds = typeof FIVE_SIM_SUPPORTED_COUNTRY_ID_SET !== 'undefined'
|
||||
? FIVE_SIM_SUPPORTED_COUNTRY_ID_SET
|
||||
: new Set(['indonesia', 'thailand', 'vietnam']);
|
||||
const fallbackSource = fallback === undefined || fallback === null ? DEFAULT_FIVE_SIM_COUNTRY_ID : fallback;
|
||||
const normalizedFallback = String(fallbackSource).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||
const rawNormalized = typeof window !== 'undefined' && window.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryId
|
||||
? window.PhoneSmsFiveSimProvider.normalizeFiveSimCountryId(value, '')
|
||||
: String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||
const normalized = String(rawNormalized || '').trim().toLowerCase();
|
||||
if (supportedCountryIds.has(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||
return normalized || fallback;
|
||||
if (!normalizedFallback) {
|
||||
return '';
|
||||
}
|
||||
return supportedCountryIds.has(normalizedFallback) ? normalizedFallback : DEFAULT_FIVE_SIM_COUNTRY_ID;
|
||||
}
|
||||
|
||||
function normalizeFiveSimCountryLabel(value = '', fallback = DEFAULT_FIVE_SIM_COUNTRY_LABEL) {
|
||||
@@ -3145,8 +3194,13 @@ function formatFiveSimCountryDisplayLabel(id = '', englishValue = '', fallback =
|
||||
const countryId = normalizeFiveSimCountryId(id, '');
|
||||
const english = normalizeFiveSimCountryLabel(englishValue || countryId || fallback, fallback);
|
||||
const chinese = FIVE_SIM_COUNTRY_CN_BY_ID[countryId] || '';
|
||||
if (chinese && english && chinese.toLowerCase() !== english.toLowerCase() && !String(english).includes(chinese)) {
|
||||
return `${chinese} (${english})`;
|
||||
if (chinese && english) {
|
||||
if (String(english).includes(chinese)) {
|
||||
return english;
|
||||
}
|
||||
if (chinese.toLowerCase() !== english.toLowerCase()) {
|
||||
return `${chinese} (${english})`;
|
||||
}
|
||||
}
|
||||
return chinese || english;
|
||||
}
|
||||
@@ -3211,8 +3265,16 @@ function normalizeFiveSimCountryFallbackList(value = []) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeHeroSmsCountryId(value) {
|
||||
return Math.max(1, Math.floor(Number(value) || DEFAULT_HERO_SMS_COUNTRY_ID));
|
||||
function normalizeHeroSmsCountryId(value, fallback = DEFAULT_HERO_SMS_COUNTRY_ID) {
|
||||
const supportedCountryIds = typeof HERO_SMS_SUPPORTED_COUNTRY_ID_SET !== 'undefined'
|
||||
? HERO_SMS_SUPPORTED_COUNTRY_ID_SET
|
||||
: new Set(['6', '52', '10']);
|
||||
const parsed = Math.floor(Number(value));
|
||||
if (Number.isFinite(parsed) && supportedCountryIds.has(String(parsed))) {
|
||||
return parsed;
|
||||
}
|
||||
const fallbackParsed = Math.floor(Number(fallback));
|
||||
return supportedCountryIds.has(String(fallbackParsed)) ? fallbackParsed : DEFAULT_HERO_SMS_COUNTRY_ID;
|
||||
}
|
||||
|
||||
function normalizeHeroSmsCountryLabel(value = '') {
|
||||
@@ -3831,7 +3893,10 @@ function normalizeHeroSmsCountryFallbackList(value = []) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!id || seen.has(id)) {
|
||||
const supportedCountryIds = typeof HERO_SMS_SUPPORTED_COUNTRY_ID_SET !== 'undefined'
|
||||
? HERO_SMS_SUPPORTED_COUNTRY_ID_SET
|
||||
: new Set(['6', '52', '10']);
|
||||
if (!id || !supportedCountryIds.has(String(id)) || seen.has(id)) {
|
||||
return;
|
||||
}
|
||||
seen.add(id);
|
||||
@@ -4005,7 +4070,9 @@ function collectHeroSmsPriceEntriesForPreview(payload, entries = []) {
|
||||
const physicalCount = Number(payload.physicalCount);
|
||||
const hasCount = Number.isFinite(count);
|
||||
const hasPhysicalCount = Number.isFinite(physicalCount);
|
||||
const stockCount = Math.max(hasCount ? count : 0, hasPhysicalCount ? physicalCount : 0);
|
||||
const stockCount = hasPhysicalCount
|
||||
? physicalCount
|
||||
: (hasCount ? count : 0);
|
||||
const hasStockField = hasCount || hasPhysicalCount;
|
||||
entries.push({
|
||||
cost: directPrice,
|
||||
@@ -4963,8 +5030,12 @@ async function loadHeroSmsCountries() {
|
||||
].filter(Boolean).join(' '),
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry.id)
|
||||
.sort((left, right) => left.label.localeCompare(right.label));
|
||||
.filter((entry) => entry.id && FIVE_SIM_SUPPORTED_COUNTRY_ID_SET.has(String(entry.id)))
|
||||
.sort((left, right) => {
|
||||
const leftIndex = FIVE_SIM_SUPPORTED_COUNTRY_ITEMS.findIndex((item) => item.id === String(left.id));
|
||||
const rightIndex = FIVE_SIM_SUPPORTED_COUNTRY_ITEMS.findIndex((item) => item.id === String(right.id));
|
||||
return leftIndex - rightIndex;
|
||||
});
|
||||
if (!optionItems.length) {
|
||||
throw new Error('empty country list');
|
||||
}
|
||||
@@ -4974,12 +5045,11 @@ async function loadHeroSmsCountries() {
|
||||
applyOptions(optionItems, selectHeroSmsCountryFallback);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load 5sim countries:', error);
|
||||
const fallbackItems = [
|
||||
{ id: 'england', label: formatFiveSimCountryDisplayLabel('england', 'England'), searchText: 'england 英国 England uk gb united kingdom' },
|
||||
{ id: 'usa', label: formatFiveSimCountryDisplayLabel('usa', 'USA'), searchText: 'usa 美国 US United States' },
|
||||
{ id: 'thailand', label: formatFiveSimCountryDisplayLabel('thailand', 'Thailand'), searchText: 'thailand 泰国 TH' },
|
||||
{ id: 'japan', label: formatFiveSimCountryDisplayLabel('japan', 'Japan'), searchText: 'japan 日本 JP' },
|
||||
];
|
||||
const fallbackItems = FIVE_SIM_SUPPORTED_COUNTRY_ITEMS.map((item) => ({
|
||||
id: item.id,
|
||||
label: formatFiveSimCountryDisplayLabel(item.id, item.eng),
|
||||
searchText: item.searchText,
|
||||
}));
|
||||
applyOptions(fallbackItems, selectHeroSmsCountry);
|
||||
applyOptions(fallbackItems, selectHeroSmsCountryFallback);
|
||||
heroSmsCountrySearchTextById.clear();
|
||||
@@ -5002,8 +5072,12 @@ async function loadHeroSmsCountries() {
|
||||
throw new Error('empty country list');
|
||||
}
|
||||
const optionItems = countries
|
||||
.filter((item) => Number(item?.id) > 0 && (String(item?.eng || '').trim() || String(item?.chn || '').trim()))
|
||||
.sort((left, right) => String(left.eng || '').localeCompare(String(right.eng || '')))
|
||||
.filter((item) => HERO_SMS_SUPPORTED_COUNTRY_ID_SET.has(String(Math.floor(Number(item?.id)))) && (String(item?.eng || '').trim() || String(item?.chn || '').trim()))
|
||||
.sort((left, right) => {
|
||||
const leftIndex = HERO_SMS_SUPPORTED_COUNTRY_ITEMS.findIndex((item) => String(item.id) === String(Math.floor(Number(left?.id))));
|
||||
const rightIndex = HERO_SMS_SUPPORTED_COUNTRY_ITEMS.findIndex((item) => String(item.id) === String(Math.floor(Number(right?.id))));
|
||||
return leftIndex - rightIndex;
|
||||
})
|
||||
.map((item) => {
|
||||
const id = normalizeHeroSmsCountryId(item.id);
|
||||
const label = buildHeroSmsCountryDisplayLabel(item);
|
||||
@@ -6049,15 +6123,21 @@ function updatePhoneVerificationSettingsUI() {
|
||||
}
|
||||
|
||||
function updatePlusModeUI() {
|
||||
const paypalValue = typeof PLUS_PAYMENT_METHOD_PAYPAL !== 'undefined' ? PLUS_PAYMENT_METHOD_PAYPAL : 'paypal';
|
||||
const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay';
|
||||
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : paypalValue;
|
||||
const enabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
|
||||
? Boolean(inputPlusModeEnabled.checked)
|
||||
: false;
|
||||
const method = enabled ? getSelectedPlusPaymentMethod() : DEFAULT_PLUS_PAYMENT_METHOD;
|
||||
const method = enabled ? getSelectedPlusPaymentMethod() : defaultMethod;
|
||||
if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) {
|
||||
selectPlusPaymentMethod.value = method;
|
||||
if (selectPlusPaymentMethod.style) {
|
||||
selectPlusPaymentMethod.style.display = enabled ? '' : 'none';
|
||||
}
|
||||
}
|
||||
if (typeof plusPaymentMethodCaption !== 'undefined' && plusPaymentMethodCaption) {
|
||||
plusPaymentMethodCaption.textContent = method === PLUS_PAYMENT_METHOD_GOPAY
|
||||
plusPaymentMethodCaption.textContent = method === gopayValue
|
||||
? 'GoPay 印尼订阅链路'
|
||||
: 'PayPal 订阅链路';
|
||||
}
|
||||
@@ -6075,7 +6155,10 @@ function updatePlusModeUI() {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
row.style.display = enabled && method === PLUS_PAYMENT_METHOD_PAYPAL ? '' : 'none';
|
||||
const selectedMethod = typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod?.value
|
||||
? normalizePlusPaymentMethod(selectPlusPaymentMethod.value)
|
||||
: method;
|
||||
row.style.display = enabled && selectedMethod === paypalValue ? '' : 'none';
|
||||
});
|
||||
[
|
||||
typeof rowGoPayCountryCode !== 'undefined' ? rowGoPayCountryCode : null,
|
||||
@@ -6086,7 +6169,10 @@ function updatePlusModeUI() {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
row.style.display = enabled && method === PLUS_PAYMENT_METHOD_GOPAY ? '' : 'none';
|
||||
const selectedMethod = typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod?.value
|
||||
? normalizePlusPaymentMethod(selectPlusPaymentMethod.value)
|
||||
: method;
|
||||
row.style.display = enabled && selectedMethod === gopayValue ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6117,6 +6203,97 @@ async function setRuntimeEmailState(email) {
|
||||
return normalizedEmail;
|
||||
}
|
||||
|
||||
async function openPlusManualConfirmationDialog(options = {}) {
|
||||
const method = String(options.method || '').trim().toLowerCase();
|
||||
const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay';
|
||||
const title = String(options.title || '').trim() || (method === gopayValue ? 'GoPay 订阅确认' : '手动确认');
|
||||
const message = String(options.message || '').trim()
|
||||
|| (method === gopayValue
|
||||
? '请在当前订阅页中手动完成 GoPay 订阅,完成后点击“我已完成订阅”继续。'
|
||||
: '请先在页面中完成当前手动操作,完成后点击确认继续。');
|
||||
return openActionModal({
|
||||
title,
|
||||
message,
|
||||
actions: [
|
||||
{ id: 'cancel', label: '取消等待', variant: 'btn-ghost' },
|
||||
{ id: 'confirm', label: '我已完成订阅', variant: 'btn-primary' },
|
||||
],
|
||||
alert: method === gopayValue
|
||||
? { text: '确认后流程会直接继续到 Plus 模式第 10 步 OAuth 登录。', tone: 'info' }
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
async function syncPlusManualConfirmationDialog() {
|
||||
const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay';
|
||||
const requestId = String(latestState?.plusManualConfirmationRequestId || '').trim();
|
||||
const pending = Boolean(latestState?.plusManualConfirmationPending);
|
||||
if (!pending || !requestId || plusManualConfirmationDialogInFlight || activePlusManualConfirmationRequestId === requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const step = Number(latestState?.plusManualConfirmationStep) || 0;
|
||||
const method = String(latestState?.plusManualConfirmationMethod || '').trim().toLowerCase();
|
||||
const title = latestState?.plusManualConfirmationTitle;
|
||||
const message = latestState?.plusManualConfirmationMessage;
|
||||
activePlusManualConfirmationRequestId = requestId;
|
||||
plusManualConfirmationDialogInFlight = true;
|
||||
let shouldReopenDialog = false;
|
||||
|
||||
try {
|
||||
const choice = await openPlusManualConfirmationDialog({
|
||||
method,
|
||||
title,
|
||||
message,
|
||||
});
|
||||
const currentRequestId = String(latestState?.plusManualConfirmationRequestId || '').trim();
|
||||
const stillPending = Boolean(latestState?.plusManualConfirmationPending);
|
||||
if (!stillPending || currentRequestId !== requestId) {
|
||||
return;
|
||||
}
|
||||
if (choice == null) {
|
||||
shouldReopenDialog = true;
|
||||
showToast('当前订阅确认仍在等待中,将重新弹出确认窗口。', 'info', 1800);
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = choice === 'confirm';
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'RESOLVE_PLUS_MANUAL_CONFIRMATION',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
step,
|
||||
requestId,
|
||||
confirmed,
|
||||
},
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
if (confirmed) {
|
||||
showToast(method === gopayValue ? 'GoPay 订阅已确认,正在继续 OAuth 登录...' : '已确认,流程继续执行中...', 'info', 2200);
|
||||
} else {
|
||||
showToast(method === gopayValue ? '已取消 GoPay 订阅等待。' : '已取消当前手动确认。', 'warn', 2200);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(error?.message || String(error || '未知错误'), 'error');
|
||||
} finally {
|
||||
if (activePlusManualConfirmationRequestId === requestId) {
|
||||
activePlusManualConfirmationRequestId = '';
|
||||
}
|
||||
plusManualConfirmationDialogInFlight = false;
|
||||
if (
|
||||
shouldReopenDialog
|
||||
&& latestState?.plusManualConfirmationPending
|
||||
&& String(latestState?.plusManualConfirmationRequestId || '').trim() === requestId
|
||||
) {
|
||||
setTimeout(() => {
|
||||
void syncPlusManualConfirmationDialog();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function clearRegistrationEmail(options = {}) {
|
||||
const { silent = false } = options;
|
||||
if (!inputEmail.value.trim() && !latestState?.email) {
|
||||
@@ -6223,13 +6400,11 @@ function applyAutoRunStatus(payload = currentAutoRun) {
|
||||
|
||||
setSettingsCardLocked(settingsCardLocked);
|
||||
|
||||
const lockedRunCount = getLockedRunCountFromEmailPool();
|
||||
const shouldSyncAutoRunTotalRuns = currentAutoRun.autoRunning
|
||||
|| locked
|
||||
|| paused
|
||||
|| scheduled;
|
||||
|
||||
inputRunCount.disabled = currentAutoRun.autoRunning || lockedRunCount > 0;
|
||||
inputRunCount.disabled = currentAutoRun.autoRunning || (
|
||||
typeof shouldLockRunCountToEmailPool === 'function'
|
||||
? shouldLockRunCountToEmailPool()
|
||||
: getLockedRunCountFromEmailPool() > 0
|
||||
);
|
||||
btnAutoRun.disabled = currentAutoRun.autoRunning;
|
||||
btnFetchEmail.disabled = locked
|
||||
|| isCustomMailProvider()
|
||||
@@ -6237,9 +6412,18 @@ function applyAutoRunStatus(payload = currentAutoRun) {
|
||||
inputEmail.disabled = locked;
|
||||
inputAutoSkipFailures.disabled = scheduled;
|
||||
|
||||
const lockedRunCount = typeof getLockedRunCountFromEmailPool === 'function'
|
||||
? getLockedRunCountFromEmailPool()
|
||||
: 0;
|
||||
const isSyncPhase = typeof isAutoRunSourceSyncPhase === 'function'
|
||||
? isAutoRunSourceSyncPhase
|
||||
: (phase) => ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase);
|
||||
const shouldSyncRunCount = typeof shouldSyncRunCountFromAutoRunSource === 'function'
|
||||
? shouldSyncRunCountFromAutoRunSource(currentAutoRun)
|
||||
: (currentAutoRun.autoRunning || isSyncPhase(currentAutoRun.phase));
|
||||
if (lockedRunCount > 0) {
|
||||
inputRunCount.value = String(lockedRunCount);
|
||||
} else if (shouldSyncAutoRunTotalRuns && currentAutoRun.totalRuns > 0) {
|
||||
} else if (shouldSyncRunCount && currentAutoRun.totalRuns > 0) {
|
||||
inputRunCount.value = String(currentAutoRun.totalRuns);
|
||||
}
|
||||
|
||||
@@ -6340,21 +6524,31 @@ function renderStepsList() {
|
||||
updateButtonStates();
|
||||
}
|
||||
|
||||
function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethod = 'paypal', options = {}) {
|
||||
function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOrOptions = {}, maybeOptions = {}) {
|
||||
const nextPlusModeEnabled = Boolean(plusModeEnabled);
|
||||
const nextPaymentMethod = normalizePlusPaymentMethod(options.plusPaymentMethod || getSelectedPlusPaymentMethod(latestState));
|
||||
const options = typeof plusPaymentMethodOrOptions === 'string'
|
||||
? maybeOptions
|
||||
: (plusPaymentMethodOrOptions || {});
|
||||
const rawPaymentMethod = typeof plusPaymentMethodOrOptions === 'string'
|
||||
? plusPaymentMethodOrOptions
|
||||
: (options.plusPaymentMethod || getSelectedPlusPaymentMethod(latestState));
|
||||
const nextPaymentMethod = normalizePlusPaymentMethod(rawPaymentMethod);
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const currentPaymentStep = stepDefinitions.find((step) => step.key === 'paypal-approve');
|
||||
const nextPaymentTitle = window.MultiPageStepDefinitions?.getPlusPaymentStepTitle?.({
|
||||
const nextPaymentTitle = rootScope.MultiPageStepDefinitions?.getPlusPaymentStepTitle?.({
|
||||
plusModeEnabled: nextPlusModeEnabled,
|
||||
plusPaymentMethod: nextPaymentMethod,
|
||||
});
|
||||
const paymentTitleChanged = Boolean(nextPlusModeEnabled && currentPaymentStep && nextPaymentTitle && currentPaymentStep.title !== nextPaymentTitle);
|
||||
const shouldRender = Boolean(options.render) || nextPlusModeEnabled !== currentPlusModeEnabled || paymentTitleChanged;
|
||||
const shouldRender = Boolean(options.render)
|
||||
|| nextPlusModeEnabled !== currentPlusModeEnabled
|
||||
|| nextPaymentMethod !== currentPlusPaymentMethod
|
||||
|| paymentTitleChanged;
|
||||
if (!shouldRender) {
|
||||
return;
|
||||
}
|
||||
|
||||
rebuildStepDefinitionState(nextPlusModeEnabled, { plusPaymentMethod: nextPaymentMethod });
|
||||
rebuildStepDefinitionState(nextPlusModeEnabled, nextPaymentMethod);
|
||||
renderStepsList();
|
||||
}
|
||||
|
||||
@@ -6755,7 +6949,13 @@ function applySettingsState(state) {
|
||||
if (typeof updateHeroSmsRuntimeDisplay === 'function') {
|
||||
updateHeroSmsRuntimeDisplay(state);
|
||||
}
|
||||
if (state?.autoRunTotalRuns) {
|
||||
const isSyncPhase = typeof isAutoRunSourceSyncPhase === 'function'
|
||||
? isAutoRunSourceSyncPhase
|
||||
: (phase) => ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase);
|
||||
const shouldSyncInitialRunCount = typeof shouldSyncRunCountFromAutoRunSource === 'function'
|
||||
? shouldSyncRunCountFromAutoRunSource(state)
|
||||
: (Boolean(state?.autoRunning) || isSyncPhase(state?.autoRunPhase ?? state?.phase));
|
||||
if (state?.autoRunTotalRuns && shouldSyncInitialRunCount) {
|
||||
inputRunCount.value = String(state.autoRunTotalRuns);
|
||||
}
|
||||
|
||||
@@ -10418,14 +10618,68 @@ inputAutoDelayMinutes.addEventListener('blur', () => {
|
||||
});
|
||||
|
||||
|
||||
|
||||
function getPhoneSmsCountrySelectionForProvider(provider = getSelectedPhoneSmsProvider(), options = {}) {
|
||||
const normalizedProvider = normalizePhoneSmsProvider(provider);
|
||||
const countrySelect = selectHeroSmsCountry || selectHeroSmsCountryFallback;
|
||||
const selectionLimit = Math.max(1, Math.floor(Number(options.maxSelection) || HERO_SMS_COUNTRY_SELECTION_MAX));
|
||||
const ensureDefault = options.ensureDefault !== false;
|
||||
const defaultCountry = normalizedProvider === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
? { id: DEFAULT_FIVE_SIM_COUNTRY_ID, label: DEFAULT_FIVE_SIM_COUNTRY_LABEL }
|
||||
: { id: DEFAULT_HERO_SMS_COUNTRY_ID, label: DEFAULT_HERO_SMS_COUNTRY_LABEL };
|
||||
|
||||
if (!countrySelect) {
|
||||
return ensureDefault ? [defaultCountry] : [];
|
||||
}
|
||||
|
||||
const optionByValue = new Map(
|
||||
Array.from(countrySelect.options || []).map((option) => [String(option.value), option])
|
||||
);
|
||||
const selectedIds = Array.from(countrySelect.options || [])
|
||||
.filter((option) => option.selected)
|
||||
.map((option) => normalizePhoneSmsCountryId(option.value, normalizedProvider))
|
||||
.filter(Boolean);
|
||||
|
||||
if (!selectedIds.length && !countrySelect.multiple) {
|
||||
const fallbackId = normalizePhoneSmsCountryId(countrySelect.value, normalizedProvider);
|
||||
if (fallbackId) {
|
||||
selectedIds.push(fallbackId);
|
||||
}
|
||||
}
|
||||
|
||||
const selectedSet = new Set(selectedIds.map((id) => String(id)));
|
||||
let orderedIds = heroSmsCountrySelectionOrder
|
||||
.map((id) => normalizePhoneSmsCountryId(id, normalizedProvider))
|
||||
.filter((id) => id && selectedSet.has(String(id)));
|
||||
|
||||
selectedIds.forEach((id) => {
|
||||
if (!orderedIds.some((existing) => String(existing) === String(id))) {
|
||||
orderedIds.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
if (!orderedIds.length && ensureDefault) {
|
||||
orderedIds = [defaultCountry.id];
|
||||
}
|
||||
|
||||
return orderedIds.slice(0, selectionLimit).map((id) => {
|
||||
const option = optionByValue.get(String(id));
|
||||
const optionLabel = String(option?.textContent || '').trim();
|
||||
return {
|
||||
id,
|
||||
label: optionLabel || normalizePhoneSmsCountryLabel(defaultCountry.id === id ? defaultCountry.label : '', normalizedProvider),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function switchPhoneSmsProvider(nextProvider) {
|
||||
const previousProvider = getLastAppliedPhoneSmsProvider();
|
||||
const normalizedNextProvider = normalizePhoneSmsProvider(nextProvider);
|
||||
|
||||
const currentApiKey = String(inputHeroSmsApiKey?.value || '');
|
||||
const currentMaxPrice = normalizePhoneSmsMaxPriceValue(inputHeroSmsMaxPrice?.value || '', previousProvider);
|
||||
const currentSelection = typeof syncHeroSmsFallbackSelectionOrderFromSelect === 'function'
|
||||
? syncHeroSmsFallbackSelectionOrderFromSelect({ enforceMax: true, ensureDefault: true, showLimitToast: false })
|
||||
const currentSelection = typeof getPhoneSmsCountrySelectionForProvider === 'function'
|
||||
? getPhoneSmsCountrySelectionForProvider(previousProvider, { ensureDefault: true })
|
||||
: [];
|
||||
const currentPrimary = currentSelection[0] || getSelectedHeroSmsCountryOption();
|
||||
const currentFallback = currentSelection.slice(1);
|
||||
@@ -10439,6 +10693,9 @@ async function switchPhoneSmsProvider(nextProvider) {
|
||||
patch.fiveSimCountryId = currentPrimary.id;
|
||||
patch.fiveSimCountryLabel = currentPrimary.label;
|
||||
patch.fiveSimCountryFallback = currentFallback;
|
||||
patch.fiveSimCountryOrder = [currentPrimary, ...currentFallback]
|
||||
.map((country) => normalizeFiveSimCountryId(country?.id, ''))
|
||||
.filter(Boolean);
|
||||
patch.fiveSimOperator = normalizeFiveSimOperator(inputFiveSimOperator?.value || latestState?.fiveSimOperator);
|
||||
} else {
|
||||
patch.heroSmsApiKey = currentApiKey;
|
||||
@@ -11260,6 +11517,16 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
);
|
||||
updatePlusModeUI();
|
||||
}
|
||||
if (
|
||||
message.payload.plusManualConfirmationPending !== undefined
|
||||
|| message.payload.plusManualConfirmationRequestId !== undefined
|
||||
|| message.payload.plusManualConfirmationStep !== undefined
|
||||
|| message.payload.plusManualConfirmationMethod !== undefined
|
||||
|| message.payload.plusManualConfirmationTitle !== undefined
|
||||
|| message.payload.plusManualConfirmationMessage !== undefined
|
||||
) {
|
||||
void syncPlusManualConfirmationDialog();
|
||||
}
|
||||
if (message.payload.currentHotmailAccountId !== undefined || message.payload.hotmailAccounts !== undefined) {
|
||||
renderHotmailAccounts();
|
||||
if (selectMailProvider.value === 'hotmail-api') {
|
||||
@@ -11511,6 +11778,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
|| message.payload.fiveSimCountryId !== undefined
|
||||
|| message.payload.fiveSimCountryLabel !== undefined
|
||||
|| message.payload.fiveSimCountryFallback !== undefined
|
||||
|| message.payload.fiveSimCountryOrder !== undefined
|
||||
) {
|
||||
const activeProvider = getSelectedPhoneSmsProvider();
|
||||
const nextPrimary = activeProvider === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
@@ -11542,6 +11810,8 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
? normalizeFiveSimCountryFallbackList(
|
||||
message.payload.fiveSimCountryFallback !== undefined
|
||||
? message.payload.fiveSimCountryFallback
|
||||
: message.payload.fiveSimCountryOrder !== undefined
|
||||
? message.payload.fiveSimCountryOrder
|
||||
: latestState?.fiveSimCountryFallback
|
||||
)
|
||||
: normalizeHeroSmsCountryFallbackList(
|
||||
|
||||
@@ -11,6 +11,8 @@ test('address sources normalize supported countries and return local seeds', ()
|
||||
assert.equal(api.normalizeCountryCode('澳大利亚'), 'AU');
|
||||
assert.equal(api.normalizeCountryCode('印尼'), 'ID');
|
||||
assert.equal(api.normalizeCountryCode('日本'), 'JP');
|
||||
assert.equal(api.normalizeCountryCode('韩国'), 'KR');
|
||||
assert.equal(api.normalizeCountryCode('South Korea'), 'KR');
|
||||
assert.equal(api.normalizeCountryCode('unknown'), '');
|
||||
|
||||
const deSeed = api.getAddressSeedForCountry('DE');
|
||||
@@ -30,4 +32,9 @@ test('address sources normalize supported countries and return local seeds', ()
|
||||
const jpSeed = api.getAddressSeedForCountry('日本');
|
||||
assert.equal(jpSeed.countryCode, 'JP');
|
||||
assert.equal(jpSeed.fallback.region, 'Tokyo');
|
||||
|
||||
const krSeed = api.getAddressSeedForCountry('Korea');
|
||||
assert.equal(krSeed.countryCode, 'KR');
|
||||
assert.equal(krSeed.fallback.region, 'Seoul');
|
||||
assert.match(krSeed.fallback.postalCode, /^\d{5}$/);
|
||||
});
|
||||
|
||||
@@ -653,6 +653,172 @@ test('auto-run controller skips user_already_exists failures to the next round i
|
||||
assert.equal(runtime.state.autoRunSessionId, 0);
|
||||
});
|
||||
|
||||
|
||||
test('auto-run controller skips step 4 repeated 405 recovery failures to the next round', async () => {
|
||||
const events = {
|
||||
logs: [],
|
||||
broadcasts: [],
|
||||
accountRecords: [],
|
||||
runCalls: 0,
|
||||
};
|
||||
|
||||
let currentState = {
|
||||
stepStatuses: {},
|
||||
vpsUrl: 'https://example.com/vps',
|
||||
vpsPassword: 'secret',
|
||||
customPassword: '',
|
||||
autoRunSkipFailures: true,
|
||||
autoRunFallbackThreadIntervalMinutes: 0,
|
||||
autoRunDelayEnabled: false,
|
||||
autoRunDelayMinutes: 30,
|
||||
autoStepDelaySeconds: null,
|
||||
mailProvider: '163',
|
||||
emailGenerator: 'duck',
|
||||
gmailBaseEmail: '',
|
||||
mail2925BaseEmail: '',
|
||||
emailPrefix: 'demo',
|
||||
inbucketHost: '',
|
||||
inbucketMailbox: '',
|
||||
cloudflareDomain: '',
|
||||
cloudflareDomains: [],
|
||||
tabRegistry: {},
|
||||
sourceLastUrls: {},
|
||||
autoRunRoundSummaries: [],
|
||||
};
|
||||
|
||||
const runtime = {
|
||||
state: {
|
||||
autoRunActive: false,
|
||||
autoRunCurrentRun: 0,
|
||||
autoRunTotalRuns: 1,
|
||||
autoRunAttemptRun: 0,
|
||||
autoRunSessionId: 0,
|
||||
},
|
||||
get() {
|
||||
return { ...this.state };
|
||||
},
|
||||
set(updates = {}) {
|
||||
this.state = { ...this.state, ...updates };
|
||||
},
|
||||
};
|
||||
|
||||
let sessionSeed = 0;
|
||||
|
||||
const controller = api.createAutoRunController({
|
||||
addLog: async (message, level = 'info') => {
|
||||
events.logs.push({ message, level });
|
||||
},
|
||||
appendAccountRunRecord: async (status, _state, reason) => {
|
||||
events.accountRecords.push({ status, reason });
|
||||
return { status, reason };
|
||||
},
|
||||
AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
|
||||
AUTO_RUN_RETRY_DELAY_MS: 3000,
|
||||
AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
|
||||
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
|
||||
broadcastAutoRunStatus: async (phase, payload = {}) => {
|
||||
events.broadcasts.push({ phase, ...payload });
|
||||
currentState = {
|
||||
...currentState,
|
||||
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
|
||||
autoRunPhase: phase,
|
||||
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
|
||||
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
|
||||
autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
|
||||
autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
|
||||
};
|
||||
},
|
||||
broadcastStopToContentScripts: async () => {},
|
||||
cancelPendingCommands: () => {},
|
||||
clearStopRequest: () => {},
|
||||
createAutoRunSessionId: () => {
|
||||
sessionSeed += 1;
|
||||
return sessionSeed;
|
||||
},
|
||||
getAutoRunStatusPayload: (phase, payload = {}) => ({
|
||||
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
|
||||
autoRunPhase: phase,
|
||||
autoRunCurrentRun: payload.currentRun ?? 0,
|
||||
autoRunTotalRuns: payload.totalRuns ?? 1,
|
||||
autoRunAttemptRun: payload.attemptRun ?? 0,
|
||||
autoRunSessionId: payload.sessionId ?? 0,
|
||||
}),
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getFirstUnfinishedStep: () => 1,
|
||||
getPendingAutoRunTimerPlan: () => null,
|
||||
getRunningSteps: () => [],
|
||||
getState: async () => ({
|
||||
...currentState,
|
||||
stepStatuses: { ...(currentState.stepStatuses || {}) },
|
||||
tabRegistry: { ...(currentState.tabRegistry || {}) },
|
||||
sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
|
||||
}),
|
||||
getStopRequested: () => false,
|
||||
hasSavedProgress: () => false,
|
||||
isAddPhoneAuthFailure: () => false,
|
||||
isPlusCheckoutNonFreeTrialFailure: () => false,
|
||||
isRestartCurrentAttemptError: () => false,
|
||||
isSignupUserAlreadyExistsFailure: () => false,
|
||||
isStep4Route405RecoveryLimitFailure: (error) => /STEP4_405_RECOVERY_LIMIT::/.test(error?.message || String(error || '')),
|
||||
isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
|
||||
launchAutoRunTimerPlan: async () => false,
|
||||
normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
|
||||
persistAutoRunTimerPlan: async () => ({}),
|
||||
resetState: async () => {
|
||||
currentState = {
|
||||
...currentState,
|
||||
stepStatuses: {},
|
||||
tabRegistry: {},
|
||||
sourceLastUrls: {},
|
||||
};
|
||||
},
|
||||
runAutoSequenceFromStep: async () => {
|
||||
events.runCalls += 1;
|
||||
if (events.runCalls === 1) {
|
||||
throw new Error('STEP4_405_RECOVERY_LIMIT::步骤 4:检测到 405 错误页面,已连续点击“重试”恢复 3/3 次仍未恢复,当前轮将结束并进入下一轮。');
|
||||
}
|
||||
},
|
||||
runtime,
|
||||
setState: async (updates = {}) => {
|
||||
currentState = {
|
||||
...currentState,
|
||||
...updates,
|
||||
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
|
||||
tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
|
||||
sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
|
||||
};
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfAutoRunSessionStopped: (sessionId) => {
|
||||
if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
|
||||
throw new Error('流程已被用户停止。');
|
||||
}
|
||||
},
|
||||
waitForRunningStepsToFinish: async () => currentState,
|
||||
chrome: {
|
||||
runtime: {
|
||||
sendMessage() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await controller.autoRunLoop(2, {
|
||||
autoRunSkipFailures: true,
|
||||
mode: 'restart',
|
||||
});
|
||||
|
||||
assert.equal(events.runCalls, 2, 'step 4 repeated 405 failure should skip the current round and continue with the next round');
|
||||
assert.equal(events.broadcasts.some(({ phase }) => phase === 'retrying'), false, 'step 4 repeated 405 should not retry the same round');
|
||||
assert.equal(events.accountRecords.length, 1, 'step 4 repeated 405 should persist a failed round record');
|
||||
assert.equal(events.accountRecords[0].status, 'failed');
|
||||
assert.match(events.accountRecords[0].reason, /STEP4_405_RECOVERY_LIMIT::/);
|
||||
assert.ok(events.logs.some(({ message }) => /连续 405.*继续下一轮|继续下一轮/.test(message)));
|
||||
assert.equal(runtime.state.autoRunActive, false);
|
||||
assert.equal(runtime.state.autoRunSessionId, 0);
|
||||
});
|
||||
|
||||
test('auto-run controller keeps retrying the same custom mail provider pool email until success', async () => {
|
||||
const events = {
|
||||
logs: [],
|
||||
@@ -817,3 +983,171 @@ test('auto-run controller keeps retrying the same custom mail provider pool emai
|
||||
assert.equal(runtime.state.autoRunActive, false);
|
||||
assert.equal(runtime.state.autoRunSessionId, 0);
|
||||
});
|
||||
|
||||
test('auto-run controller retries 5sim rate limit failures instead of treating current add-phone page as fatal', async () => {
|
||||
const events = {
|
||||
logs: [],
|
||||
broadcasts: [],
|
||||
accountRecords: [],
|
||||
runCalls: 0,
|
||||
sleeps: [],
|
||||
};
|
||||
|
||||
let currentState = {
|
||||
stepStatuses: {},
|
||||
vpsUrl: 'https://example.com/vps',
|
||||
vpsPassword: 'secret',
|
||||
customPassword: '',
|
||||
autoRunSkipFailures: true,
|
||||
autoRunFallbackThreadIntervalMinutes: 0,
|
||||
autoRunDelayEnabled: false,
|
||||
autoRunDelayMinutes: 30,
|
||||
autoStepDelaySeconds: null,
|
||||
mailProvider: '163',
|
||||
emailGenerator: 'duck',
|
||||
gmailBaseEmail: '',
|
||||
mail2925BaseEmail: '',
|
||||
emailPrefix: 'demo',
|
||||
inbucketHost: '',
|
||||
inbucketMailbox: '',
|
||||
cloudflareDomain: '',
|
||||
cloudflareDomains: [],
|
||||
tabRegistry: {},
|
||||
sourceLastUrls: {},
|
||||
autoRunRoundSummaries: [],
|
||||
};
|
||||
|
||||
const runtime = {
|
||||
state: {
|
||||
autoRunActive: false,
|
||||
autoRunCurrentRun: 0,
|
||||
autoRunTotalRuns: 1,
|
||||
autoRunAttemptRun: 0,
|
||||
autoRunSessionId: 0,
|
||||
},
|
||||
get() {
|
||||
return { ...this.state };
|
||||
},
|
||||
set(updates = {}) {
|
||||
this.state = { ...this.state, ...updates };
|
||||
},
|
||||
};
|
||||
|
||||
let sessionSeed = 0;
|
||||
|
||||
const controller = api.createAutoRunController({
|
||||
addLog: async (message, level = 'info') => {
|
||||
events.logs.push({ message, level });
|
||||
},
|
||||
appendAccountRunRecord: async (status, _state, reason) => {
|
||||
events.accountRecords.push({ status, reason });
|
||||
return { status, reason };
|
||||
},
|
||||
AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
|
||||
AUTO_RUN_RETRY_DELAY_MS: 3000,
|
||||
AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
|
||||
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
|
||||
broadcastAutoRunStatus: async (phase, payload = {}) => {
|
||||
events.broadcasts.push({ phase, ...payload });
|
||||
currentState = {
|
||||
...currentState,
|
||||
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
|
||||
autoRunPhase: phase,
|
||||
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
|
||||
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
|
||||
autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
|
||||
autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
|
||||
};
|
||||
},
|
||||
broadcastStopToContentScripts: async () => {},
|
||||
cancelPendingCommands: () => {},
|
||||
clearStopRequest: () => {},
|
||||
createAutoRunSessionId: () => {
|
||||
sessionSeed += 1;
|
||||
return sessionSeed;
|
||||
},
|
||||
getAutoRunStatusPayload: (phase, payload = {}) => ({
|
||||
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
|
||||
autoRunPhase: phase,
|
||||
autoRunCurrentRun: payload.currentRun ?? 0,
|
||||
autoRunTotalRuns: payload.totalRuns ?? 1,
|
||||
autoRunAttemptRun: payload.attemptRun ?? 0,
|
||||
autoRunSessionId: payload.sessionId ?? 0,
|
||||
}),
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getFirstUnfinishedStep: () => 1,
|
||||
getPendingAutoRunTimerPlan: () => null,
|
||||
getRunningSteps: () => [],
|
||||
getState: async () => ({
|
||||
...currentState,
|
||||
stepStatuses: { ...(currentState.stepStatuses || {}) },
|
||||
tabRegistry: { ...(currentState.tabRegistry || {}) },
|
||||
sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
|
||||
}),
|
||||
getStopRequested: () => false,
|
||||
hasSavedProgress: () => false,
|
||||
isAddPhoneAuthFailure: (error) => /add-phone|手机号页面|手机号页|手机号码|手机号/i.test(error?.message || String(error || '')),
|
||||
isPhoneSmsPlatformRateLimitFailure: (error) => /FIVE_SIM_RATE_LIMIT::|5sim[\s\S]*(?:限流|rate\s*limit)/i.test(error?.message || String(error || '')),
|
||||
isRestartCurrentAttemptError: () => false,
|
||||
isSignupUserAlreadyExistsFailure: () => false,
|
||||
isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
|
||||
launchAutoRunTimerPlan: async () => false,
|
||||
normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
|
||||
persistAutoRunTimerPlan: async () => ({}),
|
||||
resetState: async () => {
|
||||
currentState = {
|
||||
...currentState,
|
||||
stepStatuses: {},
|
||||
tabRegistry: {},
|
||||
sourceLastUrls: {},
|
||||
};
|
||||
},
|
||||
runAutoSequenceFromStep: async () => {
|
||||
events.runCalls += 1;
|
||||
if (events.runCalls === 1) {
|
||||
throw new Error('FIVE_SIM_RATE_LIMIT::5sim 购买接口触发限流,请稍后再试:印度 (India): rate limit。当前页面 https://auth.openai.com/add-phone');
|
||||
}
|
||||
},
|
||||
runtime,
|
||||
setState: async (updates = {}) => {
|
||||
currentState = {
|
||||
...currentState,
|
||||
...updates,
|
||||
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
|
||||
tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
|
||||
sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
|
||||
};
|
||||
},
|
||||
sleepWithStop: async (ms) => {
|
||||
events.sleeps.push(ms);
|
||||
},
|
||||
throwIfAutoRunSessionStopped: (sessionId) => {
|
||||
if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
|
||||
throw new Error('流程已被用户停止。');
|
||||
}
|
||||
},
|
||||
waitForRunningStepsToFinish: async () => currentState,
|
||||
chrome: {
|
||||
runtime: {
|
||||
sendMessage() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await controller.autoRunLoop(1, {
|
||||
autoRunSkipFailures: true,
|
||||
mode: 'restart',
|
||||
});
|
||||
|
||||
assert.equal(events.runCalls, 2, '5sim rate limit should use same-round retry instead of add-phone fatal skip');
|
||||
assert.equal(events.broadcasts.filter(({ phase }) => phase === 'retrying').length, 1);
|
||||
assert.equal(events.accountRecords.length, 0);
|
||||
assert.ok(events.logs.some(({ message }) => /自动重试/.test(message)));
|
||||
assert.equal(events.logs.some(({ message }) => /触发 add-phone\/手机号页/.test(message)), false);
|
||||
assert.equal(events.sleeps.filter((ms) => ms === 3000).length, 1);
|
||||
assert.equal(runtime.state.autoRunActive, false);
|
||||
assert.equal(runtime.state.autoRunSessionId, 0);
|
||||
});
|
||||
|
||||
|
||||
@@ -147,6 +147,10 @@ function getLoginAuthStateLabel(state) {
|
||||
function getErrorMessage(error) {
|
||||
return error?.message || String(error || '');
|
||||
}
|
||||
function isPhoneSmsPlatformRateLimitFailure(error) {
|
||||
const message = getErrorMessage(error);
|
||||
return /FIVE_SIM_RATE_LIMIT::|5sim[\s\S]*(?:限流|rate\s*limit)/i.test(message);
|
||||
}
|
||||
async function getLoginAuthStateFromContent() {
|
||||
return ${JSON.stringify(authState)};
|
||||
}
|
||||
@@ -254,6 +258,24 @@ test('auto-run does not restart step 7 when phone verification exhausted replace
|
||||
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
|
||||
});
|
||||
|
||||
|
||||
test('auto-run post-login restart decision does not treat 5sim rate limit on add-phone page as add-phone fatal', async () => {
|
||||
const harness = createHarness({
|
||||
failureStep: 9,
|
||||
failureBudget: 1,
|
||||
failureMessage: 'FIVE_SIM_RATE_LIMIT::5sim 购买接口触发限流,请稍后再试:印度 (India): rate limit。',
|
||||
authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' },
|
||||
});
|
||||
|
||||
const result = await harness.runAndCaptureError();
|
||||
|
||||
assert.ok(result?.error);
|
||||
assert.equal(result.events.invalidations.length, 0);
|
||||
assert.deepStrictEqual(result.events.steps, [7, 8, 9]);
|
||||
assert.ok(!result.events.logs.some(({ message }) => /进入 add-phone/.test(message)));
|
||||
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run stop errors after step 7 are rethrown immediately instead of restarting', async () => {
|
||||
const harness = createHarness({
|
||||
failureStep: 9,
|
||||
|
||||
@@ -106,9 +106,11 @@ const HERO_SMS_COUNTRY_ID = 52;
|
||||
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const FIVE_SIM_COUNTRY_ID = 'vietnam';
|
||||
const FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
|
||||
const FIVE_SIM_OPERATOR = 'any';
|
||||
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
|
||||
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
|
||||
const PERSISTED_SETTING_DEFAULTS = {
|
||||
autoStepDelaySeconds: null,
|
||||
mailProvider: '163',
|
||||
@@ -156,19 +158,19 @@ return {
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', '5SIM'), '5sim');
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'unknown'), 'hero-sms');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimApiKey', ' demo-five '), ' demo-five ');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ' England! '), 'england');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ''), 'england');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryLabel', ''), '英国 (England)');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ' England! '), 'vietnam');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ''), 'vietnam');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryLabel', ''), '越南 (Vietnam)');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimMaxPrice', '9.87654'), '9.8765');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimMaxPrice', '-1'), '');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimOperator', ''), 'any');
|
||||
assert.deepStrictEqual(
|
||||
api.normalizePersistentSettingValue('fiveSimCountryFallback', [{ id: 'usa', label: 'USA' }, 'thailand:Thailand']),
|
||||
[{ id: 'usa', label: 'USA' }, { id: 'thailand', label: 'Thailand' }]
|
||||
[{ id: 'thailand', label: 'Thailand' }]
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
api.normalizePersistentSettingValue('heroSmsCountryFallback', [{ id: 16, label: 'United Kingdom' }, { id: 52 }]),
|
||||
[{ id: 16, label: 'United Kingdom' }, { id: 52, label: 'Country #52' }]
|
||||
[{ id: 52, label: 'Country #52' }]
|
||||
);
|
||||
assert.equal(
|
||||
api.normalizePersistentSettingValue('accountRunHistoryHelperBaseUrl', 'http://127.0.0.1:17373/append-account-log'),
|
||||
|
||||
@@ -19,6 +19,18 @@ const IP_PROXY_FETCH_TIMEOUT_MS = 20000;
|
||||
const IP_PROXY_SETTINGS_SCOPE = 'regular';
|
||||
const IP_PROXY_BYPASS_LIST = ['<local>', 'localhost', '127.0.0.1'];
|
||||
const IP_PROXY_ROUTE_ALL_TRAFFIC = true;
|
||||
const IP_PROXY_FORCE_DIRECT_HOST_PATTERNS = [
|
||||
'pm-redirects.stripe.com',
|
||||
'*.pm-redirects.stripe.com',
|
||||
'hwork.pro',
|
||||
'*.hwork.pro',
|
||||
'auth.openai.com',
|
||||
'auth0.openai.com',
|
||||
'accounts.openai.com',
|
||||
'luckyous.com',
|
||||
'*.luckyous.com',
|
||||
];
|
||||
const IP_PROXY_FORCE_DIRECT_FALLBACK = 'PROXY 127.0.0.1:7897';
|
||||
const IP_PROXY_ACCOUNT_LIST_ENABLED = ${accountListEnabled ? 'true' : 'false'};
|
||||
const IP_PROXY_TARGET_HOST_PATTERNS = [
|
||||
'openai.com',
|
||||
@@ -32,11 +44,17 @@ ${coreSource}
|
||||
return {
|
||||
applyExitRegionExpectation,
|
||||
buildIpProxyPacScript,
|
||||
buildIpProxyRoutingStatePatch,
|
||||
applyTargetReachabilityExpectation,
|
||||
getAccountModeProxyPoolFromState,
|
||||
normalizeIpProxyAccountList,
|
||||
normalizeProxyPoolEntries,
|
||||
parseProxyExitProbePayload,
|
||||
parseIpProxyLine,
|
||||
resolveExitProbeEndpoints,
|
||||
resolveIpProxyAutoSwitchThreshold,
|
||||
resolveTargetReachabilityEndpoints,
|
||||
shouldEnableIpProxyLeakGuardForStatus,
|
||||
};
|
||||
`)();
|
||||
}
|
||||
@@ -75,6 +93,48 @@ test('IP proxy parser ignores disabled lines and normalizes proxy entries', () =
|
||||
assert.equal(pool[1].port, 8080);
|
||||
});
|
||||
|
||||
test('IP proxy probe payload parser extracts country from common probe endpoints', () => {
|
||||
const api = loadIpProxyCore();
|
||||
|
||||
assert.deepEqual(
|
||||
api.parseProxyExitProbePayload('ip=219.104.171.52\nloc=JP\ncolo=NRT', 'text/plain'),
|
||||
{ ip: '219.104.171.52', region: 'JP' }
|
||||
);
|
||||
assert.deepEqual(
|
||||
api.parseProxyExitProbePayload(JSON.stringify({
|
||||
ip: '219.104.171.52',
|
||||
country: 'JP',
|
||||
city: 'Osaka',
|
||||
}), 'application/json'),
|
||||
{ ip: '219.104.171.52', region: 'JP' }
|
||||
);
|
||||
assert.deepEqual(
|
||||
api.parseProxyExitProbePayload(JSON.stringify({
|
||||
ip: '219.104.171.52',
|
||||
country_code: 'JP',
|
||||
country: 'Japan',
|
||||
}), 'application/json'),
|
||||
{ ip: '219.104.171.52', region: 'JP' }
|
||||
);
|
||||
});
|
||||
|
||||
test('IP proxy routing state patch keeps exit probe endpoint for diagnostics', () => {
|
||||
const api = loadIpProxyCore();
|
||||
const patch = api.buildIpProxyRoutingStatePatch({
|
||||
applied: true,
|
||||
reason: 'applied',
|
||||
provider: '711proxy',
|
||||
exitIp: '219.104.171.52',
|
||||
exitRegion: 'JP',
|
||||
exitSource: 'page_context',
|
||||
exitEndpoint: 'https://ipinfo.io/json',
|
||||
});
|
||||
|
||||
assert.equal(patch.ipProxyAppliedExitIp, '219.104.171.52');
|
||||
assert.equal(patch.ipProxyAppliedExitRegion, 'JP');
|
||||
assert.equal(patch.ipProxyAppliedExitEndpoint, 'https://ipinfo.io/json');
|
||||
});
|
||||
|
||||
test('711 fixed-account mode applies region and sticky session parameters', () => {
|
||||
const api = loadIpProxyCore();
|
||||
const pool = api.getAccountModeProxyPoolFromState({
|
||||
@@ -112,6 +172,15 @@ test('IP proxy PAC keeps local traffic direct and routes target traffic through
|
||||
assert.match(pac, /PROXY global\.rotgb\.711proxy\.com:10000/);
|
||||
assert.match(pac, /chatgpt\.com/);
|
||||
assert.match(pac, /openai\.com/);
|
||||
assert.match(pac, /pm-redirects\.stripe\.com/);
|
||||
assert.match(pac, /hwork\.pro/);
|
||||
assert.match(pac, /auth\.openai\.com/);
|
||||
assert.match(pac, /auth0\.openai\.com/);
|
||||
assert.match(pac, /accounts\.openai\.com/);
|
||||
assert.match(pac, /luckyous\.com/);
|
||||
assert.match(pac, /forceDirectPatterns/);
|
||||
assert.match(pac, /PROXY 127\.0\.0\.1:7897/);
|
||||
assert.doesNotMatch(pac, /PROXY 127\.0\.0\.1:7897; DIRECT/);
|
||||
});
|
||||
|
||||
test('sidepanel loads IP proxy scripts before sidepanel bootstrap', () => {
|
||||
@@ -161,3 +230,53 @@ test('711 proxy region mismatch with missing auth challenge keeps routing as war
|
||||
);
|
||||
assert.match(String(status.warning || ''), /期望 US,实际 BR/);
|
||||
});
|
||||
|
||||
test('711 sticky session keeps IP probe on ipinfo but separately checks ChatGPT target', () => {
|
||||
const api = loadIpProxyCore();
|
||||
|
||||
assert.deepStrictEqual(
|
||||
api.resolveExitProbeEndpoints({
|
||||
provider: '711proxy',
|
||||
username: 'USER794331-zone-custom-region-TH-session-69381850-sessTime-5',
|
||||
}),
|
||||
['https://ipinfo.io/json']
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(api.resolveTargetReachabilityEndpoints(), ['https://chatgpt.com/']);
|
||||
});
|
||||
|
||||
test('target reachability failure turns detected exit IP into connectivity_failed', () => {
|
||||
const api = loadIpProxyCore();
|
||||
const status = api.applyTargetReachabilityExpectation({
|
||||
applied: true,
|
||||
reason: 'applied',
|
||||
exitIp: '58.10.48.73',
|
||||
exitRegion: 'TH',
|
||||
}, {
|
||||
reachable: false,
|
||||
endpoint: 'https://chatgpt.com/',
|
||||
error: 'target:page_context:https://chatgpt.com/:net::ERR_EMPTY_RESPONSE',
|
||||
});
|
||||
|
||||
assert.equal(status.applied, false);
|
||||
assert.equal(status.reason, 'connectivity_failed');
|
||||
assert.match(status.error, /已检测到出口 IP 58\.10\.48\.73 \[TH\]/);
|
||||
assert.match(status.error, /真实目标 chatgpt\.com 不可达/);
|
||||
assert.match(status.error, /ERR_EMPTY_RESPONSE/);
|
||||
});
|
||||
|
||||
test('connectivity_failed keeps DNR leak guard off so ChatGPT shows proxy error instead of blocked by extension', () => {
|
||||
const api = loadIpProxyCore();
|
||||
|
||||
assert.equal(api.shouldEnableIpProxyLeakGuardForStatus({
|
||||
enabled: true,
|
||||
applied: false,
|
||||
reason: 'connectivity_failed',
|
||||
}), false);
|
||||
|
||||
assert.equal(api.shouldEnableIpProxyLeakGuardForStatus({
|
||||
enabled: true,
|
||||
applied: false,
|
||||
reason: 'missing_proxy_entry',
|
||||
}), true);
|
||||
});
|
||||
|
||||
@@ -451,6 +451,9 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
luckmailBaseUrl: DEFAULT_LUCKMAIL_BASE_URL,
|
||||
luckmailEmailType: DEFAULT_LUCKMAIL_EMAIL_TYPE,
|
||||
luckmailDomain: '',
|
||||
luckmailUsedPurchases: {},
|
||||
luckmailPreserveTagId: 0,
|
||||
luckmailPreserveTagName: '保留',
|
||||
};
|
||||
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
||||
function normalizeLuckmailBaseUrl(value) {
|
||||
@@ -462,6 +465,18 @@ function normalizeLuckmailEmailType(value) {
|
||||
? String(value || '').trim()
|
||||
: DEFAULT_LUCKMAIL_EMAIL_TYPE;
|
||||
}
|
||||
function normalizeLuckmailPurchaseId(value) {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) && numeric > 0 ? String(Math.floor(numeric)) : '';
|
||||
}
|
||||
function normalizeLuckmailUsedPurchases(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
||||
return Object.entries(value).reduce((result, [key, used]) => {
|
||||
const normalizedKey = normalizeLuckmailPurchaseId(key);
|
||||
if (normalizedKey) result[normalizedKey] = Boolean(used);
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
function resolveLegacyAutoStepDelaySeconds() {
|
||||
return undefined;
|
||||
}
|
||||
@@ -487,6 +502,17 @@ return {
|
||||
luckmailEmailType: 'ms_imap',
|
||||
luckmailDomain: 'outlook.com',
|
||||
});
|
||||
|
||||
const statePayload = api.buildPersistentSettingsPayload({
|
||||
luckmailUsedPurchases: { 88: true, 99: false, bad: true },
|
||||
luckmailPreserveTagId: '9',
|
||||
luckmailPreserveTagName: ' 保留邮箱 ',
|
||||
});
|
||||
assert.deepStrictEqual(statePayload, {
|
||||
luckmailUsedPurchases: { 88: true, 99: false },
|
||||
luckmailPreserveTagId: 9,
|
||||
luckmailPreserveTagName: '保留邮箱',
|
||||
});
|
||||
});
|
||||
|
||||
test('listLuckmailPurchasesByProject only keeps openai purchases', async () => {
|
||||
@@ -879,3 +905,117 @@ return {
|
||||
assert.deepStrictEqual(snapshot.usedMarker, { purchaseId: 456, used: true });
|
||||
assert.equal(snapshot.logs.some((entry) => /已在本地标记为已用/.test(entry.message)), true);
|
||||
});
|
||||
|
||||
test('setLuckmailPurchaseUsedState persists used map to storage.local so reload keeps it non-reusable', async () => {
|
||||
const bundle = [
|
||||
extractFunction('getLuckmailUsedPurchases'),
|
||||
extractFunction('setLuckmailUsedPurchasesState'),
|
||||
extractFunction('setLuckmailPurchaseUsedState'),
|
||||
].join('\n');
|
||||
|
||||
const factory = new Function(`
|
||||
let sessionState = {
|
||||
luckmailUsedPurchases: { 7: true },
|
||||
};
|
||||
let persistentUpdates = [];
|
||||
let sessionUpdates = [];
|
||||
let broadcasts = [];
|
||||
function normalizeLuckmailPurchaseId(value) {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) && numeric > 0 ? String(Math.floor(numeric)) : '';
|
||||
}
|
||||
function normalizeLuckmailUsedPurchases(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
||||
return Object.entries(value).reduce((result, [key, used]) => {
|
||||
const normalizedKey = normalizeLuckmailPurchaseId(key);
|
||||
if (normalizedKey) result[normalizedKey] = Boolean(used);
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
async function getState() {
|
||||
return { ...sessionState };
|
||||
}
|
||||
async function setPersistentSettings(updates) {
|
||||
persistentUpdates.push(updates);
|
||||
}
|
||||
async function setState(updates) {
|
||||
sessionUpdates.push(updates);
|
||||
sessionState = { ...sessionState, ...updates };
|
||||
}
|
||||
function broadcastDataUpdate(updates) {
|
||||
broadcasts.push(updates);
|
||||
}
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
setLuckmailPurchaseUsedState,
|
||||
snapshot() {
|
||||
return { sessionState, persistentUpdates, sessionUpdates, broadcasts };
|
||||
},
|
||||
};
|
||||
`);
|
||||
|
||||
const api = factory();
|
||||
const result = await api.setLuckmailPurchaseUsedState(88, true);
|
||||
const snapshot = api.snapshot();
|
||||
|
||||
assert.deepStrictEqual(result, { purchaseId: 88, used: true });
|
||||
assert.deepStrictEqual(snapshot.sessionState.luckmailUsedPurchases, { 7: true, 88: true });
|
||||
assert.deepStrictEqual(snapshot.persistentUpdates, [
|
||||
{ luckmailUsedPurchases: { 7: true, 88: true } },
|
||||
]);
|
||||
assert.deepStrictEqual(snapshot.sessionUpdates, [
|
||||
{ luckmailUsedPurchases: { 7: true, 88: true } },
|
||||
]);
|
||||
assert.deepStrictEqual(snapshot.broadcasts, [
|
||||
{ luckmailUsedPurchases: { 7: true, 88: true } },
|
||||
]);
|
||||
});
|
||||
|
||||
test('setLuckmailPreserveTagInfo persists tag cache to storage.local', async () => {
|
||||
const bundle = extractFunction('setLuckmailPreserveTagInfo');
|
||||
|
||||
const factory = new Function(`
|
||||
let persistentUpdates = [];
|
||||
let sessionUpdates = [];
|
||||
let broadcasts = [];
|
||||
const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = '保留';
|
||||
function normalizeLuckmailTags(tags) {
|
||||
return (Array.isArray(tags) ? tags : []).map((tag) => ({
|
||||
id: Number(tag?.id) || 0,
|
||||
name: String(tag?.name || '').trim(),
|
||||
})).filter((tag) => tag.id > 0 || tag.name);
|
||||
}
|
||||
async function setPersistentSettings(updates) {
|
||||
persistentUpdates.push(updates);
|
||||
}
|
||||
async function setState(updates) {
|
||||
sessionUpdates.push(updates);
|
||||
}
|
||||
function broadcastDataUpdate(updates) {
|
||||
broadcasts.push(updates);
|
||||
}
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
setLuckmailPreserveTagInfo,
|
||||
snapshot() {
|
||||
return { persistentUpdates, sessionUpdates, broadcasts };
|
||||
},
|
||||
};
|
||||
`);
|
||||
|
||||
const api = factory();
|
||||
const result = await api.setLuckmailPreserveTagInfo({ id: '12', name: ' 保留邮箱 ' });
|
||||
const expected = {
|
||||
luckmailPreserveTagId: 12,
|
||||
luckmailPreserveTagName: '保留邮箱',
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(result, expected);
|
||||
assert.deepStrictEqual(api.snapshot().persistentUpdates, [expected]);
|
||||
assert.deepStrictEqual(api.snapshot().sessionUpdates, [expected]);
|
||||
assert.deepStrictEqual(api.snapshot().broadcasts, [expected]);
|
||||
});
|
||||
|
||||
@@ -237,3 +237,36 @@ test('platform verify module rejects callback when cpa oauth state mismatches',
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('platform verify module sends Plus visible step 13 to SUB2API 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',
|
||||
visibleStep: 13,
|
||||
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',
|
||||
});
|
||||
|
||||
assert.equal(sentMessages.length, 1);
|
||||
assert.equal(sentMessages[0].sourceName, 'sub2api-panel');
|
||||
assert.equal(sentMessages[0].message.step, 13);
|
||||
});
|
||||
|
||||
|
||||
+111
-19
@@ -37,25 +37,31 @@ test('5sim provider maps countries and prices', async () => {
|
||||
const parsed = new URL(url);
|
||||
requests.push({ url: parsed, options });
|
||||
if (parsed.pathname === '/v1/guest/countries') {
|
||||
return createTextResponse({ england: { text_en: 'England', iso: { GB: 1 }, prefix: { 44: 1 } } });
|
||||
return createTextResponse({
|
||||
england: { text_en: 'England', iso: { GB: 1 }, prefix: { 44: 1 } },
|
||||
indonesia: { text_en: 'Indonesia', iso: { ID: 1 }, prefix: { '+62': 1 } },
|
||||
thailand: { text_en: 'Thailand', iso: { TH: 1 }, prefix: { '+66': 1 } },
|
||||
vietnam: { text_en: 'Vietnam', iso: { VN: 1 }, prefix: { '+84': 1 } },
|
||||
});
|
||||
}
|
||||
if (parsed.pathname === '/v1/guest/prices') {
|
||||
return createTextResponse({ england: { any: { openai: { cost: 10, count: 2 } } } });
|
||||
return createTextResponse({ vietnam: { any: { openai: { cost: 10, count: 2 } } } });
|
||||
}
|
||||
throw new Error(`unexpected ${parsed.pathname}`);
|
||||
},
|
||||
});
|
||||
|
||||
const countries = await provider.fetchCountries({});
|
||||
const prices = await provider.fetchPrices({}, { id: 'england', label: 'England' });
|
||||
const prices = await provider.fetchPrices({}, { id: 'vietnam', label: 'Vietnam' });
|
||||
const entries = provider.collectPriceEntries(prices, []);
|
||||
|
||||
assert.deepStrictEqual(countries[0], {
|
||||
id: 'england',
|
||||
label: '英国 (England)',
|
||||
searchText: 'england 英国 (England) England GB 44',
|
||||
assert.deepStrictEqual(countries.map((country) => country.id), ['indonesia', 'thailand', 'vietnam']);
|
||||
assert.deepStrictEqual(countries[2], {
|
||||
id: 'vietnam',
|
||||
label: '越南 (Vietnam)',
|
||||
searchText: 'vietnam 越南 (Vietnam) Vietnam VN +84',
|
||||
});
|
||||
assert.equal(requests[1].url.searchParams.get('country'), 'england');
|
||||
assert.equal(requests[1].url.searchParams.get('country'), 'vietnam');
|
||||
assert.equal(requests[1].url.searchParams.get('product'), 'openai');
|
||||
assert.deepStrictEqual(entries, [{ cost: 10, count: 2, inStock: true }]);
|
||||
});
|
||||
@@ -66,14 +72,14 @@ test('5sim provider buys, checks, finishes, cancels, bans, and reuses activation
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
const parsed = new URL(url);
|
||||
requests.push({ url: parsed, options });
|
||||
if (parsed.pathname === '/v1/guest/products/england/any') {
|
||||
if (parsed.pathname === '/v1/guest/products/vietnam/any') {
|
||||
return createTextResponse({ openai: { Category: 'activation', Qty: 4, Price: 8 } });
|
||||
}
|
||||
if (parsed.pathname === '/v1/guest/prices') {
|
||||
return createTextResponse({ england: { any: { openai: { cost: 9.5, count: 4 } } } });
|
||||
return createTextResponse({ vietnam: { any: { openai: { cost: 9.5, count: 4 } } } });
|
||||
}
|
||||
if (parsed.pathname === '/v1/user/buy/activation/england/any/openai') {
|
||||
return createTextResponse({ id: 1001, phone: '+447911123456', country: 'england', operator: 'any', status: 'PENDING' });
|
||||
if (parsed.pathname === '/v1/user/buy/activation/vietnam/any/openai') {
|
||||
return createTextResponse({ id: 1001, phone: '+84901123456', country: 'vietnam', operator: 'any', status: 'PENDING' });
|
||||
}
|
||||
if (parsed.pathname === '/v1/user/check/1001') {
|
||||
return createTextResponse({ id: 1001, phone: '+447911123456', status: 'RECEIVED', sms: [{ text: 'code 112233' }] });
|
||||
@@ -81,8 +87,8 @@ test('5sim provider buys, checks, finishes, cancels, bans, and reuses activation
|
||||
if (parsed.pathname === '/v1/user/finish/1001') return createTextResponse({ status: 'FINISHED' });
|
||||
if (parsed.pathname === '/v1/user/cancel/1001') return createTextResponse({ status: 'CANCELED' });
|
||||
if (parsed.pathname === '/v1/user/ban/1001') return createTextResponse({ status: 'BANNED' });
|
||||
if (parsed.pathname === '/v1/user/reuse/openai/447911123456') {
|
||||
return createTextResponse({ id: 1002, phone: '+447911123456', country: 'england', status: 'PENDING' });
|
||||
if (parsed.pathname === '/v1/user/reuse/openai/84901123456') {
|
||||
return createTextResponse({ id: 1002, phone: '+84901123456', country: 'vietnam', status: 'PENDING' });
|
||||
}
|
||||
throw new Error(`unexpected ${parsed.pathname}`);
|
||||
},
|
||||
@@ -90,7 +96,7 @@ test('5sim provider buys, checks, finishes, cancels, bans, and reuses activation
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const state = { fiveSimApiKey: 'demo-key', fiveSimCountryId: 'england', fiveSimCountryLabel: 'England', fiveSimMaxPrice: '12', fiveSimOperator: 'any' };
|
||||
const state = { fiveSimApiKey: 'demo-key', fiveSimCountryId: 'vietnam', fiveSimCountryLabel: '越南 (Vietnam)', fiveSimMaxPrice: '12', fiveSimOperator: 'any' };
|
||||
const activation = await provider.requestActivation(state);
|
||||
const code = await provider.pollActivationCode(state, activation, { timeoutMs: 1000, intervalMs: 1, maxRounds: 1 });
|
||||
await provider.finishActivation(state, activation);
|
||||
@@ -100,7 +106,7 @@ test('5sim provider buys, checks, finishes, cancels, bans, and reuses activation
|
||||
|
||||
assert.equal(activation.provider, '5sim');
|
||||
assert.equal(activation.activationId, '1001');
|
||||
assert.equal(activation.countryId, 'england');
|
||||
assert.equal(activation.countryId, 'vietnam');
|
||||
assert.equal(code, '112233');
|
||||
assert.equal(reused.activationId, '1002');
|
||||
const buy = requests.find((entry) => entry.url.pathname.includes('/buy/activation'));
|
||||
@@ -109,14 +115,14 @@ test('5sim provider buys, checks, finishes, cancels, bans, and reuses activation
|
||||
assert.deepStrictEqual(
|
||||
requests.map((entry) => entry.url.pathname),
|
||||
[
|
||||
'/v1/guest/products/england/any',
|
||||
'/v1/guest/products/vietnam/any',
|
||||
'/v1/guest/prices',
|
||||
'/v1/user/buy/activation/england/any/openai',
|
||||
'/v1/user/buy/activation/vietnam/any/openai',
|
||||
'/v1/user/check/1001',
|
||||
'/v1/user/finish/1001',
|
||||
'/v1/user/cancel/1001',
|
||||
'/v1/user/ban/1001',
|
||||
'/v1/user/reuse/openai/447911123456',
|
||||
'/v1/user/reuse/openai/84901123456',
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -168,3 +174,89 @@ test('5sim provider prefers buy-compatible products price over operator detail p
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('5sim provider reports raw buy payload when HTTP 200 response has no activation', async () => {
|
||||
const provider = api.createProvider({
|
||||
fetchImpl: async (url) => {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.pathname === '/v1/guest/products/vietnam/any') {
|
||||
return createTextResponse({ openai: { Category: 'activation', Qty: 10, Price: 0.08 } });
|
||||
}
|
||||
if (parsed.pathname === '/v1/guest/prices') {
|
||||
return createTextResponse({ vietnam: { openai: { virtual47: { cost: 0.1282, count: 10 } } } });
|
||||
}
|
||||
if (parsed.pathname === '/v1/user/buy/activation/vietnam/any/openai') {
|
||||
return createTextResponse({ status: 'no free phones', detail: 'operator unavailable' });
|
||||
}
|
||||
throw new Error(`unexpected ${parsed.pathname}`);
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => provider.requestActivation({
|
||||
fiveSimApiKey: 'demo-key',
|
||||
fiveSimCountryId: 'vietnam',
|
||||
fiveSimCountryLabel: '越南 (Vietnam)',
|
||||
fiveSimOperator: 'any',
|
||||
}),
|
||||
/越南 \(Vietnam\): no free phones/
|
||||
);
|
||||
});
|
||||
|
||||
test('5sim provider reports purchase rate limit separately from no-number countries', async () => {
|
||||
const requests = [];
|
||||
const provider = api.createProvider({
|
||||
fetchImpl: async (url) => {
|
||||
const parsed = new URL(url);
|
||||
requests.push(parsed.pathname);
|
||||
if (parsed.pathname === '/v1/guest/products/thailand/any') {
|
||||
return createTextResponse({ openai: { Category: 'activation', Qty: 10, Price: 0.1 } });
|
||||
}
|
||||
if (parsed.pathname === '/v1/guest/products/vietnam/any') {
|
||||
return createTextResponse({ openai: { Category: 'activation', Qty: 10, Price: 0.08 } });
|
||||
}
|
||||
if (parsed.pathname === '/v1/guest/prices') {
|
||||
const country = parsed.searchParams.get('country');
|
||||
return createTextResponse({ [country]: { any: { openai: { cost: country === 'vietnam' ? 0.08 : 0.1, count: 10 } } } });
|
||||
}
|
||||
if (parsed.pathname === '/v1/user/buy/activation/thailand/any/openai') {
|
||||
return createTextResponse({ status: 'rate limit' });
|
||||
}
|
||||
if (parsed.pathname === '/v1/user/buy/activation/vietnam/any/openai') {
|
||||
return createTextResponse({ status: 'rate limit' });
|
||||
}
|
||||
throw new Error(`unexpected ${parsed.pathname}`);
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => provider.requestActivation({
|
||||
fiveSimApiKey: 'demo-key',
|
||||
fiveSimCountryId: 'thailand',
|
||||
fiveSimCountryLabel: '泰国 (Thailand)',
|
||||
fiveSimCountryFallback: [{ id: 'vietnam', label: '越南 (Vietnam)' }],
|
||||
fiveSimOperator: 'any',
|
||||
}),
|
||||
(error) => {
|
||||
assert.match(error.message, /^FIVE_SIM_RATE_LIMIT::/);
|
||||
assert.match(error.message, /5sim 购买接口触发限流/);
|
||||
assert.match(error.message, /泰国 \(Thailand\): rate limit/);
|
||||
assert.match(error.message, /越南 \(Vietnam\): rate limit/);
|
||||
assert.doesNotMatch(error.message, /均无可用号码/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
requests.filter((pathname) => pathname.includes('/buy/activation')),
|
||||
[
|
||||
'/v1/user/buy/activation/thailand/any/openai',
|
||||
'/v1/user/buy/activation/vietnam/any/openai',
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -100,6 +100,17 @@ test('GoPay approve does not treat phone linking page as debugger iframe action'
|
||||
assert.match(source, /filter\(\(target\) => target\.type === 'iframe'\)/);
|
||||
});
|
||||
|
||||
test('GoPay approve waits and retries slowly on Hubungkan linking page', () => {
|
||||
assert.match(source, /GOPAY_LINKING_RETRY_WAIT_MS/);
|
||||
assert.match(source, /GOPAY_LINKING_STABLE_WAIT_MS/);
|
||||
assert.match(source, /createGoPayStableStateTracker/);
|
||||
assert.match(source, /clickGoPayContinueBestEffort/);
|
||||
assert.match(source, /hubungkan\|sambungkan\|tautkan/);
|
||||
assert.match(source, /先等待 linking 页面加载\/跳转/);
|
||||
assert.match(source, /改用兜底点击 Hubungkan\/确认按钮/);
|
||||
assert.doesNotMatch(source, /GoPay 确认按钮点击后页面仍未变化,已暂停自动重复点击。请手动点击页面上的确认按钮,插件会继续等待后续页面。/);
|
||||
});
|
||||
|
||||
|
||||
test('background auto-run routes GoPay restart sentinel back to step 6', () => {
|
||||
const backgroundSource = fs.readFileSync('background.js', 'utf8');
|
||||
@@ -109,3 +120,10 @@ test('background auto-run routes GoPay restart sentinel back to step 6', () => {
|
||||
assert.match(backgroundSource, /step = 6/);
|
||||
assert.match(backgroundSource, /invalidateDownstreamAfterStepRestart\(5/);
|
||||
});
|
||||
|
||||
test('GoPay approve gives PIN precedence over OTP on ambiguous second PIN pages', () => {
|
||||
assert.match(source, /pageState\.hasPinInput && !pinSubmitted/);
|
||||
assert.match(source, /pageState\.hasOtpInput && !pageState\.hasPinInput && !otpSubmitted/);
|
||||
assert.ok(source.indexOf('pageState.hasPinInput && !pinSubmitted') < source.indexOf('pageState.hasOtpInput && !pageState.hasPinInput && !otpSubmitted'));
|
||||
assert.doesNotMatch(source, /otp\|one\[-\\s\]\*time\|kode\|verification\|whatsapp\|code\|pin-input-field/);
|
||||
});
|
||||
|
||||
@@ -137,6 +137,66 @@ return { getGoPayContinueTarget };
|
||||
assert.match(target.target, /Link and pay/);
|
||||
});
|
||||
|
||||
test('GoPay continue target recognizes Indonesian Hubungkan linking button', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getActionText'),
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('isEnabledControl'),
|
||||
extractFunction('findClickableByText'),
|
||||
extractFunction('findContinueButton'),
|
||||
extractFunction('describeElement'),
|
||||
extractFunction('getElementClickRect'),
|
||||
extractFunction('getGoPayContinueTarget'),
|
||||
].join('\n');
|
||||
const button = {
|
||||
tagName: 'BUTTON',
|
||||
id: '',
|
||||
className: 'btn primary',
|
||||
textContent: 'Hubungkan',
|
||||
innerText: 'Hubungkan',
|
||||
value: '',
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
parentElement: null,
|
||||
getAttribute(name) {
|
||||
if (name === 'class') return this.className;
|
||||
return '';
|
||||
},
|
||||
getBoundingClientRect() { return { left: 40, top: 720, width: 280, height: 48 }; },
|
||||
};
|
||||
const termsLink = {
|
||||
...button,
|
||||
tagName: 'A',
|
||||
className: 'terms-link',
|
||||
textContent: 'Syarat & Ketentuan',
|
||||
innerText: 'Syarat & Ketentuan',
|
||||
getBoundingClientRect() { return { left: 120, top: 650, width: 120, height: 16 }; },
|
||||
};
|
||||
const api = new Function('button', 'termsLink', `
|
||||
const window = {
|
||||
getComputedStyle() { return { display: 'block', visibility: 'visible', opacity: '1' }; },
|
||||
innerWidth: 390,
|
||||
innerHeight: 844,
|
||||
};
|
||||
const document = {
|
||||
querySelectorAll(selector) {
|
||||
return selector.includes('button') || selector.includes('a') || selector.includes('[role="button"]') ? [termsLink, button] : [];
|
||||
},
|
||||
};
|
||||
${bundle}
|
||||
return { findContinueButton, getGoPayContinueTarget };
|
||||
`)(button, termsLink);
|
||||
|
||||
assert.equal(api.findContinueButton(), button);
|
||||
const target = api.getGoPayContinueTarget();
|
||||
assert.equal(target.found, true);
|
||||
assert.match(target.target, /Hubungkan/);
|
||||
assert.equal(target.rect.centerX, 180);
|
||||
assert.equal(target.rect.centerY, 744);
|
||||
});
|
||||
|
||||
|
||||
test('GoPay PIN page detection wins over generic pin-input OTP attributes', () => {
|
||||
const bundle = [
|
||||
@@ -145,6 +205,7 @@ test('GoPay PIN page detection wins over generic pin-input OTP attributes', () =
|
||||
extractFunction('getPageBodyText'),
|
||||
extractFunction('isGoPayOtpPageText'),
|
||||
extractFunction('isGoPayPinPageText'),
|
||||
extractFunction('isGoPayPinInput'),
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('isEnabledControl'),
|
||||
@@ -155,6 +216,8 @@ test('GoPay PIN page detection wins over generic pin-input OTP attributes', () =
|
||||
extractFunction('findOtpInput'),
|
||||
extractFunction('getGoPayPinInputs'),
|
||||
extractFunction('findPinInput'),
|
||||
extractFunction('normalizeOtp'),
|
||||
extractFunction('fillVisibleOtpInputs'),
|
||||
].join('\n');
|
||||
const pinInputs = Array.from({ length: 6 }, (_, index) => ({
|
||||
tagName: 'INPUT',
|
||||
@@ -185,12 +248,13 @@ const document = {
|
||||
querySelectorAll(selector) { return selector.includes('input') ? pinInputs : []; },
|
||||
};
|
||||
${bundle}
|
||||
return { isGoPayOtpPageText, isGoPayPinPageText, findOtpInput, findPinInput, getGoPayPinInputs };
|
||||
return { isGoPayOtpPageText, isGoPayPinPageText, findOtpInput, findPinInput, getGoPayPinInputs, fillVisibleOtpInputs };
|
||||
`)(pinInputs);
|
||||
|
||||
assert.equal(api.isGoPayPinPageText(), true);
|
||||
assert.equal(api.isGoPayOtpPageText(), false);
|
||||
assert.equal(api.findOtpInput(), null);
|
||||
assert.equal(api.fillVisibleOtpInputs('123456'), false);
|
||||
assert.equal(api.findPinInput(), pinInputs[0]);
|
||||
assert.equal(api.getGoPayPinInputs().length, 6);
|
||||
});
|
||||
@@ -303,6 +367,7 @@ test('GoPay terminal timeout page is reported as retry-required state', () => {
|
||||
extractFunction('getActionText'),
|
||||
extractFunction('getPageBodyText'),
|
||||
extractFunction('isGoPayPinPageText'),
|
||||
extractFunction('isGoPayPinInput'),
|
||||
extractFunction('detectGoPayTerminalError'),
|
||||
extractFunction('isGoPayOtpPageText'),
|
||||
extractFunction('isVisibleElement'),
|
||||
|
||||
@@ -83,6 +83,56 @@ test('phone verification helper requests HeroSMS numbers with fixed OpenAI and T
|
||||
assert.equal(requests[1].searchParams.get('api_key'), 'demo-key');
|
||||
});
|
||||
|
||||
test('phone verification helper ignores HeroSMS virtual-only stock when physicalCount is zero', async () => {
|
||||
const requests = [];
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push(parsedUrl);
|
||||
const action = parsedUrl.searchParams.get('action');
|
||||
if (action === 'getPrices') {
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => buildHeroSmsPricesPayload({ count: 3, physicalCount: 0, cost: 0.05 }),
|
||||
};
|
||||
}
|
||||
if (action === 'getNumber' || action === 'getNumberV2') {
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => 'NO_NUMBERS',
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected HeroSMS action: ${action}`);
|
||||
},
|
||||
getState: async () => ({ heroSmsApiKey: 'demo-key' }),
|
||||
sendToContentScriptResilient: async () => ({}),
|
||||
setState: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key', heroSmsActivationRetryRounds: 1 }),
|
||||
/HeroSMS 已尝试 1 个候选国家,均无可用号码/
|
||||
);
|
||||
|
||||
const actions = requests.map((requestUrl) => `${requestUrl.searchParams.get('action')}:${requestUrl.searchParams.get('maxPrice') || ''}`);
|
||||
assert.deepStrictEqual(actions, [
|
||||
'getPrices:',
|
||||
'getPrices:',
|
||||
'getPrices:',
|
||||
'getNumber:',
|
||||
'getNumberV2:',
|
||||
'getPrices:',
|
||||
'getPrices:',
|
||||
'getPrices:',
|
||||
'getNumber:',
|
||||
'getNumberV2:',
|
||||
]);
|
||||
});
|
||||
|
||||
test('phone verification helper retries HeroSMS getPrices until it receives a usable lowest price', async () => {
|
||||
const requests = [];
|
||||
let getPricesAttempt = 0;
|
||||
@@ -4116,8 +4166,8 @@ test('phone verification helper routes 5sim buy, check, and finish by current ac
|
||||
let currentState = {
|
||||
phoneSmsProvider: '5sim',
|
||||
fiveSimApiKey: 'demo-key',
|
||||
fiveSimCountryId: 'england',
|
||||
fiveSimCountryLabel: 'England',
|
||||
fiveSimCountryId: 'vietnam',
|
||||
fiveSimCountryLabel: '越南 (Vietnam)',
|
||||
fiveSimMaxPrice: '12',
|
||||
fiveSimOperator: 'any',
|
||||
verificationResendCount: 0,
|
||||
@@ -4139,7 +4189,7 @@ test('phone verification helper routes 5sim buy, check, and finish by current ac
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push({ url: parsedUrl, options });
|
||||
if (parsedUrl.pathname === '/v1/guest/products/england/any') {
|
||||
if (parsedUrl.pathname === '/v1/guest/products/vietnam/any') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
@@ -4150,14 +4200,14 @@ test('phone verification helper routes 5sim buy, check, and finish by current ac
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({ england: { any: { openai: { cost: 9.5, count: 3 } } } }),
|
||||
text: async () => JSON.stringify({ vietnam: { any: { openai: { cost: 9.5, count: 3 } } } }),
|
||||
};
|
||||
}
|
||||
if (parsedUrl.pathname === '/v1/user/buy/activation/england/any/openai') {
|
||||
if (parsedUrl.pathname === '/v1/user/buy/activation/vietnam/any/openai') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({ id: 5001, phone: '+447911223344', country: 'england', operator: 'any', status: 'PENDING' }),
|
||||
text: async () => JSON.stringify({ id: 5001, phone: '+84901122334', country: 'vietnam', operator: 'any', status: 'PENDING' }),
|
||||
};
|
||||
}
|
||||
if (parsedUrl.pathname === '/v1/user/check/5001') {
|
||||
@@ -4216,9 +4266,9 @@ test('phone verification helper routes 5sim buy, check, and finish by current ac
|
||||
assert.deepStrictEqual(
|
||||
requests.map((entry) => entry.url.pathname),
|
||||
[
|
||||
'/v1/guest/products/england/any',
|
||||
'/v1/guest/products/vietnam/any',
|
||||
'/v1/guest/prices',
|
||||
'/v1/user/buy/activation/england/any/openai',
|
||||
'/v1/user/buy/activation/vietnam/any/openai',
|
||||
'/v1/user/check/5001',
|
||||
'/v1/user/finish/5001',
|
||||
]
|
||||
@@ -4230,8 +4280,8 @@ test('phone verification helper routes 5sim reusable activation through reuse en
|
||||
let currentState = {
|
||||
phoneSmsProvider: '5sim',
|
||||
fiveSimApiKey: 'demo-key',
|
||||
fiveSimCountryId: 'england',
|
||||
fiveSimCountryLabel: 'England',
|
||||
fiveSimCountryId: 'vietnam',
|
||||
fiveSimCountryLabel: '越南 (Vietnam)',
|
||||
fiveSimOperator: 'any',
|
||||
verificationResendCount: 0,
|
||||
phoneVerificationReplacementLimit: 2,
|
||||
@@ -4242,11 +4292,11 @@ test('phone verification helper routes 5sim reusable activation through reuse en
|
||||
currentPhoneActivation: null,
|
||||
reusablePhoneActivation: {
|
||||
activationId: '4001',
|
||||
phoneNumber: '+447911223344',
|
||||
phoneNumber: '+84901122334',
|
||||
provider: '5sim',
|
||||
serviceCode: 'openai',
|
||||
countryId: 'england',
|
||||
countryLabel: 'England',
|
||||
countryId: 'vietnam',
|
||||
countryLabel: '越南 (Vietnam)',
|
||||
successfulUses: 1,
|
||||
maxUses: 3,
|
||||
},
|
||||
@@ -4261,8 +4311,8 @@ test('phone verification helper routes 5sim reusable activation through reuse en
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push(parsedUrl);
|
||||
if (parsedUrl.pathname === '/v1/user/reuse/openai/447911223344') {
|
||||
return { ok: true, status: 200, text: async () => JSON.stringify({ id: 4002, phone: '+447911223344', country: 'england', status: 'PENDING' }) };
|
||||
if (parsedUrl.pathname === '/v1/user/reuse/openai/84901122334') {
|
||||
return { ok: true, status: 200, text: async () => JSON.stringify({ id: 4002, phone: '+84901122334', country: 'vietnam', status: 'PENDING' }) };
|
||||
}
|
||||
if (parsedUrl.pathname === '/v1/user/check/4002') {
|
||||
return { ok: true, status: 200, text: async () => JSON.stringify({ id: 4002, phone: '+447911223344', status: 'RECEIVED', sms: [{ code: '654321' }] }) };
|
||||
@@ -4306,7 +4356,7 @@ test('phone verification helper routes 5sim reusable activation through reuse en
|
||||
assert.deepStrictEqual(
|
||||
requests.map((url) => url.pathname),
|
||||
[
|
||||
'/v1/user/reuse/openai/447911223344',
|
||||
'/v1/user/reuse/openai/84901122334',
|
||||
'/v1/user/check/4002',
|
||||
'/v1/user/finish/4002',
|
||||
]
|
||||
|
||||
@@ -383,6 +383,7 @@ test('isPayPalPaymentMethodActive requires a selected PayPal control', () => {
|
||||
extractFunction('getPaymentMethodSearchCandidates'),
|
||||
extractFunction('getPayPalSearchCandidates'),
|
||||
extractFunction('hasCreditCardFields'),
|
||||
extractFunction('hasPaymentMethodSelectionMarker'),
|
||||
extractFunction('hasSelectedPaymentMethodControl'),
|
||||
extractFunction('hasSelectedPayPalControl'),
|
||||
extractFunction('isPaymentMethodActive'),
|
||||
@@ -725,6 +726,7 @@ test('payment method helpers can find and confirm selected GoPay controls', () =
|
||||
extractFunction('getGoPaySearchCandidates'),
|
||||
extractFunction('findPaymentMethodTarget'),
|
||||
extractFunction('findGoPayPaymentMethodTarget'),
|
||||
extractFunction('hasPaymentMethodSelectionMarker'),
|
||||
extractFunction('hasSelectedPaymentMethodControl'),
|
||||
extractFunction('hasSelectedGoPayControl'),
|
||||
extractFunction('isPaymentMethodActive'),
|
||||
@@ -774,6 +776,74 @@ return { findGoPayPaymentMethodTarget, getGoPaySearchCandidates, hasSelectedGoPa
|
||||
assert.equal(api.isGoPayPaymentMethodActive(), true);
|
||||
});
|
||||
|
||||
test('GoPay active detection accepts nested selected radio inside payment card', () => {
|
||||
const bundle = [
|
||||
"const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';",
|
||||
"const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';",
|
||||
"const PAYMENT_METHOD_CONFIGS = { paypal: { id: 'paypal', label: 'PayPal', patterns: [/paypal/i] }, gopay: { id: 'gopay', label: 'GoPay', patterns: [/gopay|go\\s*pay/i] } };",
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getActionText'),
|
||||
extractFunction('getSearchText'),
|
||||
extractFunction('getFieldText'),
|
||||
extractFunction('getCombinedSearchText'),
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('isDocumentLevelContainer'),
|
||||
extractFunction('normalizePlusPaymentMethod'),
|
||||
extractFunction('getPaymentMethodConfig'),
|
||||
extractFunction('getPaymentMethodSearchCandidates'),
|
||||
extractFunction('hasPaymentMethodSelectionMarker'),
|
||||
extractFunction('hasSelectedPaymentMethodControl'),
|
||||
extractFunction('hasSelectedGoPayControl'),
|
||||
extractFunction('isPaymentMethodActive'),
|
||||
extractFunction('isGoPayPaymentMethodActive'),
|
||||
].join('\n');
|
||||
|
||||
const radio = createElement({
|
||||
tagName: 'INPUT',
|
||||
attrs: {
|
||||
type: 'radio',
|
||||
role: 'radio',
|
||||
'aria-checked': 'true',
|
||||
value: 'gopay',
|
||||
},
|
||||
});
|
||||
radio.checked = true;
|
||||
const textNode = createElement({ tagName: 'SPAN', text: 'GoPay' });
|
||||
const card = createElement({ tagName: 'DIV', text: 'GoPay' });
|
||||
radio.parentElement = card;
|
||||
textNode.parentElement = card;
|
||||
card.children = [radio, textNode];
|
||||
card.querySelector = (selector) => String(selector || '').includes('radio') ? radio : null;
|
||||
const elements = [card, radio, textNode];
|
||||
const documentMock = {
|
||||
documentElement: {},
|
||||
body: {},
|
||||
querySelectorAll: (selector) => {
|
||||
if (String(selector || '').includes('label[for=')) return [];
|
||||
return elements.filter((element) => {
|
||||
if (String(selector || '').includes('input[type="radio"]')) return element === radio || element === card || element === textNode;
|
||||
return true;
|
||||
});
|
||||
},
|
||||
};
|
||||
const windowMock = {
|
||||
innerWidth: 1200,
|
||||
innerHeight: 900,
|
||||
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
|
||||
};
|
||||
const cssMock = {
|
||||
escape: (value) => String(value),
|
||||
};
|
||||
|
||||
const api = new Function('window', 'document', 'CSS', `
|
||||
${bundle}
|
||||
return { isGoPayPaymentMethodActive };
|
||||
`)(windowMock, documentMock, cssMock);
|
||||
|
||||
assert.equal(api.isGoPayPaymentMethodActive(), true);
|
||||
});
|
||||
|
||||
test('fillIfEmpty can overwrite invalid structured address values in the dropdown branch', () => {
|
||||
const bundle = [
|
||||
extractFunction('fillIfEmpty'),
|
||||
|
||||
@@ -50,6 +50,20 @@ function createIdAddressSeed() {
|
||||
};
|
||||
}
|
||||
|
||||
function createKrAddressSeed() {
|
||||
return {
|
||||
countryCode: 'KR',
|
||||
query: 'Seoul Jung-gu',
|
||||
suggestionIndex: 1,
|
||||
fallback: {
|
||||
address1: 'Sejong-daero 110',
|
||||
city: 'Jung-gu',
|
||||
region: 'Seoul',
|
||||
postalCode: '04524',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createSuccessfulBillingResult() {
|
||||
return {
|
||||
countryText: 'Germany',
|
||||
@@ -68,6 +82,7 @@ function createExecutorHarness({
|
||||
fetchImpl = null,
|
||||
getAddressSeedForCountry = () => createAddressSeed(),
|
||||
markCurrentRegistrationAccountUsed = async () => {},
|
||||
probeIpProxyExit = null,
|
||||
submitRedirectUrl = 'https://www.paypal.com/checkoutnow',
|
||||
}) {
|
||||
const api = loadPlusCheckoutBillingModule();
|
||||
@@ -149,6 +164,7 @@ function createExecutorHarness({
|
||||
assert.equal(matcher(submitRedirectUrl), true);
|
||||
return { id: tabId, url: submitRedirectUrl };
|
||||
},
|
||||
...(typeof probeIpProxyExit === 'function' ? { probeIpProxyExit } : {}),
|
||||
});
|
||||
|
||||
return { checkoutTab, events, executor };
|
||||
@@ -240,7 +256,7 @@ test('Plus checkout billing sends the billing command to the iframe that contain
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
});
|
||||
|
||||
test('Plus checkout billing forces Indonesia address for GoPay even when page country differs', async () => {
|
||||
test('Plus checkout billing uses proxy exit country for GoPay address when available', async () => {
|
||||
const requestedCountries = [];
|
||||
const fetchRequests = [];
|
||||
const { events, executor } = createExecutorHarness({
|
||||
@@ -263,7 +279,17 @@ test('Plus checkout billing forces Indonesia address for GoPay even when page co
|
||||
},
|
||||
getAddressSeedForCountry: (countryValue) => {
|
||||
requestedCountries.push(countryValue);
|
||||
return countryValue === 'ID' ? createIdAddressSeed() : createAddressSeed();
|
||||
return countryValue === 'JP' ? {
|
||||
countryCode: 'JP',
|
||||
query: 'Tokyo Marunouchi',
|
||||
suggestionIndex: 1,
|
||||
fallback: {
|
||||
address1: 'Marunouchi 1-1',
|
||||
city: 'Chiyoda-ku',
|
||||
region: 'Tokyo',
|
||||
postalCode: '100-0005',
|
||||
},
|
||||
} : createIdAddressSeed();
|
||||
},
|
||||
fetchImpl: async (url, init) => {
|
||||
fetchRequests.push({ url, init });
|
||||
@@ -273,10 +299,11 @@ test('Plus checkout billing forces Indonesia address for GoPay even when page co
|
||||
json: async () => ({
|
||||
status: 'ok',
|
||||
address: {
|
||||
Address: 'Jl. M.H. Thamrin No. 10',
|
||||
City: 'Jakarta',
|
||||
State: 'DKI Jakarta',
|
||||
Zip_Code: '10310',
|
||||
Address: 'トウキョウト, チヨダク, マルノウチ, 1-1',
|
||||
Trans_Address: 'Marunouchi 1-1, Chiyoda-ku, Tokyo',
|
||||
City: 'Tokyo',
|
||||
State: 'Tokyo',
|
||||
Zip_Code: '100-0005',
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -284,17 +311,206 @@ test('Plus checkout billing forces Indonesia address for GoPay even when page co
|
||||
submitRedirectUrl: 'https://app.midtrans.com/snap/v4/redirection/session#/gopay-tokenization/linking',
|
||||
});
|
||||
|
||||
await executor.executePlusCheckoutBilling({ plusPaymentMethod: 'gopay', plusCheckoutCountry: 'US' });
|
||||
await executor.executePlusCheckoutBilling({
|
||||
plusPaymentMethod: 'gopay',
|
||||
plusCheckoutCountry: 'ID',
|
||||
ipProxyAppliedExitRegion: 'JP',
|
||||
});
|
||||
|
||||
const fillMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS');
|
||||
assert.equal(requestedCountries[0], 'ID');
|
||||
assert.equal(fillMessage.message.payload.addressSeed.countryCode, 'ID');
|
||||
assert.equal(requestedCountries[0], 'JP');
|
||||
assert.equal(fillMessage.message.payload.addressSeed.countryCode, 'JP');
|
||||
assert.equal(fillMessage.message.payload.addressSeed.source, 'meiguodizhi');
|
||||
assert.deepEqual(JSON.parse(fetchRequests[0].init.body), {
|
||||
city: 'Jakarta',
|
||||
path: '/id-address',
|
||||
city: 'Chiyoda-ku',
|
||||
path: '/jp-address',
|
||||
method: 'refresh',
|
||||
});
|
||||
assert.equal(events.logs.some((entry) => /GoPay 账单地址将按当前代理出口地区 JP/.test(entry.message)), true);
|
||||
});
|
||||
|
||||
test('Plus checkout billing refreshes stale GoPay proxy country before filling address', async () => {
|
||||
const requestedCountries = [];
|
||||
const probeCalls = [];
|
||||
const { events, executor } = createExecutorHarness({
|
||||
frames: [
|
||||
{ frameId: 0, url: 'https://chatgpt.com/checkout/openai_llc/cs_test' },
|
||||
{ frameId: 7, url: 'https://js.stripe.com/v3/elements-inner-payment.html' },
|
||||
{ frameId: 8, url: 'https://js.stripe.com/v3/elements-inner-address.html' },
|
||||
],
|
||||
stateByFrame: {
|
||||
0: { hasPayPal: false, hasGoPay: false, paypalCandidates: [], gopayCandidates: [], hasSubscribeButton: true },
|
||||
7: { hasPayPal: false, hasGoPay: true, gopayCandidates: [{ tag: 'button', text: 'GoPay' }] },
|
||||
8: {
|
||||
hasPayPal: false,
|
||||
hasGoPay: false,
|
||||
paypalCandidates: [],
|
||||
gopayCandidates: [],
|
||||
billingFieldsVisible: true,
|
||||
countryText: 'Indonesia',
|
||||
},
|
||||
},
|
||||
getAddressSeedForCountry: (countryValue) => {
|
||||
requestedCountries.push(countryValue);
|
||||
return countryValue === 'JP' ? {
|
||||
countryCode: 'JP',
|
||||
query: 'Tokyo Chiyoda-ku',
|
||||
suggestionIndex: 1,
|
||||
fallback: {
|
||||
address1: 'Marunouchi 1-1',
|
||||
city: 'Chiyoda-ku',
|
||||
region: 'Tokyo',
|
||||
postalCode: '100-0005',
|
||||
},
|
||||
} : createKrAddressSeed();
|
||||
},
|
||||
fetchImpl: async () => ({
|
||||
ok: false,
|
||||
status: 503,
|
||||
json: async () => ({ status: 'error' }),
|
||||
}),
|
||||
probeIpProxyExit: async (options) => {
|
||||
probeCalls.push(options);
|
||||
return {
|
||||
proxyRouting: {
|
||||
exitRegion: 'JP',
|
||||
exitIp: '203.0.113.8',
|
||||
exitSource: 'page_context',
|
||||
exitEndpoint: 'https://ipinfo.io/json',
|
||||
},
|
||||
};
|
||||
},
|
||||
submitRedirectUrl: 'https://app.midtrans.com/snap/v4/redirection/session#/gopay-tokenization/linking',
|
||||
});
|
||||
|
||||
await executor.executePlusCheckoutBilling({
|
||||
plusPaymentMethod: 'gopay',
|
||||
plusCheckoutCountry: 'ID',
|
||||
ipProxyAppliedExitRegion: 'KR',
|
||||
});
|
||||
|
||||
const fillMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS');
|
||||
assert.equal(probeCalls.length, 1);
|
||||
assert.equal(probeCalls[0].detectWhenDisabled, true);
|
||||
assert.equal(requestedCountries[0], 'JP');
|
||||
assert.equal(fillMessage.message.payload.addressSeed.countryCode, 'JP');
|
||||
assert.equal(events.logs.some((entry) => entry.message.includes('当前代理出口复测结果:JP / 203.0.113.8')), true);
|
||||
assert.equal(events.logs.some((entry) => /GoPay 账单地址将按当前代理出口地区 JP/.test(entry.message)), true);
|
||||
assert.equal(events.logs.some((entry) => /GoPay 账单地址将按当前代理出口地区 KR/.test(entry.message)), false);
|
||||
});
|
||||
|
||||
test('Plus checkout billing refuses to reuse stale GoPay proxy country when refresh has no region', async () => {
|
||||
const requestedCountries = [];
|
||||
const { events, executor } = createExecutorHarness({
|
||||
frames: [
|
||||
{ frameId: 0, url: 'https://chatgpt.com/checkout/openai_llc/cs_test' },
|
||||
{ frameId: 7, url: 'https://js.stripe.com/v3/elements-inner-payment.html' },
|
||||
{ frameId: 8, url: 'https://js.stripe.com/v3/elements-inner-address.html' },
|
||||
],
|
||||
stateByFrame: {
|
||||
0: { hasPayPal: false, hasGoPay: false, paypalCandidates: [], gopayCandidates: [], hasSubscribeButton: true },
|
||||
7: { hasPayPal: false, hasGoPay: true, gopayCandidates: [{ tag: 'button', text: 'GoPay' }] },
|
||||
8: {
|
||||
hasPayPal: false,
|
||||
hasGoPay: false,
|
||||
paypalCandidates: [],
|
||||
gopayCandidates: [],
|
||||
billingFieldsVisible: true,
|
||||
countryText: 'Indonesia',
|
||||
},
|
||||
},
|
||||
getAddressSeedForCountry: (countryValue) => {
|
||||
requestedCountries.push(countryValue);
|
||||
return createKrAddressSeed();
|
||||
},
|
||||
probeIpProxyExit: async () => ({
|
||||
proxyRouting: {
|
||||
reason: 'disabled_probe_only',
|
||||
exitIp: '203.0.113.9',
|
||||
exitRegion: '',
|
||||
exitError: 'missing_region',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executePlusCheckoutBilling({
|
||||
plusPaymentMethod: 'gopay',
|
||||
plusCheckoutCountry: 'ID',
|
||||
ipProxyAppliedExitRegion: 'KR',
|
||||
}),
|
||||
/本次复测没有拿到国家码/
|
||||
);
|
||||
|
||||
assert.equal(requestedCountries.length, 0);
|
||||
assert.equal(events.logs.some((entry) => /已清空旧出口地区 KR/.test(entry.message)), true);
|
||||
assert.equal(events.logs.some((entry) => /GoPay 账单地址将按当前代理出口地区 KR/.test(entry.message)), false);
|
||||
});
|
||||
|
||||
test('Plus checkout billing normalizes legacy Korean postal code for GoPay address', async () => {
|
||||
const requestedCountries = [];
|
||||
const fetchRequests = [];
|
||||
const { events, executor } = createExecutorHarness({
|
||||
frames: [
|
||||
{ frameId: 0, url: 'https://chatgpt.com/checkout/openai_llc/cs_test' },
|
||||
{ frameId: 7, url: 'https://js.stripe.com/v3/elements-inner-payment.html' },
|
||||
{ frameId: 8, url: 'https://js.stripe.com/v3/elements-inner-address.html' },
|
||||
],
|
||||
stateByFrame: {
|
||||
0: { hasPayPal: false, hasGoPay: false, paypalCandidates: [], gopayCandidates: [], hasSubscribeButton: true },
|
||||
7: { hasPayPal: false, hasGoPay: true, gopayCandidates: [{ tag: 'button', text: 'GoPay' }] },
|
||||
8: {
|
||||
hasPayPal: false,
|
||||
hasGoPay: false,
|
||||
paypalCandidates: [],
|
||||
gopayCandidates: [],
|
||||
billingFieldsVisible: true,
|
||||
countryText: 'United States',
|
||||
},
|
||||
},
|
||||
getAddressSeedForCountry: (countryValue) => {
|
||||
requestedCountries.push(countryValue);
|
||||
return countryValue === 'KR' ? createKrAddressSeed() : createIdAddressSeed();
|
||||
},
|
||||
fetchImpl: async (url, init) => {
|
||||
fetchRequests.push({ url, init });
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
status: 'ok',
|
||||
address: {
|
||||
Address: '서울특별시 중구 세종대로 110',
|
||||
Trans_Address: 'Sejong-daero 110, Jung-gu, Seoul',
|
||||
City: 'Jung-gu',
|
||||
State: 'Seoul',
|
||||
Zip_Code: '150-300',
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
submitRedirectUrl: 'https://app.midtrans.com/snap/v4/redirection/session#/gopay-tokenization/linking',
|
||||
});
|
||||
|
||||
await executor.executePlusCheckoutBilling({
|
||||
plusPaymentMethod: 'gopay',
|
||||
plusCheckoutCountry: 'ID',
|
||||
ipProxyAppliedExitRegion: 'KR',
|
||||
});
|
||||
|
||||
const fillMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS');
|
||||
assert.equal(requestedCountries[0], 'KR');
|
||||
assert.equal(fillMessage.message.payload.addressSeed.countryCode, 'KR');
|
||||
assert.equal(fillMessage.message.payload.addressSeed.source, 'meiguodizhi');
|
||||
assert.equal(fillMessage.message.payload.addressSeed.fallback.address1, 'Sejong-daero 110, Jung-gu, Seoul');
|
||||
assert.equal(fillMessage.message.payload.addressSeed.fallback.postalCode, '04524');
|
||||
assert.match(fillMessage.message.payload.addressSeed.fallback.postalCode, /^\d{5}$/);
|
||||
assert.deepEqual(JSON.parse(fetchRequests[0].init.body), {
|
||||
city: 'Jung-gu',
|
||||
path: '/kr-address',
|
||||
method: 'refresh',
|
||||
});
|
||||
assert.equal(events.logs.some((entry) => /GoPay 账单地址将按当前代理出口地区 KR/.test(entry.message)), true);
|
||||
});
|
||||
|
||||
test('Plus checkout billing selects GoPay and waits for a GoPay redirect', async () => {
|
||||
|
||||
@@ -180,11 +180,13 @@ test('collectSettingsPayload omits custom password and local sync settings in co
|
||||
|
||||
const api = new Function('normalizeIcloudTargetMailboxType', 'normalizeIcloudForwardMailProvider', `
|
||||
let latestState = { contributionMode: true };
|
||||
const window = {};
|
||||
let cloudflareDomainEditMode = false;
|
||||
let cloudflareTempEmailDomainEditMode = false;
|
||||
const selectCfDomain = { value: 'example.com' };
|
||||
const selectTempEmailDomain = { value: 'mail.example.com' };
|
||||
const selectPanelMode = { value: 'cpa' };
|
||||
function getSelectedPlusPaymentMethod() { return 'paypal'; }
|
||||
const inputVpsUrl = { value: 'https://panel.example.com' };
|
||||
const inputVpsPassword = { value: 'panel-secret' };
|
||||
const inputSub2ApiUrl = { value: 'https://sub.example.com' };
|
||||
@@ -237,9 +239,11 @@ const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
|
||||
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
|
||||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||
|
||||
@@ -85,11 +85,13 @@ test('collectSettingsPayload persists icloud target mailbox settings', () => {
|
||||
|
||||
const api = new Function(`
|
||||
let latestState = { contributionMode: false };
|
||||
const window = {};
|
||||
let cloudflareDomainEditMode = false;
|
||||
let cloudflareTempEmailDomainEditMode = false;
|
||||
const selectCfDomain = { value: '' };
|
||||
const selectTempEmailDomain = { value: '' };
|
||||
const selectPanelMode = { value: 'cpa' };
|
||||
function getSelectedPlusPaymentMethod() { return 'paypal'; }
|
||||
const inputVpsUrl = { value: '' };
|
||||
const inputVpsPassword = { value: '' };
|
||||
const inputSub2ApiUrl = { value: '' };
|
||||
@@ -136,9 +138,11 @@ const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
|
||||
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
|
||||
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||
@@ -421,9 +425,11 @@ const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
|
||||
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
|
||||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||||
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||||
|
||||
@@ -158,11 +158,13 @@ let latestState = {
|
||||
mail2925UseAccountPool: true,
|
||||
currentMail2925AccountId: 'acc-2',
|
||||
};
|
||||
const window = {};
|
||||
let cloudflareDomainEditMode = false;
|
||||
let cloudflareTempEmailDomainEditMode = false;
|
||||
const selectCfDomain = { value: 'example.com' };
|
||||
const selectTempEmailDomain = { value: 'mail.example.com' };
|
||||
const selectPanelMode = { value: 'cpa' };
|
||||
function getSelectedPlusPaymentMethod() { return 'paypal'; }
|
||||
const inputVpsUrl = { value: '' };
|
||||
const inputVpsPassword = { value: '' };
|
||||
const inputSub2ApiUrl = { value: '' };
|
||||
@@ -207,9 +209,11 @@ const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
|
||||
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
|
||||
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||
|
||||
@@ -293,6 +293,7 @@ return {
|
||||
|
||||
test('collectSettingsPayload keeps local helper sync enabled while persisting sms toggle state', () => {
|
||||
const api = new Function('normalizeIcloudTargetMailboxType', 'normalizeIcloudForwardMailProvider', `
|
||||
const window = {};
|
||||
let latestState = {
|
||||
contributionMode: false,
|
||||
mail2925UseAccountPool: false,
|
||||
@@ -390,9 +391,11 @@ const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
|
||||
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
|
||||
const selectHeroSmsCountry = {
|
||||
value: '52',
|
||||
selectedIndex: 0,
|
||||
@@ -403,6 +406,7 @@ function normalizeCloudflareDomainValue(value) { return String(value || '').trim
|
||||
function getCloudflareTempEmailDomainsFromState() { return { domains: [], activeDomain: '' }; }
|
||||
function normalizeCloudflareTempEmailDomainValue(value) { return String(value || '').trim(); }
|
||||
function getSelectedLocalCpaStep9Mode() { return 'submit'; }
|
||||
function getSelectedPlusPaymentMethod() { return 'paypal'; }
|
||||
function getSelectedMail2925Mode() { return 'provide'; }
|
||||
function getSelectedHotmailServiceMode() { return 'local'; }
|
||||
function buildManagedAliasBaseEmailPayload() { return { gmailBaseEmail: '', mail2925BaseEmail: '', emailPrefix: '' }; }
|
||||
@@ -473,7 +477,7 @@ return { collectSettingsPayload };
|
||||
assert.equal(payload.heroSmsCountryLabel, 'Thailand');
|
||||
assert.deepStrictEqual(payload.heroSmsCountryFallback, [{ id: 16, label: 'United Kingdom' }]);
|
||||
assert.equal(payload.fiveSimApiKey, '');
|
||||
assert.equal(payload.fiveSimCountryId, 'england');
|
||||
assert.equal(payload.fiveSimCountryId, 'vietnam');
|
||||
});
|
||||
|
||||
test('switchPhoneSmsProvider saves API keys independently when the select value has already changed', async () => {
|
||||
@@ -487,18 +491,20 @@ let latestState = {
|
||||
heroSmsCountryId: 52,
|
||||
heroSmsCountryLabel: 'Thailand',
|
||||
heroSmsCountryFallback: [],
|
||||
fiveSimCountryId: 'england',
|
||||
fiveSimCountryLabel: '英国 (England)',
|
||||
fiveSimCountryId: 'vietnam',
|
||||
fiveSimCountryLabel: '越南 (Vietnam)',
|
||||
fiveSimCountryFallback: [],
|
||||
fiveSimOperator: 'any',
|
||||
};
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
|
||||
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
|
||||
const selectPhoneSmsProvider = { value: 'hero-sms', dataset: { activeProvider: 'hero-sms' } };
|
||||
const inputHeroSmsApiKey = { value: 'hero-live' };
|
||||
const inputHeroSmsMaxPrice = { value: '0.22' };
|
||||
@@ -530,7 +536,7 @@ function getSelectedHeroSmsCountryOption() {
|
||||
}
|
||||
function syncHeroSmsFallbackSelectionOrderFromSelect() {
|
||||
return getSelectedPhoneSmsProvider() === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
? [{ id: 'england', label: '英国 (England)' }]
|
||||
? [{ id: 'vietnam', label: '越南 (Vietnam)' }]
|
||||
: [{ id: 52, label: 'Thailand' }];
|
||||
}
|
||||
function syncLatestState(patch) { latestState = { ...latestState, ...patch }; }
|
||||
@@ -574,14 +580,40 @@ return {
|
||||
assert.equal(api.savedPayload.fiveSimApiKey, 'five-live');
|
||||
});
|
||||
|
||||
test('formatPhoneSmsPriceEntriesSummary treats HeroSMS physicalCount=0 as out of stock even when count is positive', () => {
|
||||
const api = new Function(`
|
||||
${extractFunction('normalizeHeroSmsPriceForPreview')}
|
||||
${extractFunction('collectHeroSmsPriceEntriesForPreview')}
|
||||
${extractFunction('formatPhoneSmsPriceEntriesSummary')}
|
||||
return { formatPhoneSmsPriceEntriesSummary };
|
||||
`)();
|
||||
|
||||
const summary = api.formatPhoneSmsPriceEntriesSummary({
|
||||
52: {
|
||||
dr: {
|
||||
cost: 0.05,
|
||||
count: 3,
|
||||
physicalCount: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(summary.inStockPrices, []);
|
||||
assert.deepStrictEqual(summary.allPrices, [0.05]);
|
||||
assert.equal(summary.entries[0].inStock, false);
|
||||
assert.equal(summary.entries[0].stockCount, 0);
|
||||
});
|
||||
|
||||
test('previewHeroSmsPriceTiers prefers 5sim products price for buy-compatible any operator', async () => {
|
||||
const api = new Function(`
|
||||
let latestState = { phoneSmsProvider: '5sim', fiveSimOperator: 'any' };
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
|
||||
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
|
||||
const inputHeroSmsMaxPrice = { value: '' };
|
||||
const inputHeroSmsApiKey = { value: '' };
|
||||
const inputFiveSimOperator = { value: 'any' };
|
||||
|
||||
+1
-1
@@ -459,7 +459,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
|
||||
|
||||
- HeroSMS 和 5sim 都归入接码平台适配层;默认平台是 HeroSMS,5sim 产品码固定为 `openai`,operator 默认 `any`。
|
||||
- 号码当前最多复用 3 次成功注册;超过上限后会清空可复用激活记录,下次重新申请新号码。
|
||||
- 5sim 的国家/地区使用字符串 slug(如 `england / usa / thailand`),HeroSMS 仍使用数字国家 ID。
|
||||
- 5sim 的国家/地区使用字符串 slug(当前开放 `indonesia / thailand / vietnam`),HeroSMS 仍使用数字国家 ID(当前开放印度尼西亚/泰国/越南)。
|
||||
- 如果同一个号码在重发短信后仍收不到验证码,后台会按当前平台取消或 ban 激活并换号,而不是把当前号码无限重试下去。
|
||||
|
||||
### Step 10
|
||||
|
||||
Reference in New Issue
Block a user