feat(flow): stabilize step7-9, expand HeroSMS, and update usage tutorial
This commit is contained in:
+346
-1
@@ -265,6 +265,21 @@ const AUTO_STEP_DELAY_MAX_ALLOWED_SECONDS = 600;
|
|||||||
const VERIFICATION_RESEND_COUNT_MIN = 0;
|
const VERIFICATION_RESEND_COUNT_MIN = 0;
|
||||||
const VERIFICATION_RESEND_COUNT_MAX = 20;
|
const VERIFICATION_RESEND_COUNT_MAX = 20;
|
||||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||||
|
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||||||
|
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||||||
|
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||||
|
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||||||
|
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||||||
|
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
|
||||||
|
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||||||
|
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
|
||||||
|
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
|
||||||
|
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
|
||||||
|
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
|
||||||
|
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
|
||||||
|
const PHONE_CODE_POLL_ROUNDS_MIN = 1;
|
||||||
|
const PHONE_CODE_POLL_ROUNDS_MAX = 120;
|
||||||
|
const DEFAULT_PHONE_CODE_POLL_ROUNDS = 4;
|
||||||
const LEGACY_AUTO_STEP_DELAY_KEYS = ['autoStepRandomDelayMinSeconds', 'autoStepRandomDelayMaxSeconds'];
|
const LEGACY_AUTO_STEP_DELAY_KEYS = ['autoStepRandomDelayMinSeconds', 'autoStepRandomDelayMaxSeconds'];
|
||||||
const LEGACY_VERIFICATION_RESEND_COUNT_KEYS = ['signupVerificationResendCount', 'loginVerificationResendCount'];
|
const LEGACY_VERIFICATION_RESEND_COUNT_KEYS = ['signupVerificationResendCount', 'loginVerificationResendCount'];
|
||||||
const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
|
const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
|
||||||
@@ -283,6 +298,10 @@ const HERO_SMS_SERVICE_CODE = 'dr';
|
|||||||
const HERO_SMS_SERVICE_LABEL = 'OpenAI';
|
const HERO_SMS_SERVICE_LABEL = 'OpenAI';
|
||||||
const HERO_SMS_COUNTRY_ID = 52;
|
const HERO_SMS_COUNTRY_ID = 52;
|
||||||
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||||
|
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||||
|
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||||
|
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||||
|
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||||
const DISPLAY_TIMEZONE = 'Asia/Shanghai';
|
const DISPLAY_TIMEZONE = 'Asia/Shanghai';
|
||||||
const MICROSOFT_TOKEN_DNR_RULE_ID = 1001;
|
const MICROSOFT_TOKEN_DNR_RULE_ID = 1001;
|
||||||
const PERSISTENT_ALIAS_STATE_KEYS = [
|
const PERSISTENT_ALIAS_STATE_KEYS = [
|
||||||
@@ -446,6 +465,11 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
autoStepDelaySeconds: null,
|
autoStepDelaySeconds: null,
|
||||||
phoneVerificationEnabled: false,
|
phoneVerificationEnabled: false,
|
||||||
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
|
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
|
||||||
|
phoneVerificationReplacementLimit: DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT,
|
||||||
|
phoneCodeWaitSeconds: DEFAULT_PHONE_CODE_WAIT_SECONDS,
|
||||||
|
phoneCodeTimeoutWindows: DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS,
|
||||||
|
phoneCodePollIntervalSeconds: DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS,
|
||||||
|
phoneCodePollMaxRounds: DEFAULT_PHONE_CODE_POLL_ROUNDS,
|
||||||
mailProvider: '163',
|
mailProvider: '163',
|
||||||
mail2925Mode: DEFAULT_MAIL_2925_MODE,
|
mail2925Mode: DEFAULT_MAIL_2925_MODE,
|
||||||
mail2925UseAccountPool: false,
|
mail2925UseAccountPool: false,
|
||||||
@@ -481,9 +505,12 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
mail2925Accounts: [],
|
mail2925Accounts: [],
|
||||||
paypalAccounts: [],
|
paypalAccounts: [],
|
||||||
heroSmsApiKey: '',
|
heroSmsApiKey: '',
|
||||||
|
heroSmsReuseEnabled: DEFAULT_HERO_SMS_REUSE_ENABLED,
|
||||||
|
heroSmsAcquirePriority: DEFAULT_HERO_SMS_ACQUIRE_PRIORITY,
|
||||||
heroSmsMaxPrice: '',
|
heroSmsMaxPrice: '',
|
||||||
heroSmsCountryId: HERO_SMS_COUNTRY_ID,
|
heroSmsCountryId: HERO_SMS_COUNTRY_ID,
|
||||||
heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL,
|
heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL,
|
||||||
|
heroSmsCountryFallback: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
||||||
@@ -563,7 +590,13 @@ const DEFAULT_STATE = {
|
|||||||
currentLuckmailPurchase: null,
|
currentLuckmailPurchase: null,
|
||||||
currentLuckmailMailCursor: null,
|
currentLuckmailMailCursor: null,
|
||||||
currentPhoneActivation: null,
|
currentPhoneActivation: null,
|
||||||
|
currentPhoneVerificationCode: '',
|
||||||
reusablePhoneActivation: null,
|
reusablePhoneActivation: null,
|
||||||
|
heroSmsLastPriceTiers: [],
|
||||||
|
heroSmsLastPriceCountryId: 0,
|
||||||
|
heroSmsLastPriceCountryLabel: '',
|
||||||
|
heroSmsLastPriceUserLimit: '',
|
||||||
|
heroSmsLastPriceAt: 0,
|
||||||
pendingPhoneActivationConfirmation: null,
|
pendingPhoneActivationConfirmation: null,
|
||||||
autoRunning: false, // 当前是否处于自动运行中。
|
autoRunning: false, // 当前是否处于自动运行中。
|
||||||
autoRunPhase: 'idle', // 当前自动运行阶段。
|
autoRunPhase: 'idle', // 当前自动运行阶段。
|
||||||
@@ -664,6 +697,143 @@ function normalizeVerificationResendCount(value, fallback) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePhoneVerificationReplacementLimit(value, fallback = DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT) {
|
||||||
|
const rawValue = String(value ?? '').trim();
|
||||||
|
const numeric = Number(rawValue);
|
||||||
|
if (!rawValue || !Number.isFinite(numeric)) {
|
||||||
|
return Math.min(
|
||||||
|
PHONE_REPLACEMENT_LIMIT_MAX,
|
||||||
|
Math.max(PHONE_REPLACEMENT_LIMIT_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Math.min(
|
||||||
|
PHONE_REPLACEMENT_LIMIT_MAX,
|
||||||
|
Math.max(PHONE_REPLACEMENT_LIMIT_MIN, Math.floor(numeric))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePhoneCodeWaitSeconds(value, fallback = DEFAULT_PHONE_CODE_WAIT_SECONDS) {
|
||||||
|
const rawValue = String(value ?? '').trim();
|
||||||
|
const numeric = Number(rawValue);
|
||||||
|
if (!rawValue || !Number.isFinite(numeric)) {
|
||||||
|
return Math.min(
|
||||||
|
PHONE_CODE_WAIT_SECONDS_MAX,
|
||||||
|
Math.max(PHONE_CODE_WAIT_SECONDS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_WAIT_SECONDS))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Math.min(
|
||||||
|
PHONE_CODE_WAIT_SECONDS_MAX,
|
||||||
|
Math.max(PHONE_CODE_WAIT_SECONDS_MIN, Math.floor(numeric))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePhoneCodeTimeoutWindows(value, fallback = DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS) {
|
||||||
|
const rawValue = String(value ?? '').trim();
|
||||||
|
const numeric = Number(rawValue);
|
||||||
|
if (!rawValue || !Number.isFinite(numeric)) {
|
||||||
|
return Math.min(
|
||||||
|
PHONE_CODE_TIMEOUT_WINDOWS_MAX,
|
||||||
|
Math.max(PHONE_CODE_TIMEOUT_WINDOWS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Math.min(
|
||||||
|
PHONE_CODE_TIMEOUT_WINDOWS_MAX,
|
||||||
|
Math.max(PHONE_CODE_TIMEOUT_WINDOWS_MIN, Math.floor(numeric))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePhoneCodePollIntervalSeconds(value, fallback = DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS) {
|
||||||
|
const rawValue = String(value ?? '').trim();
|
||||||
|
const numeric = Number(rawValue);
|
||||||
|
if (!rawValue || !Number.isFinite(numeric)) {
|
||||||
|
return Math.min(
|
||||||
|
PHONE_CODE_POLL_INTERVAL_SECONDS_MAX,
|
||||||
|
Math.max(PHONE_CODE_POLL_INTERVAL_SECONDS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Math.min(
|
||||||
|
PHONE_CODE_POLL_INTERVAL_SECONDS_MAX,
|
||||||
|
Math.max(PHONE_CODE_POLL_INTERVAL_SECONDS_MIN, Math.floor(numeric))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePhoneCodePollMaxRounds(value, fallback = DEFAULT_PHONE_CODE_POLL_ROUNDS) {
|
||||||
|
const rawValue = String(value ?? '').trim();
|
||||||
|
const numeric = Number(rawValue);
|
||||||
|
if (!rawValue || !Number.isFinite(numeric)) {
|
||||||
|
return Math.min(
|
||||||
|
PHONE_CODE_POLL_ROUNDS_MAX,
|
||||||
|
Math.max(PHONE_CODE_POLL_ROUNDS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_POLL_ROUNDS))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Math.min(
|
||||||
|
PHONE_CODE_POLL_ROUNDS_MAX,
|
||||||
|
Math.max(PHONE_CODE_POLL_ROUNDS_MIN, Math.floor(numeric))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHeroSmsMaxPrice(value = '') {
|
||||||
|
const rawValue = String(value ?? '').trim();
|
||||||
|
if (!rawValue) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const numeric = Number(rawValue);
|
||||||
|
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return String(Math.round(numeric * 10000) / 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHeroSmsAcquirePriority(value = '') {
|
||||||
|
return String(value || '').trim().toLowerCase() === HERO_SMS_ACQUIRE_PRIORITY_PRICE
|
||||||
|
? HERO_SMS_ACQUIRE_PRIORITY_PRICE
|
||||||
|
: HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHeroSmsCountryFallback(value = []) {
|
||||||
|
const source = Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: String(value || '')
|
||||||
|
.split(/[\r\n,,;;]+/)
|
||||||
|
.map((entry) => String(entry || '').trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const seenIds = new Set();
|
||||||
|
const normalized = [];
|
||||||
|
|
||||||
|
for (const entry of source) {
|
||||||
|
let countryId = 0;
|
||||||
|
let countryLabel = '';
|
||||||
|
|
||||||
|
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
||||||
|
countryId = Math.floor(Number(entry.countryId ?? entry.id) || 0);
|
||||||
|
countryLabel = String((entry.countryLabel ?? entry.label) || '').trim();
|
||||||
|
} else {
|
||||||
|
const text = String(entry || '').trim();
|
||||||
|
const structuredMatch = text.match(/^(\d+)\s*(?:[:|/-]\s*(.+))?$/);
|
||||||
|
if (structuredMatch) {
|
||||||
|
countryId = Math.floor(Number(structuredMatch[1]) || 0);
|
||||||
|
countryLabel = String(structuredMatch[2] || '').trim();
|
||||||
|
} else {
|
||||||
|
countryId = Math.floor(Number(text) || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isFinite(countryId) || countryId <= 0 || seenIds.has(countryId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenIds.add(countryId);
|
||||||
|
normalized.push({
|
||||||
|
id: countryId,
|
||||||
|
label: countryLabel || `Country #${countryId}`,
|
||||||
|
});
|
||||||
|
if (normalized.length >= 20) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveLegacyAutoStepDelaySeconds(input = {}) {
|
function resolveLegacyAutoStepDelaySeconds(input = {}) {
|
||||||
const hasLegacyMin = input.autoStepRandomDelayMinSeconds !== undefined;
|
const hasLegacyMin = input.autoStepRandomDelayMinSeconds !== undefined;
|
||||||
const hasLegacyMax = input.autoStepRandomDelayMaxSeconds !== undefined;
|
const hasLegacyMax = input.autoStepRandomDelayMaxSeconds !== undefined;
|
||||||
@@ -1264,6 +1434,16 @@ function normalizePersistentSettingValue(key, value) {
|
|||||||
return normalizeAutoStepDelaySeconds(value, PERSISTED_SETTING_DEFAULTS.autoStepDelaySeconds);
|
return normalizeAutoStepDelaySeconds(value, PERSISTED_SETTING_DEFAULTS.autoStepDelaySeconds);
|
||||||
case 'verificationResendCount':
|
case 'verificationResendCount':
|
||||||
return normalizeVerificationResendCount(value, DEFAULT_VERIFICATION_RESEND_COUNT);
|
return normalizeVerificationResendCount(value, DEFAULT_VERIFICATION_RESEND_COUNT);
|
||||||
|
case 'phoneVerificationReplacementLimit':
|
||||||
|
return normalizePhoneVerificationReplacementLimit(value, DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT);
|
||||||
|
case 'phoneCodeWaitSeconds':
|
||||||
|
return normalizePhoneCodeWaitSeconds(value, DEFAULT_PHONE_CODE_WAIT_SECONDS);
|
||||||
|
case 'phoneCodeTimeoutWindows':
|
||||||
|
return normalizePhoneCodeTimeoutWindows(value, DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS);
|
||||||
|
case 'phoneCodePollIntervalSeconds':
|
||||||
|
return normalizePhoneCodePollIntervalSeconds(value, DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS);
|
||||||
|
case 'phoneCodePollMaxRounds':
|
||||||
|
return normalizePhoneCodePollMaxRounds(value, DEFAULT_PHONE_CODE_POLL_ROUNDS);
|
||||||
case 'mailProvider':
|
case 'mailProvider':
|
||||||
return normalizeMailProvider(value);
|
return normalizeMailProvider(value);
|
||||||
case 'mail2925Mode':
|
case 'mail2925Mode':
|
||||||
@@ -1327,12 +1507,18 @@ function normalizePersistentSettingValue(key, value) {
|
|||||||
return normalizePayPalAccounts(value);
|
return normalizePayPalAccounts(value);
|
||||||
case 'heroSmsApiKey':
|
case 'heroSmsApiKey':
|
||||||
return String(value || '');
|
return String(value || '');
|
||||||
|
case 'heroSmsReuseEnabled':
|
||||||
|
return Boolean(value);
|
||||||
|
case 'heroSmsAcquirePriority':
|
||||||
|
return normalizeHeroSmsAcquirePriority(value);
|
||||||
case 'heroSmsMaxPrice':
|
case 'heroSmsMaxPrice':
|
||||||
return String(value || '').trim();
|
return normalizeHeroSmsMaxPrice(value);
|
||||||
case 'heroSmsCountryId':
|
case 'heroSmsCountryId':
|
||||||
return Math.max(1, Math.floor(Number(value) || HERO_SMS_COUNTRY_ID));
|
return Math.max(1, Math.floor(Number(value) || HERO_SMS_COUNTRY_ID));
|
||||||
case 'heroSmsCountryLabel':
|
case 'heroSmsCountryLabel':
|
||||||
return String(value || HERO_SMS_COUNTRY_LABEL).trim() || HERO_SMS_COUNTRY_LABEL;
|
return String(value || HERO_SMS_COUNTRY_LABEL).trim() || HERO_SMS_COUNTRY_LABEL;
|
||||||
|
case 'heroSmsCountryFallback':
|
||||||
|
return normalizeHeroSmsCountryFallback(value);
|
||||||
default:
|
default:
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -1878,6 +2064,7 @@ async function resetState() {
|
|||||||
'accounts',
|
'accounts',
|
||||||
'tabRegistry',
|
'tabRegistry',
|
||||||
'sourceLastUrls',
|
'sourceLastUrls',
|
||||||
|
'reusablePhoneActivation',
|
||||||
'luckmailApiKey',
|
'luckmailApiKey',
|
||||||
'luckmailBaseUrl',
|
'luckmailBaseUrl',
|
||||||
'luckmailEmailType',
|
'luckmailEmailType',
|
||||||
@@ -1892,6 +2079,25 @@ async function resetState() {
|
|||||||
getPersistedAliasState(),
|
getPersistedAliasState(),
|
||||||
]);
|
]);
|
||||||
const contributionModeState = buildContributionModeState(Boolean(prev.contributionMode), persistedSettings, prev);
|
const contributionModeState = buildContributionModeState(Boolean(prev.contributionMode), persistedSettings, prev);
|
||||||
|
const reusablePhoneActivation = (
|
||||||
|
prev.reusablePhoneActivation
|
||||||
|
&& typeof prev.reusablePhoneActivation === 'object'
|
||||||
|
&& !Array.isArray(prev.reusablePhoneActivation)
|
||||||
|
&& String(
|
||||||
|
prev.reusablePhoneActivation.activationId
|
||||||
|
?? prev.reusablePhoneActivation.id
|
||||||
|
?? prev.reusablePhoneActivation.activation
|
||||||
|
?? ''
|
||||||
|
).trim()
|
||||||
|
&& String(
|
||||||
|
prev.reusablePhoneActivation.phoneNumber
|
||||||
|
?? prev.reusablePhoneActivation.number
|
||||||
|
?? prev.reusablePhoneActivation.phone
|
||||||
|
?? ''
|
||||||
|
).trim()
|
||||||
|
)
|
||||||
|
? prev.reusablePhoneActivation
|
||||||
|
: null;
|
||||||
await chrome.storage.session.clear();
|
await chrome.storage.session.clear();
|
||||||
await chrome.storage.session.set({
|
await chrome.storage.session.set({
|
||||||
...DEFAULT_STATE,
|
...DEFAULT_STATE,
|
||||||
@@ -1912,6 +2118,8 @@ async function resetState() {
|
|||||||
luckmailPreserveTagName: String(prev.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
|
luckmailPreserveTagName: String(prev.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
|
||||||
currentLuckmailPurchase: null,
|
currentLuckmailPurchase: null,
|
||||||
currentLuckmailMailCursor: null,
|
currentLuckmailMailCursor: null,
|
||||||
|
// Keep reusable phone activation across round resets so the same number can be reactivated up to maxUses.
|
||||||
|
reusablePhoneActivation,
|
||||||
preferredIcloudHost: prev.preferredIcloudHost || '',
|
preferredIcloudHost: prev.preferredIcloudHost || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -6042,6 +6250,7 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
lastSignupCode: null,
|
lastSignupCode: null,
|
||||||
lastLoginCode: null,
|
lastLoginCode: null,
|
||||||
localhostUrl: null,
|
localhostUrl: null,
|
||||||
|
currentPhoneVerificationCode: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (step === 2) {
|
if (step === 2) {
|
||||||
@@ -6057,6 +6266,7 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
lastSignupCode: null,
|
lastSignupCode: null,
|
||||||
lastLoginCode: null,
|
lastLoginCode: null,
|
||||||
localhostUrl: null,
|
localhostUrl: null,
|
||||||
|
currentPhoneVerificationCode: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (step === 3 || step === 4) {
|
if (step === 3 || step === 4) {
|
||||||
@@ -6071,6 +6281,7 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
lastSignupCode: null,
|
lastSignupCode: null,
|
||||||
lastLoginCode: null,
|
lastLoginCode: null,
|
||||||
localhostUrl: null,
|
localhostUrl: null,
|
||||||
|
currentPhoneVerificationCode: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (step === 5 || step === 6 || step === 7 || step === 8) {
|
if (step === 5 || step === 6 || step === 7 || step === 8) {
|
||||||
@@ -6092,6 +6303,7 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
oauthFlowDeadlineSourceUrl: null,
|
oauthFlowDeadlineSourceUrl: null,
|
||||||
pendingPhoneActivationConfirmation: null,
|
pendingPhoneActivationConfirmation: null,
|
||||||
localhostUrl: null,
|
localhostUrl: null,
|
||||||
|
currentPhoneVerificationCode: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (step === 9) {
|
if (step === 9) {
|
||||||
@@ -6099,6 +6311,7 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
pendingPhoneActivationConfirmation: null,
|
pendingPhoneActivationConfirmation: null,
|
||||||
plusReturnUrl: '',
|
plusReturnUrl: '',
|
||||||
localhostUrl: null,
|
localhostUrl: null,
|
||||||
|
currentPhoneVerificationCode: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (stepKey === 'oauth-login' || stepKey === 'fetch-login-code') {
|
if (stepKey === 'oauth-login' || stepKey === 'fetch-login-code') {
|
||||||
@@ -6109,6 +6322,7 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
oauthFlowDeadlineSourceUrl: null,
|
oauthFlowDeadlineSourceUrl: null,
|
||||||
pendingPhoneActivationConfirmation: null,
|
pendingPhoneActivationConfirmation: null,
|
||||||
localhostUrl: null,
|
localhostUrl: null,
|
||||||
|
currentPhoneVerificationCode: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (stepKey === 'confirm-oauth') {
|
if (stepKey === 'confirm-oauth') {
|
||||||
@@ -8598,6 +8812,11 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create
|
|||||||
const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.createPhoneVerificationHelpers({
|
const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.createPhoneVerificationHelpers({
|
||||||
addLog,
|
addLog,
|
||||||
DEFAULT_HERO_SMS_BASE_URL,
|
DEFAULT_HERO_SMS_BASE_URL,
|
||||||
|
DEFAULT_HERO_SMS_REUSE_ENABLED,
|
||||||
|
DEFAULT_PHONE_CODE_WAIT_SECONDS,
|
||||||
|
DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS,
|
||||||
|
DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS,
|
||||||
|
DEFAULT_PHONE_CODE_POLL_ROUNDS,
|
||||||
ensureStep8SignupPageReady,
|
ensureStep8SignupPageReady,
|
||||||
getOAuthFlowStepTimeoutMs,
|
getOAuthFlowStepTimeoutMs,
|
||||||
getState,
|
getState,
|
||||||
@@ -9448,6 +9667,10 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
|||||||
const hasTransientNetworkSignal = /connect:\s*connection refused|failed to fetch|i\/o timeout|context deadline exceeded|eof|connection reset by peer/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);
|
||||||
return mentionsTokenExchange && hasTransientNetworkSignal;
|
return mentionsTokenExchange && hasTransientNetworkSignal;
|
||||||
};
|
};
|
||||||
|
const isPhoneVerificationLocalFailure = (errorMessage = '') => {
|
||||||
|
const normalizedMessage = String(errorMessage || '');
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
const normalizedStep = Number(step);
|
const normalizedStep = Number(step);
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
@@ -9478,6 +9701,17 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPhoneVerificationLocalFailure(errorMessage)) {
|
||||||
|
return {
|
||||||
|
shouldRestart: false,
|
||||||
|
blockedByAddPhone: true,
|
||||||
|
forcedByPhoneVerificationTimeout: false,
|
||||||
|
restartStep: authChainStartStep,
|
||||||
|
errorMessage,
|
||||||
|
authState: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldForceRestartFromStep7) {
|
if (shouldForceRestartFromStep7) {
|
||||||
return {
|
return {
|
||||||
shouldRestart: true,
|
shouldRestart: true,
|
||||||
@@ -10080,6 +10314,116 @@ function getStep8EffectLabel(effect) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isStep9OAuthLocalhostTimeoutError(error, visibleStep = 9) {
|
||||||
|
const message = getErrorMessage(error);
|
||||||
|
if (!message) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!/从拿到 OAuth 登录地址开始/.test(message)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!/localhost 回调|OAuth localhost 回调/i.test(message)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const normalizedStep = Number(visibleStep);
|
||||||
|
if (Number.isFinite(normalizedStep) && normalizedStep > 0) {
|
||||||
|
const stepPrefix = new RegExp(`步骤\\s*${normalizedStep}\\s*:`);
|
||||||
|
if (!stepPrefix.test(message)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recoverOAuthLocalhostTimeout(details = {}) {
|
||||||
|
const {
|
||||||
|
error,
|
||||||
|
state,
|
||||||
|
visibleStep = 9,
|
||||||
|
} = details;
|
||||||
|
|
||||||
|
if (!isStep9OAuthLocalhostTimeoutError(error, visibleStep)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authLoginStep = typeof getAuthChainStartStepId === 'function'
|
||||||
|
? getAuthChainStartStepId(state || {})
|
||||||
|
: FINAL_OAUTH_CHAIN_START_STEP;
|
||||||
|
const loginCodeStep = Number(visibleStep) >= 12 ? 11 : 8;
|
||||||
|
|
||||||
|
await addLog(
|
||||||
|
`步骤 ${visibleStep}:检测到 OAuth localhost 回调等待窗口已过期,正在复核认证页并回到步骤 ${authLoginStep} 重拉授权链路。`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
|
||||||
|
let authState = null;
|
||||||
|
try {
|
||||||
|
authState = await getLoginAuthStateFromContent({
|
||||||
|
timeoutMs: 10000,
|
||||||
|
responseTimeoutMs: 10000,
|
||||||
|
logMessage: `步骤 ${visibleStep}:正在复核认证页状态,确认是否可自动恢复 localhost 回调链路...`,
|
||||||
|
});
|
||||||
|
} catch (inspectError) {
|
||||||
|
await addLog(
|
||||||
|
`步骤 ${visibleStep}:复核认证页状态失败(${getErrorMessage(inspectError)}),将先尝试按步骤 ${loginCodeStep} 收尾恢复。`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAddPhoneAuthState(authState)) {
|
||||||
|
const stateLabel = getLoginAuthStateLabel(authState.state);
|
||||||
|
await addLog(
|
||||||
|
`步骤 ${visibleStep}:当前认证页为 ${stateLabel},将直接回到步骤 ${authLoginStep} 重新拉起授权链路,避免步骤 8/9 恢复冲突。`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
} else if (authState && authState.state && !['verification_page', 'oauth_consent_page'].includes(authState.state)) {
|
||||||
|
const stateLabel = getLoginAuthStateLabel(authState.state);
|
||||||
|
await addLog(
|
||||||
|
`步骤 ${visibleStep}:当前认证页为 ${stateLabel},不满足快速恢复条件,将回到步骤 ${authLoginStep} 重开授权链路。`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestState = await getState();
|
||||||
|
if (!step7Executor?.executeStep7 || !step8Executor?.executeStep8) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog(
|
||||||
|
`步骤 ${visibleStep}:正在自动重开步骤 ${authLoginStep} -> ${loginCodeStep},恢复到可继续捕获 localhost 回调的状态。`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
await step7Executor.executeStep7({
|
||||||
|
...latestState,
|
||||||
|
visibleStep: authLoginStep,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stateAfterStep7 = await getState();
|
||||||
|
await step8Executor.executeStep8({
|
||||||
|
...stateAfterStep7,
|
||||||
|
visibleStep: loginCodeStep,
|
||||||
|
});
|
||||||
|
|
||||||
|
const recoveredState = await getState();
|
||||||
|
const oauthUrl = String(recoveredState?.oauthUrl || state?.oauthUrl || '').trim();
|
||||||
|
if (oauthUrl && typeof startOAuthFlowTimeoutWindow === 'function') {
|
||||||
|
await startOAuthFlowTimeoutWindow({
|
||||||
|
step: Number(visibleStep) || 9,
|
||||||
|
oauthUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await setState({
|
||||||
|
localhostUrl: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await addLog(
|
||||||
|
`步骤 ${visibleStep}:已恢复到步骤 ${authLoginStep} -> ${loginCodeStep} 收尾状态,并刷新 OAuth localhost 回调等待窗口,准备重试当前步骤。`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
return await getState();
|
||||||
|
}
|
||||||
|
|
||||||
const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
|
const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
|
||||||
addLog,
|
addLog,
|
||||||
chrome,
|
chrome,
|
||||||
@@ -10097,6 +10441,7 @@ const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
|
|||||||
getStep8TabUpdatedListener,
|
getStep8TabUpdatedListener,
|
||||||
isTabAlive,
|
isTabAlive,
|
||||||
prepareStep8DebuggerClick,
|
prepareStep8DebuggerClick,
|
||||||
|
recoverOAuthLocalhostTimeout,
|
||||||
reloadStep8ConsentPage,
|
reloadStep8ConsentPage,
|
||||||
reuseOrCreateTab,
|
reuseOrCreateTab,
|
||||||
setStep8PendingReject,
|
setStep8PendingReject,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
|||||||
getTabId,
|
getTabId,
|
||||||
isTabAlive,
|
isTabAlive,
|
||||||
prepareStep8DebuggerClick,
|
prepareStep8DebuggerClick,
|
||||||
|
recoverOAuthLocalhostTimeout,
|
||||||
reloadStep8ConsentPage,
|
reloadStep8ConsentPage,
|
||||||
reuseOrCreateTab,
|
reuseOrCreateTab,
|
||||||
sleepWithStop,
|
sleepWithStop,
|
||||||
@@ -44,19 +45,43 @@
|
|||||||
|
|
||||||
async function executeStep9(state) {
|
async function executeStep9(state) {
|
||||||
const visibleStep = getVisibleStep(state, 9);
|
const visibleStep = getVisibleStep(state, 9);
|
||||||
if (!state.oauthUrl) {
|
let activeState = state;
|
||||||
|
|
||||||
|
if (!activeState.oauthUrl) {
|
||||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||||
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${authLoginStep}。`);
|
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${authLoginStep}。`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await addLog(`步骤 ${visibleStep}:正在监听 localhost 回调地址...`);
|
await addLog(`步骤 ${visibleStep}:正在监听 localhost 回调地址...`);
|
||||||
|
|
||||||
const callbackTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
let callbackTimeoutMs = 240000;
|
||||||
? await getOAuthFlowStepTimeoutMs(240000, {
|
let timeoutRecoveryAttempted = false;
|
||||||
step: visibleStep,
|
while (true) {
|
||||||
actionLabel: 'OAuth localhost 回调',
|
try {
|
||||||
})
|
callbackTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||||
: 240000;
|
? await getOAuthFlowStepTimeoutMs(240000, {
|
||||||
|
step: visibleStep,
|
||||||
|
actionLabel: 'OAuth localhost 回调',
|
||||||
|
oauthUrl: activeState?.oauthUrl || '',
|
||||||
|
})
|
||||||
|
: 240000;
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
if (timeoutRecoveryAttempted || typeof recoverOAuthLocalhostTimeout !== 'function') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const recoveredState = await recoverOAuthLocalhostTimeout({
|
||||||
|
error,
|
||||||
|
state: activeState,
|
||||||
|
visibleStep,
|
||||||
|
});
|
||||||
|
if (!recoveredState) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
activeState = recoveredState;
|
||||||
|
timeoutRecoveryAttempted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
@@ -124,7 +149,7 @@
|
|||||||
await chrome.tabs.update(signupTabId, { active: true });
|
await chrome.tabs.update(signupTabId, { active: true });
|
||||||
await addLog(`步骤 ${visibleStep}:已切回认证页,正在准备调试器点击...`);
|
await addLog(`步骤 ${visibleStep}:已切回认证页,正在准备调试器点击...`);
|
||||||
} else {
|
} else {
|
||||||
signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
|
signupTabId = await reuseOrCreateTab('signup-page', activeState.oauthUrl);
|
||||||
await addLog(`步骤 ${visibleStep}:已重新打开认证页,正在准备调试器点击...`);
|
await addLog(`步骤 ${visibleStep}:已重新打开认证页,正在准备调试器点击...`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -222,6 +222,28 @@
|
|||||||
return Math.min(20, Math.max(0, Math.floor(numeric)));
|
return Math.min(20, Math.max(0, Math.floor(numeric)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getVerificationRequestedAtStateKey(step) {
|
||||||
|
if (Number(step) === 4) return 'signupVerificationRequestedAt';
|
||||||
|
if (Number(step) === 8) return 'loginVerificationRequestedAt';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveInitialVerificationRequestedAt(step, state = {}, fallback = 0) {
|
||||||
|
const stateKey = getVerificationRequestedAtStateKey(step);
|
||||||
|
const candidateValues = [
|
||||||
|
fallback,
|
||||||
|
stateKey ? state?.[stateKey] : 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const value of candidateValues) {
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (Number.isFinite(numeric) && numeric > 0) {
|
||||||
|
return Math.floor(numeric);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
function getLegacyVerificationResendCountDefault(step, options = {}) {
|
function getLegacyVerificationResendCountDefault(step, options = {}) {
|
||||||
const requestFreshCodeFirst = Boolean(options.requestFreshCodeFirst);
|
const requestFreshCodeFirst = Boolean(options.requestFreshCodeFirst);
|
||||||
const legacyMaxRounds = Math.max(1, Math.floor(Number(VERIFICATION_POLL_MAX_ROUNDS) || 1));
|
const legacyMaxRounds = Math.max(1, Math.floor(Number(VERIFICATION_POLL_MAX_ROUNDS) || 1));
|
||||||
@@ -919,10 +941,14 @@
|
|||||||
: getConfiguredVerificationResendCount(step, state, { requestFreshCodeFirst });
|
: getConfiguredVerificationResendCount(step, state, { requestFreshCodeFirst });
|
||||||
const maxSubmitAttempts = mail.provider === LUCKMAIL_PROVIDER ? 3 : 15;
|
const maxSubmitAttempts = mail.provider === LUCKMAIL_PROVIDER ? 3 : 15;
|
||||||
const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
|
const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
|
||||||
let lastResendAt = Number(options.lastResendAt) || 0;
|
|
||||||
const externalOnResendRequestedAt = typeof options.onResendRequestedAt === 'function'
|
const externalOnResendRequestedAt = typeof options.onResendRequestedAt === 'function'
|
||||||
? options.onResendRequestedAt
|
? options.onResendRequestedAt
|
||||||
: null;
|
: null;
|
||||||
|
let lastResendAt = resolveInitialVerificationRequestedAt(
|
||||||
|
step,
|
||||||
|
state,
|
||||||
|
Number(options.lastResendAt) || 0
|
||||||
|
);
|
||||||
|
|
||||||
const updateFilterAfterTimestampForVerificationStep = async (requestedAt) => {
|
const updateFilterAfterTimestampForVerificationStep = async (requestedAt) => {
|
||||||
if (externalOnResendRequestedAt) {
|
if (externalOnResendRequestedAt) {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
throwIfStopped,
|
throwIfStopped,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} = deps;
|
} = deps;
|
||||||
|
const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::';
|
||||||
|
const PHONE_RESEND_THROTTLED_PATTERN = /tried\s+to\s+resend\s+too\s+many\s+times|please\s+try\s+again\s+later|too\s+many\s+resend|resend\s+too\s+many|发送.*过于频繁|稍后再试|重试次数过多/i;
|
||||||
|
|
||||||
function dispatchInputEvents(element) {
|
function dispatchInputEvents(element) {
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
@@ -312,6 +314,90 @@
|
|||||||
return matches?.[0] ? matches[0].replace(/\s+/g, ' ').trim() : '';
|
return matches?.[0] ? matches[0].replace(/\s+/g, ' ').trim() : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAddPhoneErrorText() {
|
||||||
|
const form = getAddPhoneForm();
|
||||||
|
if (!form) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = [];
|
||||||
|
const selectors = [
|
||||||
|
'.react-aria-FieldError',
|
||||||
|
'[slot="errorMessage"]',
|
||||||
|
'[id$="-error"]',
|
||||||
|
'[data-invalid="true"] + *',
|
||||||
|
'[aria-invalid="true"] + *',
|
||||||
|
'[class*="error"]',
|
||||||
|
];
|
||||||
|
for (const selector of selectors) {
|
||||||
|
form.querySelectorAll(selector).forEach((el) => {
|
||||||
|
const text = String(el?.textContent || '').replace(/\s+/g, ' ').trim();
|
||||||
|
if (text) {
|
||||||
|
messages.push(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidInput = form.querySelector('input[aria-invalid="true"], input[data-invalid="true"]');
|
||||||
|
if (invalidInput) {
|
||||||
|
const wrapper = invalidInput.closest('form, [data-rac], div');
|
||||||
|
const text = String(wrapper?.textContent || '').replace(/\s+/g, ' ').trim();
|
||||||
|
if (text) {
|
||||||
|
messages.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferred = messages.find((text) => (
|
||||||
|
/already|used|linked|eligible|invalid|phone|号码|手机号|错误|失败|try\s+again/i.test(text)
|
||||||
|
));
|
||||||
|
return preferred || messages[0] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPhoneVerificationInlineMessages() {
|
||||||
|
const form = getPhoneVerificationForm();
|
||||||
|
if (!form) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const messages = [];
|
||||||
|
const selectors = [
|
||||||
|
'.react-aria-FieldError',
|
||||||
|
'[slot="errorMessage"]',
|
||||||
|
'[id$="-error"]',
|
||||||
|
'[data-invalid="true"] + *',
|
||||||
|
'[aria-invalid="true"] + *',
|
||||||
|
'[class*="error"]',
|
||||||
|
];
|
||||||
|
for (const selector of selectors) {
|
||||||
|
form.querySelectorAll(selector).forEach((element) => {
|
||||||
|
const text = String(element?.textContent || '').replace(/\s+/g, ' ').trim();
|
||||||
|
if (text) {
|
||||||
|
messages.push(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const verificationError = String(getVerificationErrorText?.() || '').trim();
|
||||||
|
if (verificationError) {
|
||||||
|
messages.push(verificationError);
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPhoneResendThrottleText() {
|
||||||
|
const inlineMatch = getPhoneVerificationInlineMessages()
|
||||||
|
.find((text) => PHONE_RESEND_THROTTLED_PATTERN.test(text));
|
||||||
|
if (inlineMatch) {
|
||||||
|
return inlineMatch;
|
||||||
|
}
|
||||||
|
const pageSnapshot = String(getPageTextSnapshot?.() || '').replace(/\s+/g, ' ').trim();
|
||||||
|
if (pageSnapshot && PHONE_RESEND_THROTTLED_PATTERN.test(pageSnapshot)) {
|
||||||
|
const concise = pageSnapshot.match(
|
||||||
|
/tried\s+to\s+resend\s+too\s+many\s+times[^.。!?]*[.。!?]?|please\s+try\s+again\s+later[^.。!?]*[.。!?]?|发送.*过于频繁[^。!?]*[。!?]?|稍后再试[^。!?]*[。!?]?/i
|
||||||
|
);
|
||||||
|
return String(concise?.[0] || pageSnapshot).trim();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForAddPhoneReady(timeout = 20000) {
|
async function waitForAddPhoneReady(timeout = 20000) {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
while (Date.now() - start < timeout) {
|
while (Date.now() - start < timeout) {
|
||||||
@@ -335,8 +421,28 @@
|
|||||||
url: location.href,
|
url: location.href,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (isAddPhonePageReady()) {
|
||||||
|
const errorText = getAddPhoneErrorText();
|
||||||
|
if (errorText) {
|
||||||
|
return {
|
||||||
|
addPhoneRejected: true,
|
||||||
|
errorText,
|
||||||
|
url: location.href,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
await sleep(150);
|
await sleep(150);
|
||||||
}
|
}
|
||||||
|
if (isAddPhonePageReady()) {
|
||||||
|
const errorText = getAddPhoneErrorText();
|
||||||
|
if (errorText) {
|
||||||
|
return {
|
||||||
|
addPhoneRejected: true,
|
||||||
|
errorText,
|
||||||
|
url: location.href,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
throw new Error('Timed out waiting for phone verification page.');
|
throw new Error('Timed out waiting for phone verification page.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,11 +572,19 @@
|
|||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
while (Date.now() - start < timeout) {
|
while (Date.now() - start < timeout) {
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
|
const throttledText = getPhoneResendThrottleText();
|
||||||
|
if (throttledText) {
|
||||||
|
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${throttledText}`);
|
||||||
|
}
|
||||||
const resendButton = getPhoneVerificationResendButton({ allowDisabled: true });
|
const resendButton = getPhoneVerificationResendButton({ allowDisabled: true });
|
||||||
if (resendButton && isActionEnabled(resendButton)) {
|
if (resendButton && isActionEnabled(resendButton)) {
|
||||||
await humanPause(250, 700);
|
await humanPause(250, 700);
|
||||||
simulateClick(resendButton);
|
simulateClick(resendButton);
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
|
const afterClickThrottleText = getPhoneResendThrottleText();
|
||||||
|
if (afterClickThrottleText) {
|
||||||
|
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${afterClickThrottleText}`);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
resent: true,
|
resent: true,
|
||||||
url: location.href,
|
url: location.href,
|
||||||
@@ -479,6 +593,11 @@
|
|||||||
await sleep(250);
|
await sleep(250);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timeoutThrottleText = getPhoneResendThrottleText();
|
||||||
|
if (timeoutThrottleText) {
|
||||||
|
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${timeoutThrottleText}`);
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error('Timed out waiting for the phone verification resend button.');
|
throw new Error('Timed out waiting for the phone verification resend button.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+74
-8
@@ -1,6 +1,6 @@
|
|||||||
# Codex 注册扩展相关项目、更新、邮箱、PayPal 与 Clash Verge 配置教程
|
# Codex 注册扩展相关项目、更新、邮箱、PayPal 与 Clash Verge 配置教程
|
||||||
|
|
||||||
本教程用于说明相关项目地址、扩展更新方式、`Cloudflare Temp Email`、`iCloud 隐私邮箱` 与 `QQ 邮箱` 的使用方法、`PayPal` 注册绑卡流程、扩展内置动态 IP 代理、节点检测与纯净度检查,以及 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询` 配置方法。
|
本教程用于说明相关项目地址、扩展更新方式、`Cloudflare Temp Email`、`iCloud 隐私邮箱` 与 `QQ 邮箱` 的使用方法、`HeroSMS` 手机接码扩展能力、`PayPal` 注册绑卡流程、扩展内置动态 IP 代理、节点检测与纯净度检查,以及 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询` 配置方法。
|
||||||
|
|
||||||
## 适用场景
|
## 适用场景
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
- 需要使用 `iCloud+` 的 `隐藏邮件地址` 作为隐私邮箱
|
- 需要使用 `iCloud+` 的 `隐藏邮件地址` 作为隐私邮箱
|
||||||
- 需要临时切换 `QQ 邮箱` 地址继续使用
|
- 需要临时切换 `QQ 邮箱` 地址继续使用
|
||||||
- 需要使用 `网易邮箱`、`网易邮箱大师` 注册多个邮箱或替身邮箱
|
- 需要使用 `网易邮箱`、`网易邮箱大师` 注册多个邮箱或替身邮箱
|
||||||
|
- 需要在 `Step 9` 的手机号验证阶段使用 `HeroSMS` 接码(含多国家回退与价格策略)
|
||||||
- 需要注册并使用 `PayPal` 个人账户
|
- 需要注册并使用 `PayPal` 个人账户
|
||||||
- 需要在扩展内使用 `711Proxy` 动态 IP,并在自动流程中按轮次切换出口
|
- 需要在扩展内使用 `711Proxy` 动态 IP,并在自动流程中按轮次切换出口
|
||||||
- 需要检查当前节点的出口 IP、纯净度、泄露情况和访问速度
|
- 需要检查当前节点的出口 IP、纯净度、泄露情况和访问速度
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
- 一个可正常登录的 `QQ 邮箱`
|
- 一个可正常登录的 `QQ 邮箱`
|
||||||
- 手机端已安装 `网易邮箱` 或 `网易邮箱大师`
|
- 手机端已安装 `网易邮箱` 或 `网易邮箱大师`
|
||||||
- 一个可正常接收短信的手机号
|
- 一个可正常接收短信的手机号
|
||||||
|
- 一个可用的 `HeroSMS API Key`(用于手机号接码)
|
||||||
- 一张可在线支付的借记卡或信用卡
|
- 一张可在线支付的借记卡或信用卡
|
||||||
- 如需使用扩展内置动态 IP,需准备 `711Proxy` 的 Host、Port、代理账号和代理密码
|
- 如需使用扩展内置动态 IP,需准备 `711Proxy` 的 Host、Port、代理账号和代理密码
|
||||||
- 如需部署 `cpa`,部署环境必须可以访问 `OpenAI`
|
- 如需部署 `cpa`,部署环境必须可以访问 `OpenAI`
|
||||||
@@ -221,7 +223,71 @@
|
|||||||
只要网络环境不要太差,并且不要连续高频注册,一般不容易马上触发额外手机号验证。
|
只要网络环境不要太差,并且不要连续高频注册,一般不容易马上触发额外手机号验证。
|
||||||
建议注册一个后换节奏再继续,不要一口气连续创建。
|
建议注册一个后换节奏再继续,不要一口气连续创建。
|
||||||
|
|
||||||
### 第七部分:`PayPal` 注册与绑卡使用教程
|
### 第七部分:`HeroSMS` 手机接码扩展使用教程(新增)
|
||||||
|
|
||||||
|
本部分用于说明扩展中 `Step 9` 手机号验证链路的最新接码能力与推荐用法。
|
||||||
|
目标是减少“重复重发导致封禁”与“同国家持续拿不到码”的失败率,并让失败可以在 `Step 9` 内部自愈。
|
||||||
|
|
||||||
|
#### 一、入口与启用
|
||||||
|
|
||||||
|
1. 打开扩展侧栏,找到 `接码设置`。
|
||||||
|
2. 打开右侧接码开关(`IP 代理`下方同风格开关)。
|
||||||
|
3. 展开设置后,先确认 `接码平台` 显示为 `HeroSMS / OpenAI`。
|
||||||
|
|
||||||
|
#### 二、国家与优先级(新增能力)
|
||||||
|
|
||||||
|
1. 在 `接码国家` 中多选国家(最多 `3` 个),按你的业务顺序排列。
|
||||||
|
2. 在 `国家优先级` 中选择策略:
|
||||||
|
- `国家优先`:严格按你选择的顺序先后尝试。
|
||||||
|
- `价格优先`:在候选国家里先选可用价格更低的国家,再尝试拿号。
|
||||||
|
3. 当同一国家连续失败达到阈值后,流程会自动优先尝试下一个候选国家。
|
||||||
|
|
||||||
|
#### 三、价格控制与价格预览(新增能力)
|
||||||
|
|
||||||
|
1. 填写 `接码 API`(支持右侧眼睛按钮显示/隐藏,避免粘贴错误)。
|
||||||
|
2. 在 `价格` 一行点击 `查询价格`,查看候选国家当前档位。
|
||||||
|
3. 设置 `价格上限`(`maxPrice`)限制成本,避免异常高价拿号。
|
||||||
|
4. 如果日志提示 `no numbers within maxPrice`,说明上限太低,按当前价格结果适度上调即可。
|
||||||
|
|
||||||
|
#### 四、号码复用与参数建议
|
||||||
|
|
||||||
|
1. `号码复用` 开关:
|
||||||
|
- 开启:同号码在可复用范围内优先复用,减少频繁拿号。
|
||||||
|
- 关闭:每次优先新号,适合风控更严格的场景。
|
||||||
|
2. 建议参数(稳妥起步):
|
||||||
|
- `验证码重发`:`0` 或 `1`
|
||||||
|
- `换号上限`:`3`
|
||||||
|
- `验证码限时`:`60` 秒
|
||||||
|
- `超时次数`:`2`
|
||||||
|
- `轮询间隔`:`5` 秒
|
||||||
|
- `轮询次数`:`3-4` 次(不要设置过高)
|
||||||
|
|
||||||
|
#### 五、失败自愈策略(新增能力)
|
||||||
|
|
||||||
|
当前版本已内置以下保护逻辑:
|
||||||
|
|
||||||
|
1. 当页面出现 `Tried to resend too many times. Please try again later.` 时,流程会停止继续狂点 `Resend`,改为换号/换国家路径。
|
||||||
|
2. 单个号码不会无限重发,避免被目标站点判定重发过频。
|
||||||
|
3. 收不到短信时优先在 `Step 9` 内局部恢复,不直接回卷到 `Step 1`。
|
||||||
|
|
||||||
|
#### 六、运行状态怎么看
|
||||||
|
|
||||||
|
在 `运行状态` 中重点看:
|
||||||
|
|
||||||
|
1. `当前分配`:当前拿到的手机号与国家。
|
||||||
|
2. `验证码`:是否已收到可提交验证码。
|
||||||
|
3. `价格预览`:当前国家候选的可用价格信息(查询后刷新)。
|
||||||
|
|
||||||
|
#### 七、常见问题与处理
|
||||||
|
|
||||||
|
1. `no numbers available across ...`
|
||||||
|
表示候选国家当前无号或价格上限过低。先提高价格上限,再调整国家顺序。
|
||||||
|
2. `NO_NUMBERS` 频繁出现
|
||||||
|
增加候选国家数量(最多 3 个),并优先启用 `价格优先`。
|
||||||
|
3. 页面提示重发过多
|
||||||
|
不要手工持续点重发,让流程自动换号/换国家即可。
|
||||||
|
|
||||||
|
### 第八部分:`PayPal` 注册与绑卡使用教程
|
||||||
|
|
||||||
1. 打开注册页面
|
1. 打开注册页面
|
||||||
打开 [https://www.paypal.com/signin](https://www.paypal.com/signin)。
|
打开 [https://www.paypal.com/signin](https://www.paypal.com/signin)。
|
||||||
@@ -273,14 +339,14 @@
|
|||||||
常见情况是上传身份证件。
|
常见情况是上传身份证件。
|
||||||
`PayPal` 官方帮助中心说明,通常会在 `2 个工作日` 内审核,但某些情况可能更久。
|
`PayPal` 官方帮助中心说明,通常会在 `2 个工作日` 内审核,但某些情况可能更久。
|
||||||
|
|
||||||
### 第八部分:0元试用 ChatGPT Plus 教程
|
### 第九部分:0元试用 ChatGPT Plus 教程
|
||||||
|
|
||||||
本部分说明如何在已登录 ChatGPT 的状态下,通过脚本快速生成 Plus 支付链接,然后选择 PayPal 支付完成0元试用订阅。
|
本部分说明如何在已登录 ChatGPT 的状态下,通过脚本快速生成 Plus 支付链接,然后选择 PayPal 支付完成0元试用订阅。
|
||||||
|
|
||||||
#### 准备工作
|
#### 准备工作
|
||||||
|
|
||||||
1. 已有一个登录状态的 ChatGPT 账户。
|
1. 已有一个登录状态的 ChatGPT 账户。
|
||||||
2. 一个可用的 PayPal 账户(参考第七部分进行注册和绑卡)。
|
2. 一个可用的 PayPal 账户(参考第八部分进行注册和绑卡)。
|
||||||
3. 能够接收生成的账单地址的真实地址或虚拟地址。
|
3. 能够接收生成的账单地址的真实地址或虚拟地址。
|
||||||
4. Chrome 浏览器(推荐使用地址补全功能)。
|
4. Chrome 浏览器(推荐使用地址补全功能)。
|
||||||
|
|
||||||
@@ -428,7 +494,7 @@
|
|||||||
- **PayPal 登录后页面无法继续跳转**
|
- **PayPal 登录后页面无法继续跳转**
|
||||||
稍等片刻让页面加载完毕。如果长时间未响应,检查浏览器是否有弹窗被隐藏,或尝试刷新页面。
|
稍等片刻让页面加载完毕。如果长时间未响应,检查浏览器是否有弹窗被隐藏,或尝试刷新页面。
|
||||||
|
|
||||||
### 第九部分:扩展内置动态 IP 代理使用教程
|
### 第十部分:扩展内置动态 IP 代理使用教程
|
||||||
|
|
||||||
本部分说明扩展侧边栏里的 `IP代理` 功能。
|
本部分说明扩展侧边栏里的 `IP代理` 功能。
|
||||||
它和 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 这类系统代理不一样:扩展会通过浏览器代理 API 给当前浏览器设置 PAC 代理,并自动回填代理鉴权。当前首版只开放 `711Proxy` 的账号密码模式,`API 拉取` 和多账号列表入口暂未开放。
|
它和 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 这类系统代理不一样:扩展会通过浏览器代理 API 给当前浏览器设置 PAC 代理,并自动回填代理鉴权。当前首版只开放 `711Proxy` 的账号密码模式,`API 拉取` 和多账号列表入口暂未开放。
|
||||||
@@ -534,7 +600,7 @@
|
|||||||
如果后续要换出口,优先尝试 `Change`。
|
如果后续要换出口,优先尝试 `Change`。
|
||||||
如果 `Change` 不可用,再调整 session 或代理账号后点击 `同步`。
|
如果 `Change` 不可用,再调整 session 或代理账号后点击 `同步`。
|
||||||
|
|
||||||
### 第十部分:节点检测与纯净度检查网站
|
### 第十一部分:节点检测与纯净度检查网站
|
||||||
|
|
||||||
本部分用于检查当前代理节点的出口 IP、归属地、风险标签、泄露情况和网站访问速度。
|
本部分用于检查当前代理节点的出口 IP、归属地、风险标签、泄露情况和网站访问速度。
|
||||||
建议每次切换节点后都重新打开这些网站检查一次。
|
建议每次切换节点后都重新打开这些网站检查一次。
|
||||||
@@ -568,7 +634,7 @@
|
|||||||
接着打开 [IPPure](https://ippure.com/) 检查纯净度、黑名单、代理识别和泄露情况。
|
接着打开 [IPPure](https://ippure.com/) 检查纯净度、黑名单、代理识别和泄露情况。
|
||||||
最后用 [TCPTest 网站测速](https://www.tcptest.cn/?cckey=e62f31db) 测试目标网站的访问速度和连通性。
|
最后用 [TCPTest 网站测速](https://www.tcptest.cn/?cckey=e62f31db) 测试目标网站的访问速度和连通性。
|
||||||
|
|
||||||
### 第十一部分:配置 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询`
|
### 第十二部分:配置 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询`
|
||||||
|
|
||||||
#### 第一步:添加扩展脚本
|
#### 第一步:添加扩展脚本
|
||||||
|
|
||||||
@@ -677,7 +743,7 @@ function main(config, profileName) {
|
|||||||
5. 确认左侧 `设置` 中的 `系统代理`(`System Proxy`)已经开启。
|
5. 确认左侧 `设置` 中的 `系统代理`(`System Proxy`)已经开启。
|
||||||

|

|
||||||
|
|
||||||
### 第十二部分:订阅节点与自建推荐
|
### 第十三部分:订阅节点与自建推荐
|
||||||
|
|
||||||
如果你还没有订阅节点或想要寻找稳定、便宜的科学上网方式,可以参考以下三种方案获取。
|
如果你还没有订阅节点或想要寻找稳定、便宜的科学上网方式,可以参考以下三种方案获取。
|
||||||
|
|
||||||
|
|||||||
+13
-23
@@ -907,14 +907,18 @@ function buildIpProxyActionHintText(options = {}) {
|
|||||||
const mode = normalizeIpProxyModeForCurrentRelease(options?.mode || DEFAULT_IP_PROXY_MODE);
|
const mode = normalizeIpProxyModeForCurrentRelease(options?.mode || DEFAULT_IP_PROXY_MODE);
|
||||||
const poolCount = Math.max(0, Number(options?.poolCount) || 0);
|
const poolCount = Math.max(0, Number(options?.poolCount) || 0);
|
||||||
const changeAvailable = Boolean(options?.changeAvailable);
|
const changeAvailable = Boolean(options?.changeAvailable);
|
||||||
|
const dynamicPoolCount = poolCount > 0 ? poolCount : 1;
|
||||||
|
|
||||||
if (mode === 'api') {
|
if (mode === 'api') {
|
||||||
return '下一条:切到已拉取代理池的下一条。Change:仅账号模式可用。';
|
const nextPart = poolCount > 1
|
||||||
|
? `下一条:当前共 ${dynamicPoolCount} 条节点,切到已拉取代理池的下一条节点。`
|
||||||
|
: `下一条:当前仅 ${dynamicPoolCount} 条节点,执行重绑复测(不保证更换出口)。`;
|
||||||
|
return `${nextPart} Change:仅账号模式可用。`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPart = poolCount > 1
|
const nextPart = poolCount > 1
|
||||||
? '下一条:切到代理池的下一条节点。'
|
? `下一条:当前共 ${dynamicPoolCount} 条节点,切到代理池的下一条节点。`
|
||||||
: '下一条:当前仅 1 条节点,执行重绑复测(不保证更换出口)。';
|
: `下一条:当前仅 ${dynamicPoolCount} 条节点,执行重绑复测(不保证更换出口)。`;
|
||||||
const changePart = changeAvailable
|
const changePart = changeAvailable
|
||||||
? 'Change:保持当前 session 重绑链路并复测出口。'
|
? 'Change:保持当前 session 重绑链路并复测出口。'
|
||||||
: 'Change:需 711 账号模式且用户名包含 session。';
|
: 'Change:需 711 账号模式且用户名包含 session。';
|
||||||
@@ -931,7 +935,6 @@ function setIpProxyCurrentDisplay(text = '', hasValue = false) {
|
|||||||
function formatIpProxyCurrentDisplay(state = latestState) {
|
function formatIpProxyCurrentDisplay(state = latestState) {
|
||||||
const mode = normalizeIpProxyModeForCurrentRelease(state?.ipProxyMode);
|
const mode = normalizeIpProxyModeForCurrentRelease(state?.ipProxyMode);
|
||||||
if (mode === 'account') {
|
if (mode === 'account') {
|
||||||
const runtime = getIpProxyRuntimeSnapshot(state, mode);
|
|
||||||
const current = getIpProxyCurrentEntry(state);
|
const current = getIpProxyCurrentEntry(state);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
return {
|
return {
|
||||||
@@ -939,10 +942,8 @@ function formatIpProxyCurrentDisplay(state = latestState) {
|
|||||||
hasValue: false,
|
hasValue: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const count = runtime.pool.length > 0 ? runtime.pool.length : 1;
|
|
||||||
const index = runtime.index;
|
|
||||||
return {
|
return {
|
||||||
text: `${current.host}:${current.port}${current.region ? ` [${current.region}]` : ''} (${Math.min(index + 1, count)}/${count})`,
|
text: `${current.host}:${current.port}${current.region ? ` [${current.region}]` : ''}`,
|
||||||
hasValue: true,
|
hasValue: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -960,7 +961,7 @@ function formatIpProxyCurrentDisplay(state = latestState) {
|
|||||||
const region = String(current.region || '').trim();
|
const region = String(current.region || '').trim();
|
||||||
const label = region ? `${current.host}:${current.port} [${region}]` : `${current.host}:${current.port}`;
|
const label = region ? `${current.host}:${current.port} [${region}]` : `${current.host}:${current.port}`;
|
||||||
return {
|
return {
|
||||||
text: `${label}${count ? ` (${Math.min(index + 1, count)}/${count})` : ''}`,
|
text: label,
|
||||||
hasValue: true,
|
hasValue: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -986,19 +987,7 @@ function buildIpProxyCurrentDisplayText(display = {}, runtimeStatus = {}) {
|
|||||||
if (!hasValue || !rawText) {
|
if (!hasValue || !rawText) {
|
||||||
return rawText;
|
return rawText;
|
||||||
}
|
}
|
||||||
const runtimeText = String(runtimeStatus?.text || '').trim().toLowerCase();
|
return rawText;
|
||||||
if (!runtimeText) {
|
|
||||||
return rawText;
|
|
||||||
}
|
|
||||||
const endpointToken = extractIpProxyEndpointToken(rawText);
|
|
||||||
if (!endpointToken || !runtimeText.includes(endpointToken)) {
|
|
||||||
return rawText;
|
|
||||||
}
|
|
||||||
const indexToken = extractIpProxyIndexToken(rawText);
|
|
||||||
if (indexToken) {
|
|
||||||
return `节点 ${indexToken}`;
|
|
||||||
}
|
|
||||||
return '当前节点';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatIpProxyRuntimeStatus(state = latestState) {
|
function formatIpProxyRuntimeStatus(state = latestState) {
|
||||||
@@ -1353,11 +1342,12 @@ function updateIpProxyUI(state = latestState) {
|
|||||||
setIpProxyCurrentDisplay(currentDisplayText, currentDisplay.hasValue);
|
setIpProxyCurrentDisplay(currentDisplayText, currentDisplay.hasValue);
|
||||||
const runtimeSnapshot = getIpProxyRuntimeSnapshot(runtimeState, mode, service);
|
const runtimeSnapshot = getIpProxyRuntimeSnapshot(runtimeState, mode, service);
|
||||||
const runtimePoolCount = Array.isArray(runtimeSnapshot?.pool) ? runtimeSnapshot.pool.length : 0;
|
const runtimePoolCount = Array.isArray(runtimeSnapshot?.pool) ? runtimeSnapshot.pool.length : 0;
|
||||||
|
const runtimePoolCountForDisplay = runtimePoolCount > 0 ? runtimePoolCount : 1;
|
||||||
const hasCurrentEntry = Boolean(getIpProxyCurrentEntry(runtimeState));
|
const hasCurrentEntry = Boolean(getIpProxyCurrentEntry(runtimeState));
|
||||||
const changeAvailable = canChangeIpProxyExitWithCurrentSession(runtimeState);
|
const changeAvailable = canChangeIpProxyExitWithCurrentSession(runtimeState);
|
||||||
const nextActionTitle = runtimePoolCount > 1
|
const nextActionTitle = runtimePoolCount > 1
|
||||||
? '切换到代理池下一条节点并应用'
|
? `切换到代理池下一条节点并应用(当前共 ${runtimePoolCountForDisplay} 条)`
|
||||||
: '当前仅 1 条节点:重绑当前节点并复测连通性(不保证更换出口)';
|
: `当前仅 ${runtimePoolCountForDisplay} 条节点:重绑当前节点并复测连通性(不保证更换出口)`;
|
||||||
|
|
||||||
if (btnIpProxyRefresh) {
|
if (btnIpProxyRefresh) {
|
||||||
btnIpProxyRefresh.disabled = actionBusy || !enabled || !canOperate;
|
btnIpProxyRefresh.disabled = actionBusy || !enabled || !canOperate;
|
||||||
|
|||||||
+378
-23
@@ -607,7 +607,12 @@ header {
|
|||||||
Data Card
|
Data Card
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
#data-section { margin-bottom: 14px; }
|
#data-section {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.data-card {
|
.data-card {
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
@@ -769,6 +774,21 @@ header {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#settings-card .data-row.module-divider-start {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings-card .data-row.module-divider-start::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--border) 76%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.data-check-row {
|
.data-check-row {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
@@ -798,24 +818,6 @@ header {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-collapse-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-collapse-body[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#btn-toggle-hotmail-section {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#btn-toggle-cloudflare-temp-email-section {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ip-proxy-fold {
|
.ip-proxy-fold {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -832,6 +834,39 @@ header {
|
|||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.phone-verification-card {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-verification-header-actions {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-toggle-phone-verification-section {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-verification-fold-row {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-verification-fold {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-verification-fold-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
border-top: none;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.ip-proxy-layout-row {
|
.ip-proxy-layout-row {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -891,10 +926,18 @@ header {
|
|||||||
|
|
||||||
.ip-proxy-actions-inline {
|
.ip-proxy-actions-inline {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
row-gap: 6px;
|
row-gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#row-ip-proxy-actions {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#row-ip-proxy-actions > .data-label {
|
||||||
|
padding-top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
.ip-proxy-action-grid {
|
.ip-proxy-action-grid {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -936,19 +979,25 @@ header {
|
|||||||
|
|
||||||
.ip-proxy-runtime-main {
|
.ip-proxy-runtime-main {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-proxy-runtime-meta {
|
.ip-proxy-runtime-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-proxy-check-ip-btn {
|
.ip-proxy-check-ip-btn {
|
||||||
min-width: 64px;
|
min-width: 0;
|
||||||
padding-inline: 10px;
|
padding-inline: 8px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-proxy-runtime-current {
|
.ip-proxy-runtime-current {
|
||||||
@@ -962,6 +1011,14 @@ header {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#row-ip-proxy-runtime-status {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#row-ip-proxy-runtime-status > .data-label {
|
||||||
|
padding-top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
.ip-proxy-runtime-dot {
|
.ip-proxy-runtime-dot {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
@@ -990,20 +1047,53 @@ header {
|
|||||||
.ip-proxy-runtime-details {
|
.ip-proxy-runtime-details {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding-right: 84px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-proxy-runtime-details-row {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-proxy-runtime-details summary {
|
.ip-proxy-runtime-details summary {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-height: 24px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-proxy-runtime-details summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-proxy-runtime-details summary::after {
|
||||||
|
content: '▾';
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
color: inherit;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transform-origin: center;
|
||||||
|
transition: transform var(--transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-proxy-runtime-details[open] summary {
|
.ip-proxy-runtime-details[open] summary {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ip-proxy-runtime-details[open] summary::after {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
.ip-proxy-runtime-details-text {
|
.ip-proxy-runtime-details-text {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -1859,6 +1949,271 @@ header {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-sms-country-stack {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-country-mainline {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-country-note {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-reuse-max-inline {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-reuse-max-left {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-reuse-max-right {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-max-price-input {
|
||||||
|
width: 72px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-country-menu {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-country-menu-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 33px;
|
||||||
|
min-height: 33px;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
justify-content: flex-start;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-country-menu-btn[aria-expanded="true"] {
|
||||||
|
border-color: var(--blue);
|
||||||
|
color: var(--blue);
|
||||||
|
background: var(--blue-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-country-menu-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1200;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--bg-base);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-country-menu-search {
|
||||||
|
padding-bottom: 6px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-country-menu-search-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-country-menu-dropdown[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-country-menu-item {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-country-menu-item-label {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-country-menu-item-badge {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 42px;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--brand);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-runtime-grid {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 4px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-runtime-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-runtime-cell-span2 {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-runtime-key {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-runtime-value {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-price-preview-stack {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-price-preview-head {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-hero-sms-price-preview {
|
||||||
|
height: 33px;
|
||||||
|
min-height: 33px;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-price-controls-grid {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-price-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-price-control .setting-controls {
|
||||||
|
margin-left: auto;
|
||||||
|
width: 104px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-price-control-reuse {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#row-hero-sms-max-price,
|
||||||
|
#row-phone-code-settings-group {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#row-hero-sms-max-price > .data-label,
|
||||||
|
#row-phone-code-settings-group > .data-label {
|
||||||
|
padding-top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-toggle-controls {
|
||||||
|
width: 104px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-price-preview-result {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-price-preview-text {
|
||||||
|
display: block;
|
||||||
|
white-space: pre-line;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-settings-grid {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-settings-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-settings-cell .setting-controls {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sms-settings-caption {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.data-unit {
|
.data-unit {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
+296
-171
@@ -216,7 +216,7 @@
|
|||||||
title="显示 Codex2API 管理密钥"></button>
|
title="显示 Codex2API 管理密钥"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-custom-password">
|
<div class="data-row module-divider-start" id="row-custom-password">
|
||||||
<span class="data-label">账户密码</span>
|
<span class="data-label">账户密码</span>
|
||||||
<div class="input-with-icon">
|
<div class="input-with-icon">
|
||||||
<input type="password" id="input-password" class="data-input data-input-with-icon"
|
<input type="password" id="input-password" class="data-input data-input-with-icon"
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
<button id="btn-add-paypal-account" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
|
<button id="btn-add-paypal-account" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row module-divider-start" id="row-mail-provider">
|
||||||
<span class="data-label">邮箱服务</span>
|
<span class="data-label">邮箱服务</span>
|
||||||
<div class="data-inline">
|
<div class="data-inline">
|
||||||
<select id="select-mail-provider" class="data-select">
|
<select id="select-mail-provider" class="data-select">
|
||||||
@@ -335,14 +335,14 @@
|
|||||||
<span class="data-label">邮箱名</span>
|
<span class="data-label">邮箱名</span>
|
||||||
<input type="text" id="input-inbucket-mailbox" class="data-input" placeholder="例如 zju2001" />
|
<input type="text" id="input-inbucket-mailbox" class="data-input" placeholder="例如 zju2001" />
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row" id="row-auto-run-controls">
|
||||||
<span class="data-label">注册邮箱</span>
|
<span class="data-label">注册邮箱</span>
|
||||||
<div class="data-inline">
|
<div class="data-inline">
|
||||||
<input type="text" id="input-email" class="data-input" placeholder="自动生成或手动粘贴邮箱" />
|
<input type="text" id="input-email" class="data-input" placeholder="自动生成或手动粘贴邮箱" />
|
||||||
<button id="btn-fetch-email" class="btn btn-outline btn-sm data-inline-btn" type="button">获取</button>
|
<button id="btn-fetch-email" class="btn btn-outline btn-sm data-inline-btn" type="button">获取</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row module-divider-start" id="row-auto-delay-settings">
|
||||||
<span class="data-label">延迟</span>
|
<span class="data-label">延迟</span>
|
||||||
<div class="data-inline setting-pair">
|
<div class="data-inline setting-pair">
|
||||||
<div class="setting-group setting-group-primary">
|
<div class="setting-group setting-group-primary">
|
||||||
@@ -389,58 +389,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-phone-verification-enabled">
|
<div class="data-row module-divider-start" id="row-oauth-display">
|
||||||
<span class="data-label">接码</span>
|
|
||||||
<div class="data-inline setting-pair">
|
|
||||||
<div class="setting-group setting-group-primary">
|
|
||||||
<label class="toggle-switch" for="input-phone-verification-enabled">
|
|
||||||
<input type="checkbox" id="input-phone-verification-enabled" />
|
|
||||||
<span class="toggle-switch-track" aria-hidden="true">
|
|
||||||
<span class="toggle-switch-thumb"></span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="setting-group setting-group-secondary">
|
|
||||||
<span class="setting-caption">验证码重发</span>
|
|
||||||
<div class="setting-controls">
|
|
||||||
<input type="number" id="input-verification-resend-count" class="data-input auto-delay-input" value="4"
|
|
||||||
min="0" max="20" step="1" title="自动点击重新发送验证码的次数" />
|
|
||||||
<span class="data-unit">次</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="data-row" id="row-account-run-history-helper-base-url" style="display:none;">
|
|
||||||
<span class="data-label">同步服务</span>
|
|
||||||
<input type="text" id="input-account-run-history-helper-base-url" class="data-input mono"
|
|
||||||
placeholder="http://127.0.0.1:17373" />
|
|
||||||
</div>
|
|
||||||
<div class="data-row" id="row-hero-sms-platform" style="display:none;">
|
|
||||||
<span class="data-label">接码平台</span>
|
|
||||||
<span id="display-hero-sms-platform" class="data-value mono">HeroSMS / OpenAI / Thailand</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-row" id="row-hero-sms-country" style="display:none;">
|
|
||||||
<span class="data-label">接码国家</span>
|
|
||||||
<select id="select-hero-sms-country" class="data-input mono">
|
|
||||||
<option value="52" selected>Thailand</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="data-row" id="row-hero-sms-max-price" style="display:none;">
|
|
||||||
<span class="data-label">最高价格</span>
|
|
||||||
<input type="number" id="input-hero-sms-max-price" class="data-input mono" min="0.0001" step="0.0001" required
|
|
||||||
placeholder="必填,例如 0.08" />
|
|
||||||
</div>
|
|
||||||
<div class="data-row" id="row-hero-sms-api-key" style="display:none;">
|
|
||||||
<span class="data-label">接码 API</span>
|
|
||||||
<div class="input-with-icon">
|
|
||||||
<input type="password" id="input-hero-sms-api-key" class="data-input data-input-with-icon mono"
|
|
||||||
placeholder="请输入 HeroSMS API Key" />
|
|
||||||
<button id="btn-toggle-hero-sms-api-key" class="input-icon-btn" type="button"
|
|
||||||
data-password-toggle="input-hero-sms-api-key" data-show-label="显示 HeroSMS API Key"
|
|
||||||
data-hide-label="隐藏 HeroSMS API Key" aria-label="显示 HeroSMS API Key" title="显示 HeroSMS API Key"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="data-row">
|
|
||||||
<span class="data-label">OAuth</span>
|
<span class="data-label">OAuth</span>
|
||||||
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
|
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -452,6 +401,171 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="phone-verification-section" class="data-card phone-verification-card">
|
||||||
|
<div class="section-mini-header">
|
||||||
|
<div class="section-mini-copy">
|
||||||
|
<span class="section-label">接码设置</span>
|
||||||
|
<span class="data-value">手机号验证与 HeroSMS 获取策略</span>
|
||||||
|
</div>
|
||||||
|
<div id="row-phone-verification-enabled" class="section-mini-actions phone-verification-header-actions">
|
||||||
|
<button id="btn-toggle-phone-verification-section" class="btn btn-ghost btn-xs" type="button"
|
||||||
|
aria-expanded="false" aria-controls="row-phone-verification-fold">展开设置</button>
|
||||||
|
<label class="toggle-switch" for="input-phone-verification-enabled" title="启用或禁用手机号接码流程">
|
||||||
|
<input type="checkbox" id="input-phone-verification-enabled" />
|
||||||
|
<span class="toggle-switch-track" aria-hidden="true">
|
||||||
|
<span class="toggle-switch-thumb"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-row phone-verification-fold-row" id="row-phone-verification-fold" style="display:none;">
|
||||||
|
<div id="phone-verification-fold" class="phone-verification-fold">
|
||||||
|
<div class="phone-verification-fold-body">
|
||||||
|
<div class="data-row" id="row-account-run-history-helper-base-url" style="display:none;">
|
||||||
|
<span class="data-label">同步服务</span>
|
||||||
|
<input type="text" id="input-account-run-history-helper-base-url" class="data-input mono"
|
||||||
|
placeholder="http://127.0.0.1:17373" />
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-hero-sms-platform" style="display:none;">
|
||||||
|
<span class="data-label">接码平台</span>
|
||||||
|
<span id="display-hero-sms-platform" class="data-value mono">HeroSMS / OpenAI / Thailand</span>
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-hero-sms-country" style="display:none;">
|
||||||
|
<span class="data-label">国家优先级</span>
|
||||||
|
<div class="data-inline hero-sms-country-stack">
|
||||||
|
<select id="select-hero-sms-country" class="data-input mono" multiple size="6" style="display:none;">
|
||||||
|
<option value="52" selected>Thailand</option>
|
||||||
|
</select>
|
||||||
|
<div class="hero-sms-country-mainline">
|
||||||
|
<div id="hero-sms-country-menu-shell" class="hero-sms-country-menu">
|
||||||
|
<button id="btn-hero-sms-country-menu" class="btn btn-outline btn-sm hero-sms-country-menu-btn" type="button" aria-haspopup="listbox" aria-expanded="false">
|
||||||
|
Thailand (1/3)
|
||||||
|
</button>
|
||||||
|
<div id="hero-sms-country-menu" class="hero-sms-country-menu-dropdown" role="listbox" aria-multiselectable="true" hidden></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="data-value hero-sms-country-note">多选最多 3 个,按点击顺序生效。</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-hero-sms-country-fallback" style="display:none;">
|
||||||
|
<span class="data-label">生效顺序</span>
|
||||||
|
<div class="data-inline data-value-actions">
|
||||||
|
<span id="display-hero-sms-country-fallback-order" class="data-value data-value-fill mono">Thailand(52)</span>
|
||||||
|
<button id="btn-hero-sms-country-clear" class="btn btn-ghost btn-xs data-inline-btn" type="button">清空</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-hero-sms-acquire-priority" style="display:none;">
|
||||||
|
<span class="data-label">拿号优先级</span>
|
||||||
|
<select id="select-hero-sms-acquire-priority" class="data-input mono">
|
||||||
|
<option value="country">国家优先(默认)</option>
|
||||||
|
<option value="price">价格优先(同价按国家顺序)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-hero-sms-api-key" style="display:none;">
|
||||||
|
<span class="data-label">接码 API</span>
|
||||||
|
<div class="input-with-icon">
|
||||||
|
<input type="password" id="input-hero-sms-api-key" class="data-input data-input-with-icon mono"
|
||||||
|
placeholder="请输入 HeroSMS API Key" />
|
||||||
|
<button id="btn-toggle-hero-sms-api-key" class="input-icon-btn" type="button"
|
||||||
|
data-password-toggle="input-hero-sms-api-key" data-show-label="显示 HeroSMS API Key"
|
||||||
|
data-hide-label="隐藏 HeroSMS API Key" aria-label="显示 HeroSMS API Key" title="显示 HeroSMS API Key"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-hero-sms-max-price" style="display:none;">
|
||||||
|
<span class="data-label">价格</span>
|
||||||
|
<div class="data-inline hero-sms-price-preview-stack">
|
||||||
|
<div class="hero-sms-price-preview-head">
|
||||||
|
<button id="btn-hero-sms-price-preview" class="btn btn-outline btn-xs data-inline-btn" type="button">查询价格</button>
|
||||||
|
</div>
|
||||||
|
<div id="row-hero-sms-price-tiers" class="hero-sms-price-preview-result" style="display:none;">
|
||||||
|
<span id="display-hero-sms-price-tiers" class="data-value mono hero-sms-price-preview-text">未获取</span>
|
||||||
|
</div>
|
||||||
|
<div class="hero-sms-price-controls-grid">
|
||||||
|
<div class="hero-sms-price-control">
|
||||||
|
<span class="hero-sms-settings-caption">价格上限</span>
|
||||||
|
<div class="setting-controls">
|
||||||
|
<input type="number" id="input-hero-sms-max-price" class="data-input auto-delay-input mono hero-sms-max-price-input" placeholder="0.12" min="0" step="0.0001" title="接码价格上限;可空(空=自动价格)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-sms-price-control hero-sms-price-control-reuse">
|
||||||
|
<span class="hero-sms-settings-caption">号码复用</span>
|
||||||
|
<div class="setting-controls hero-sms-toggle-controls">
|
||||||
|
<label class="toggle-switch hero-sms-price-reuse-toggle" for="input-hero-sms-reuse-enabled" title="开启后会优先复用未超次数的可用号码">
|
||||||
|
<input type="checkbox" id="input-hero-sms-reuse-enabled" />
|
||||||
|
<span class="toggle-switch-track" aria-hidden="true">
|
||||||
|
<span class="toggle-switch-thumb"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-phone-code-settings-group" style="display:none;">
|
||||||
|
<span class="data-label">接码参数</span>
|
||||||
|
<div class="data-inline hero-sms-settings-grid">
|
||||||
|
<div id="row-phone-verification-resend-count" class="hero-sms-settings-cell" style="display:none;">
|
||||||
|
<span class="hero-sms-settings-caption">验证码重发</span>
|
||||||
|
<div class="setting-controls">
|
||||||
|
<input type="number" id="input-verification-resend-count" class="data-input auto-delay-input" value="4"
|
||||||
|
min="0" max="20" step="1" title="自动点击重新发送验证码的次数" />
|
||||||
|
<span class="data-unit">次</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="row-phone-replacement-limit" class="hero-sms-settings-cell" style="display:none;">
|
||||||
|
<span class="hero-sms-settings-caption">换号上限</span>
|
||||||
|
<div class="setting-controls">
|
||||||
|
<input type="number" id="input-phone-replacement-limit" class="data-input auto-delay-input" value="3" min="1" max="20" step="1" title="步骤 9 内部允许更换号码的最大次数" />
|
||||||
|
<span class="data-unit">次</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="row-phone-code-wait-seconds" class="hero-sms-settings-cell" style="display:none;">
|
||||||
|
<span class="hero-sms-settings-caption">验证码限时</span>
|
||||||
|
<div class="setting-controls">
|
||||||
|
<input type="number" id="input-phone-code-wait-seconds" class="data-input auto-delay-input" value="60" min="15" max="300" step="1" title="每轮等待验证码的秒数" />
|
||||||
|
<span class="data-unit">秒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="row-phone-code-timeout-windows" class="hero-sms-settings-cell" style="display:none;">
|
||||||
|
<span class="hero-sms-settings-caption">超时次数</span>
|
||||||
|
<div class="setting-controls">
|
||||||
|
<input type="number" id="input-phone-code-timeout-windows" class="data-input auto-delay-input" value="2" min="1" max="10" step="1" title="验证码超时后,最多继续等待几轮再换号" />
|
||||||
|
<span class="data-unit">轮</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="row-phone-code-poll-interval-seconds" class="hero-sms-settings-cell" style="display:none;">
|
||||||
|
<span class="hero-sms-settings-caption">轮询间隔</span>
|
||||||
|
<div class="setting-controls">
|
||||||
|
<input type="number" id="input-phone-code-poll-interval-seconds" class="data-input auto-delay-input" value="5" min="1" max="30" step="1" title="向 HeroSMS 查询验证码状态的间隔秒数" />
|
||||||
|
<span class="data-unit">秒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="row-phone-code-poll-max-rounds" class="hero-sms-settings-cell" style="display:none;">
|
||||||
|
<span class="hero-sms-settings-caption">轮询次数</span>
|
||||||
|
<div class="setting-controls">
|
||||||
|
<input type="number" id="input-phone-code-poll-max-rounds" class="data-input auto-delay-input" value="4" min="1" max="120" step="1" title="每轮验证码等待窗口最多轮询次数" />
|
||||||
|
<span class="data-unit">次</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-hero-sms-runtime-pair" style="display:none;">
|
||||||
|
<span class="data-label">运行状态</span>
|
||||||
|
<div class="data-inline hero-sms-runtime-grid">
|
||||||
|
<div id="row-hero-sms-current-number" class="hero-sms-runtime-cell" style="display:none;">
|
||||||
|
<span class="hero-sms-runtime-key">当前分配</span>
|
||||||
|
<span id="display-hero-sms-current-number" class="data-value mono hero-sms-runtime-value">未分配</span>
|
||||||
|
</div>
|
||||||
|
<div id="row-hero-sms-current-code" class="hero-sms-runtime-cell" style="display:none;">
|
||||||
|
<span class="hero-sms-runtime-key">验证码</span>
|
||||||
|
<span id="display-hero-sms-current-code" class="data-value mono hero-sms-runtime-value">未获取</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="ip-proxy-section" class="data-card ip-proxy-card">
|
<div id="ip-proxy-section" class="data-card ip-proxy-card">
|
||||||
<div class="section-mini-header">
|
<div class="section-mini-header">
|
||||||
<div class="section-mini-copy">
|
<div class="section-mini-copy">
|
||||||
@@ -589,6 +703,25 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="data-row" id="row-ip-proxy-runtime-status" style="display:none;">
|
||||||
|
<span class="data-label">代理状态</span>
|
||||||
|
<div id="ip-proxy-runtime-status" class="ip-proxy-runtime-status state-idle">
|
||||||
|
<span id="ip-proxy-runtime-dot" class="ip-proxy-runtime-dot" aria-hidden="true"></span>
|
||||||
|
<div class="ip-proxy-runtime-content">
|
||||||
|
<div id="ip-proxy-runtime-text" class="ip-proxy-runtime-main">未启用,沿用浏览器默认/全局代理。</div>
|
||||||
|
<div class="ip-proxy-runtime-meta">
|
||||||
|
<span id="ip-proxy-current" class="ip-proxy-runtime-current">暂无可用代理</span>
|
||||||
|
</div>
|
||||||
|
<div class="ip-proxy-runtime-details-row">
|
||||||
|
<details id="ip-proxy-runtime-details" class="ip-proxy-runtime-details" hidden>
|
||||||
|
<summary>详情</summary>
|
||||||
|
<div id="ip-proxy-runtime-details-text" class="ip-proxy-runtime-details-text"></div>
|
||||||
|
</details>
|
||||||
|
<button id="btn-ip-proxy-check-ip" class="btn btn-outline btn-xs ip-proxy-check-ip-btn" type="button">检查IP</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -600,64 +733,60 @@
|
|||||||
<span class="data-value">用于生成邮箱或接收转发邮件</span>
|
<span class="data-value">用于生成邮箱或接收转发邮件</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-mini-actions">
|
<div class="section-mini-actions">
|
||||||
<button id="btn-toggle-cloudflare-temp-email-section" class="btn btn-ghost btn-xs" type="button"
|
|
||||||
aria-expanded="false" aria-controls="cloudflare-temp-email-section-body">展开设置</button>
|
|
||||||
<button id="btn-cloudflare-temp-email-usage-guide" class="btn btn-ghost btn-xs" type="button">使用教程</button>
|
<button id="btn-cloudflare-temp-email-usage-guide" class="btn btn-ghost btn-xs" type="button">使用教程</button>
|
||||||
<button id="btn-cloudflare-temp-email-github" class="btn btn-ghost btn-xs" type="button">GitHub</button>
|
<button id="btn-cloudflare-temp-email-github" class="btn btn-ghost btn-xs" type="button">GitHub</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="cloudflare-temp-email-section-body" class="section-collapse-body" hidden>
|
<div class="data-row" id="row-temp-email-base-url" style="display:none;">
|
||||||
<div class="data-row" id="row-temp-email-base-url" style="display:none;">
|
<span class="data-label">Temp API</span>
|
||||||
<span class="data-label">Temp API</span>
|
<input type="text" id="input-temp-email-base-url" class="data-input" placeholder="https://your-worker-domain" />
|
||||||
<input type="text" id="input-temp-email-base-url" class="data-input" placeholder="https://your-worker-domain" />
|
</div>
|
||||||
|
<div class="data-row" id="row-temp-email-admin-auth" style="display:none;">
|
||||||
|
<span class="data-label">Admin Auth</span>
|
||||||
|
<div class="input-with-icon">
|
||||||
|
<input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon"
|
||||||
|
placeholder="Cloudflare Temp Email admin password" />
|
||||||
|
<button id="btn-toggle-temp-email-admin-auth" class="input-icon-btn" type="button"
|
||||||
|
data-password-toggle="input-temp-email-admin-auth" data-show-label="显示 Admin Auth"
|
||||||
|
data-hide-label="隐藏 Admin Auth" aria-label="显示 Admin Auth" title="显示 Admin Auth"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-temp-email-admin-auth" style="display:none;">
|
</div>
|
||||||
<span class="data-label">Admin Auth</span>
|
<div class="data-row" id="row-temp-email-custom-auth" style="display:none;">
|
||||||
<div class="input-with-icon">
|
<span class="data-label">Custom Auth</span>
|
||||||
<input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon"
|
<div class="input-with-icon">
|
||||||
placeholder="Cloudflare Temp Email admin password" />
|
<input type="password" id="input-temp-email-custom-auth" class="data-input data-input-with-icon"
|
||||||
<button id="btn-toggle-temp-email-admin-auth" class="input-icon-btn" type="button"
|
placeholder="仅当站点启用了访问密码时再填写;这是额外鉴权,不替代 Admin Auth。" />
|
||||||
data-password-toggle="input-temp-email-admin-auth" data-show-label="显示 Admin Auth"
|
<button id="btn-toggle-temp-email-custom-auth" class="input-icon-btn" type="button"
|
||||||
data-hide-label="隐藏 Admin Auth" aria-label="显示 Admin Auth" title="显示 Admin Auth"></button>
|
data-password-toggle="input-temp-email-custom-auth" data-show-label="显示 Custom Auth"
|
||||||
</div>
|
data-hide-label="隐藏 Custom Auth" aria-label="显示 Custom Auth" title="显示 Custom Auth"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-temp-email-custom-auth" style="display:none;">
|
</div>
|
||||||
<span class="data-label">Custom Auth</span>
|
<div class="data-row" id="row-temp-email-receive-mailbox" style="display:none;">
|
||||||
<div class="input-with-icon">
|
<span class="data-label">邮件接收</span>
|
||||||
<input type="password" id="input-temp-email-custom-auth" class="data-input data-input-with-icon"
|
<input type="text" id="input-temp-email-receive-mailbox" class="data-input"
|
||||||
placeholder="仅当站点启用了访问密码时再填写;这是额外鉴权,不替代 Admin Auth。" />
|
placeholder="用于接收转发邮件的邮箱,例如 1@email.example.com" />
|
||||||
<button id="btn-toggle-temp-email-custom-auth" class="input-icon-btn" type="button"
|
</div>
|
||||||
data-password-toggle="input-temp-email-custom-auth" data-show-label="显示 Custom Auth"
|
<div class="data-row" id="row-temp-email-random-subdomain-toggle" style="display:none;">
|
||||||
data-hide-label="隐藏 Custom Auth" aria-label="显示 Custom Auth" title="显示 Custom Auth"></button>
|
<span class="data-label">随机子域</span>
|
||||||
</div>
|
<div class="data-inline">
|
||||||
|
<label class="toggle-switch" for="input-temp-email-use-random-subdomain"
|
||||||
|
title="依赖后端 RANDOM_SUBDOMAIN_DOMAINS 配置">
|
||||||
|
<input type="checkbox" id="input-temp-email-use-random-subdomain" />
|
||||||
|
<span class="toggle-switch-track" aria-hidden="true">
|
||||||
|
<span class="toggle-switch-thumb"></span>
|
||||||
|
</span>
|
||||||
|
<span>启用</span>
|
||||||
|
</label>
|
||||||
|
<span class="setting-caption">依赖后端 RANDOM_SUBDOMAIN_DOMAINS</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-temp-email-receive-mailbox" style="display:none;">
|
</div>
|
||||||
<span class="data-label">邮件接收</span>
|
<div class="data-row" id="row-temp-email-domain" style="display:none;">
|
||||||
<input type="text" id="input-temp-email-receive-mailbox" class="data-input"
|
<span class="data-label">Temp 域名</span>
|
||||||
placeholder="用于接收转发邮件的邮箱,例如 1@email.example.com" />
|
<div class="data-inline">
|
||||||
</div>
|
<select id="select-temp-email-domain" class="data-select"></select>
|
||||||
<div class="data-row" id="row-temp-email-random-subdomain-toggle" style="display:none;">
|
<input type="text" id="input-temp-email-domain" class="data-input" placeholder="例如 mail.example.com"
|
||||||
<span class="data-label">随机子域</span>
|
style="display:none;" />
|
||||||
<div class="data-inline">
|
<button id="btn-temp-email-domain-mode" class="btn btn-outline btn-sm" type="button">添加</button>
|
||||||
<label class="toggle-switch" for="input-temp-email-use-random-subdomain"
|
|
||||||
title="依赖后端 RANDOM_SUBDOMAIN_DOMAINS 配置">
|
|
||||||
<input type="checkbox" id="input-temp-email-use-random-subdomain" />
|
|
||||||
<span class="toggle-switch-track" aria-hidden="true">
|
|
||||||
<span class="toggle-switch-thumb"></span>
|
|
||||||
</span>
|
|
||||||
<span>启用</span>
|
|
||||||
</label>
|
|
||||||
<span class="setting-caption">依赖后端 RANDOM_SUBDOMAIN_DOMAINS</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="data-row" id="row-temp-email-domain" style="display:none;">
|
|
||||||
<span class="data-label">Temp 域名</span>
|
|
||||||
<div class="data-inline">
|
|
||||||
<select id="select-temp-email-domain" class="data-select"></select>
|
|
||||||
<input type="text" id="input-temp-email-domain" class="data-input" placeholder="例如 mail.example.com"
|
|
||||||
style="display:none;" />
|
|
||||||
<button id="btn-temp-email-domain-mode" class="btn btn-outline btn-sm" type="button">添加</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -667,8 +796,6 @@
|
|||||||
<span class="section-label">Hotmail 账号池</span>
|
<span class="section-label">Hotmail 账号池</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-mini-actions">
|
<div class="section-mini-actions">
|
||||||
<button id="btn-toggle-hotmail-section" class="btn btn-ghost btn-xs" type="button"
|
|
||||||
aria-expanded="false" aria-controls="hotmail-section-body">展开设置</button>
|
|
||||||
<button id="btn-toggle-hotmail-form" class="btn btn-outline btn-xs" type="button"
|
<button id="btn-toggle-hotmail-form" class="btn btn-outline btn-xs" type="button"
|
||||||
aria-expanded="false">添加账号</button>
|
aria-expanded="false">添加账号</button>
|
||||||
<button id="btn-hotmail-usage-guide" class="btn btn-ghost btn-xs" type="button">使用教程</button>
|
<button id="btn-hotmail-usage-guide" class="btn btn-ghost btn-xs" type="button">使用教程</button>
|
||||||
@@ -678,73 +805,71 @@
|
|||||||
aria-expanded="false">展开列表</button>
|
aria-expanded="false">展开列表</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="hotmail-section-body" class="section-collapse-body" hidden>
|
<div class="data-row" id="row-hotmail-service-mode">
|
||||||
<div class="data-row" id="row-hotmail-service-mode">
|
<span class="data-label">接码模式</span>
|
||||||
<span class="data-label">接码模式</span>
|
<div id="hotmail-service-mode-group" class="choice-group" role="group" aria-label="Hotmail 接码模式">
|
||||||
<div id="hotmail-service-mode-group" class="choice-group" role="group" aria-label="Hotmail 接码模式">
|
<button type="button" class="choice-btn" data-hotmail-service-mode="remote">API对接</button>
|
||||||
<button type="button" class="choice-btn" data-hotmail-service-mode="remote">API对接</button>
|
<button type="button" class="choice-btn" data-hotmail-service-mode="local">本地助手</button>
|
||||||
<button type="button" class="choice-btn" data-hotmail-service-mode="local">本地助手</button>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-hotmail-remote-base-url">
|
||||||
|
<span class="data-label">API对接</span>
|
||||||
|
<input type="text" id="input-hotmail-remote-base-url" class="data-input mono"
|
||||||
|
placeholder="微软邮箱 API 对接模式无需填写地址" />
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-hotmail-local-base-url" style="display:none;">
|
||||||
|
<span class="data-label">本地助手</span>
|
||||||
|
<input type="text" id="input-hotmail-local-base-url" class="data-input mono"
|
||||||
|
placeholder="http://127.0.0.1:17373" />
|
||||||
|
</div>
|
||||||
|
<div id="hotmail-form-shell" class="account-pool-form-shell" hidden>
|
||||||
|
<div class="data-row">
|
||||||
|
<span class="data-label">邮箱</span>
|
||||||
|
<input type="text" id="input-hotmail-email" class="data-input" placeholder="name@hotmail.com" />
|
||||||
|
</div>
|
||||||
|
<div class="data-row">
|
||||||
|
<span class="data-label">客户端 ID</span>
|
||||||
|
<input type="text" id="input-hotmail-client-id" class="data-input mono" placeholder="微软应用客户端 ID" />
|
||||||
|
</div>
|
||||||
|
<div class="data-row">
|
||||||
|
<span class="data-label">邮箱密码</span>
|
||||||
|
<div class="input-with-icon">
|
||||||
|
<input type="password" id="input-hotmail-password" class="data-input data-input-with-icon"
|
||||||
|
placeholder="可选,仅用于记录" />
|
||||||
|
<button id="btn-toggle-hotmail-password" class="input-icon-btn" type="button"
|
||||||
|
data-password-toggle="input-hotmail-password" data-show-label="显示 Hotmail 密码"
|
||||||
|
data-hide-label="隐藏 Hotmail 密码" aria-label="显示 Hotmail 密码" title="显示 Hotmail 密码"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-hotmail-remote-base-url">
|
<div class="data-row">
|
||||||
<span class="data-label">API对接</span>
|
<span class="data-label">刷新令牌</span>
|
||||||
<input type="text" id="input-hotmail-remote-base-url" class="data-input mono"
|
<div class="input-with-icon">
|
||||||
placeholder="微软邮箱 API 对接模式无需填写地址" />
|
<input type="password" id="input-hotmail-refresh-token" class="data-input data-input-with-icon mono"
|
||||||
</div>
|
placeholder="必填,粘贴刷新令牌(refresh token)" />
|
||||||
<div class="data-row" id="row-hotmail-local-base-url" style="display:none;">
|
<button id="btn-toggle-hotmail-refresh-token" class="input-icon-btn" type="button"
|
||||||
<span class="data-label">本地助手</span>
|
data-password-toggle="input-hotmail-refresh-token" data-show-label="显示 Hotmail 刷新令牌"
|
||||||
<input type="text" id="input-hotmail-local-base-url" class="data-input mono"
|
data-hide-label="隐藏 Hotmail 刷新令牌" aria-label="显示 Hotmail 刷新令牌"
|
||||||
placeholder="http://127.0.0.1:17373" />
|
title="显示 Hotmail 刷新令牌"></button>
|
||||||
</div>
|
|
||||||
<div id="hotmail-form-shell" class="account-pool-form-shell" hidden>
|
|
||||||
<div class="data-row">
|
|
||||||
<span class="data-label">邮箱</span>
|
|
||||||
<input type="text" id="input-hotmail-email" class="data-input" placeholder="name@hotmail.com" />
|
|
||||||
</div>
|
|
||||||
<div class="data-row">
|
|
||||||
<span class="data-label">客户端 ID</span>
|
|
||||||
<input type="text" id="input-hotmail-client-id" class="data-input mono" placeholder="微软应用客户端 ID" />
|
|
||||||
</div>
|
|
||||||
<div class="data-row">
|
|
||||||
<span class="data-label">邮箱密码</span>
|
|
||||||
<div class="input-with-icon">
|
|
||||||
<input type="password" id="input-hotmail-password" class="data-input data-input-with-icon"
|
|
||||||
placeholder="可选,仅用于记录" />
|
|
||||||
<button id="btn-toggle-hotmail-password" class="input-icon-btn" type="button"
|
|
||||||
data-password-toggle="input-hotmail-password" data-show-label="显示 Hotmail 密码"
|
|
||||||
data-hide-label="隐藏 Hotmail 密码" aria-label="显示 Hotmail 密码" title="显示 Hotmail 密码"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="data-row">
|
|
||||||
<span class="data-label">刷新令牌</span>
|
|
||||||
<div class="input-with-icon">
|
|
||||||
<input type="password" id="input-hotmail-refresh-token" class="data-input data-input-with-icon mono"
|
|
||||||
placeholder="必填,粘贴刷新令牌(refresh token)" />
|
|
||||||
<button id="btn-toggle-hotmail-refresh-token" class="input-icon-btn" type="button"
|
|
||||||
data-password-toggle="input-hotmail-refresh-token" data-show-label="显示 Hotmail 刷新令牌"
|
|
||||||
data-hide-label="隐藏 Hotmail 刷新令牌" aria-label="显示 Hotmail 刷新令牌"
|
|
||||||
title="显示 Hotmail 刷新令牌"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="data-row hotmail-actions-row">
|
|
||||||
<span class="data-label"></span>
|
|
||||||
<div class="data-inline account-pool-actions-inline">
|
|
||||||
<button id="btn-add-hotmail-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
|
|
||||||
<button id="btn-import-hotmail-accounts" class="btn btn-outline btn-sm account-pool-import-action"
|
|
||||||
type="button">批量导入</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="data-row hotmail-import-row">
|
|
||||||
<span class="data-label">批量导入</span>
|
|
||||||
<div class="hotmail-import-box">
|
|
||||||
<textarea id="input-hotmail-import" class="data-textarea mono"
|
|
||||||
placeholder="账号----密码----客户端ID----刷新令牌 name@hotmail.com----password----client-id----refresh-token"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="hotmail-list-shell" class="hotmail-list-shell is-collapsed">
|
<div class="data-row hotmail-actions-row">
|
||||||
<div id="hotmail-accounts-list" class="hotmail-accounts-list"></div>
|
<span class="data-label"></span>
|
||||||
|
<div class="data-inline account-pool-actions-inline">
|
||||||
|
<button id="btn-add-hotmail-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
|
||||||
|
<button id="btn-import-hotmail-accounts" class="btn btn-outline btn-sm account-pool-import-action"
|
||||||
|
type="button">批量导入</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="data-row hotmail-import-row">
|
||||||
|
<span class="data-label">批量导入</span>
|
||||||
|
<div class="hotmail-import-box">
|
||||||
|
<textarea id="input-hotmail-import" class="data-textarea mono"
|
||||||
|
placeholder="账号----密码----客户端ID----刷新令牌 name@hotmail.com----password----client-id----refresh-token"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="hotmail-list-shell" class="hotmail-list-shell is-collapsed">
|
||||||
|
<div id="hotmail-accounts-list" class="hotmail-accounts-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="mail2925-section" class="data-card hotmail-card" style="display:none;">
|
<div id="mail2925-section" class="data-card hotmail-card" style="display:none;">
|
||||||
|
|||||||
+1387
-196
File diff suppressed because it is too large
Load Diff
@@ -215,18 +215,20 @@ test('auto-run stops restarting on generic phone-page failure messages even with
|
|||||||
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
|
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('auto-run restarts from step 7 when phone verification cannot receive SMS after resend', async () => {
|
test('auto-run does not restart step 7 when phone verification exhausted replacement attempts in add-phone flow', async () => {
|
||||||
const harness = createHarness({
|
const harness = createHarness({
|
||||||
failureStep: 9,
|
failureStep: 9,
|
||||||
failureBudget: 1,
|
failureBudget: 1,
|
||||||
failureMessage: 'Phone verification could not receive an SMS after resend. Restart step 7 with a new number. Current number: 66959916439.',
|
failureMessage: 'Step 9: phone verification did not succeed after 3 number replacements. Last reason: sms_timeout_after_resend.',
|
||||||
authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' },
|
authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const events = await harness.run();
|
const result = await harness.runAndCaptureError();
|
||||||
|
|
||||||
assert.equal(events.invalidations.length, 1);
|
assert.ok(result?.error);
|
||||||
assert.deepStrictEqual(events.steps, [7, 8, 9, 7, 8, 9, 10]);
|
assert.equal(result.events.invalidations.length, 0);
|
||||||
|
assert.deepStrictEqual(result.events.steps, [7, 8, 9]);
|
||||||
|
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 () => {
|
test('auto-run stop errors after step 7 are rethrown immediately instead of restarting', async () => {
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ test('background account history settings are normalized independently from hotm
|
|||||||
extractFunction('normalizeHotmailLocalBaseUrl'),
|
extractFunction('normalizeHotmailLocalBaseUrl'),
|
||||||
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
|
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
|
||||||
extractFunction('normalizeVerificationResendCount'),
|
extractFunction('normalizeVerificationResendCount'),
|
||||||
|
extractFunction('normalizePhoneVerificationReplacementLimit'),
|
||||||
|
extractFunction('normalizePhoneCodeWaitSeconds'),
|
||||||
|
extractFunction('normalizePhoneCodeTimeoutWindows'),
|
||||||
|
extractFunction('normalizePhoneCodePollIntervalSeconds'),
|
||||||
|
extractFunction('normalizePhoneCodePollMaxRounds'),
|
||||||
|
extractFunction('normalizeHeroSmsMaxPrice'),
|
||||||
|
extractFunction('normalizeHeroSmsCountryFallback'),
|
||||||
extractFunction('normalizePersistentSettingValue'),
|
extractFunction('normalizePersistentSettingValue'),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
@@ -63,11 +70,28 @@ const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = DEFAULT_HOTMAIL_LOCAL_BASE_U
|
|||||||
const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
|
const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
|
||||||
const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
|
const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
|
||||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||||
|
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||||||
|
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||||||
|
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||||
|
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||||||
|
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||||||
|
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
|
||||||
|
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||||||
|
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
|
||||||
|
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
|
||||||
|
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
|
||||||
|
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
|
||||||
|
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
|
||||||
|
const PHONE_CODE_POLL_ROUNDS_MIN = 1;
|
||||||
|
const PHONE_CODE_POLL_ROUNDS_MAX = 120;
|
||||||
|
const DEFAULT_PHONE_CODE_POLL_ROUNDS = 4;
|
||||||
const DEFAULT_SUB2API_PROXY_NAME = '';
|
const DEFAULT_SUB2API_PROXY_NAME = '';
|
||||||
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
|
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
|
||||||
const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
|
const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
|
||||||
const VERIFICATION_RESEND_COUNT_MIN = 0;
|
const VERIFICATION_RESEND_COUNT_MIN = 0;
|
||||||
const VERIFICATION_RESEND_COUNT_MAX = 20;
|
const VERIFICATION_RESEND_COUNT_MAX = 20;
|
||||||
|
const HERO_SMS_COUNTRY_ID = 52;
|
||||||
|
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||||
const PERSISTED_SETTING_DEFAULTS = {
|
const PERSISTED_SETTING_DEFAULTS = {
|
||||||
autoStepDelaySeconds: null,
|
autoStepDelaySeconds: null,
|
||||||
mailProvider: '163',
|
mailProvider: '163',
|
||||||
@@ -101,6 +125,18 @@ return {
|
|||||||
assert.equal(api.normalizePersistentSettingValue('phoneVerificationEnabled', 1), true);
|
assert.equal(api.normalizePersistentSettingValue('phoneVerificationEnabled', 1), true);
|
||||||
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '7'), 7);
|
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '7'), 7);
|
||||||
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '-1'), 0);
|
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '-1'), 0);
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('phoneVerificationReplacementLimit', '9'), 9);
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('phoneVerificationReplacementLimit', '-1'), 1);
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('phoneCodeWaitSeconds', '75'), 75);
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('phoneCodeTimeoutWindows', '3'), 3);
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('phoneCodePollIntervalSeconds', '6'), 6);
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('phoneCodePollMaxRounds', '18'), 18);
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0.123456'), '0.1235');
|
||||||
|
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0'), '');
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
api.normalizePersistentSettingValue('heroSmsCountryFallback', [{ id: 16, label: 'United Kingdom' }, { id: 52 }]),
|
||||||
|
[{ id: 16, label: 'United Kingdom' }, { id: 52, label: 'Country #52' }]
|
||||||
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
api.normalizePersistentSettingValue('accountRunHistoryHelperBaseUrl', 'http://127.0.0.1:17373/append-account-log'),
|
api.normalizePersistentSettingValue('accountRunHistoryHelperBaseUrl', 'http://127.0.0.1:17373/append-account-log'),
|
||||||
'http://127.0.0.1:17373'
|
'http://127.0.0.1:17373'
|
||||||
|
|||||||
@@ -573,6 +573,8 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
|
|||||||
" luckmailPreserveTagName: '保留',",
|
" luckmailPreserveTagName: '保留',",
|
||||||
" currentLuckmailPurchase: { token: 'stale' },",
|
" currentLuckmailPurchase: { token: 'stale' },",
|
||||||
" currentLuckmailMailCursor: { messageId: 'stale' },",
|
" currentLuckmailMailCursor: { messageId: 'stale' },",
|
||||||
|
' currentPhoneActivation: null,',
|
||||||
|
' reusablePhoneActivation: null,',
|
||||||
' email: null,',
|
' email: null,',
|
||||||
'};',
|
'};',
|
||||||
'const CONTRIBUTION_RUNTIME_DEFAULTS = {',
|
'const CONTRIBUTION_RUNTIME_DEFAULTS = {',
|
||||||
@@ -618,6 +620,7 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
|
|||||||
" accounts: [{ email: 'saved@example.com' }],",
|
" accounts: [{ email: 'saved@example.com' }],",
|
||||||
" tabRegistry: { foo: { tabId: 1 } },",
|
" tabRegistry: { foo: { tabId: 1 } },",
|
||||||
" sourceLastUrls: { foo: 'https://example.com' },",
|
" sourceLastUrls: { foo: 'https://example.com' },",
|
||||||
|
" reusablePhoneActivation: { activationId: 'rx-001', phoneNumber: '66951112222', provider: 'hero-sms', serviceCode: 'dr', countryId: 52, successfulUses: 1, maxUses: 3 },",
|
||||||
" luckmailApiKey: 'sk-session',",
|
" luckmailApiKey: 'sk-session',",
|
||||||
" luckmailBaseUrl: 'https://demo.example.com/',",
|
" luckmailBaseUrl: 'https://demo.example.com/',",
|
||||||
" luckmailEmailType: 'ms_imap',",
|
" luckmailEmailType: 'ms_imap',",
|
||||||
@@ -659,6 +662,16 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
|
|||||||
assert.equal(snapshot.storedPayload.luckmailPreserveTagName, '保留');
|
assert.equal(snapshot.storedPayload.luckmailPreserveTagName, '保留');
|
||||||
assert.equal(snapshot.storedPayload.currentLuckmailPurchase, null);
|
assert.equal(snapshot.storedPayload.currentLuckmailPurchase, null);
|
||||||
assert.equal(snapshot.storedPayload.currentLuckmailMailCursor, null);
|
assert.equal(snapshot.storedPayload.currentLuckmailMailCursor, null);
|
||||||
|
assert.deepStrictEqual(snapshot.storedPayload.reusablePhoneActivation, {
|
||||||
|
activationId: 'rx-001',
|
||||||
|
phoneNumber: '66951112222',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 52,
|
||||||
|
successfulUses: 1,
|
||||||
|
maxUses: 3,
|
||||||
|
});
|
||||||
|
assert.equal(snapshot.storedPayload.currentPhoneActivation, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleStepData step 10 marks current LuckMail purchase as used and clears runtime state', async () => {
|
test('handleStepData step 10 marks current LuckMail purchase as used and clears runtime state', async () => {
|
||||||
|
|||||||
+1160
-342
File diff suppressed because it is too large
Load Diff
@@ -61,9 +61,6 @@ function createRow(initialDisplay = 'none') {
|
|||||||
test('sidepanel html places cloudflare temp email controls in a standalone section', () => {
|
test('sidepanel html places cloudflare temp email controls in a standalone section', () => {
|
||||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||||
assert.match(html, /id="cloudflare-temp-email-section"/);
|
assert.match(html, /id="cloudflare-temp-email-section"/);
|
||||||
assert.match(html, /id="btn-toggle-cloudflare-temp-email-section"/);
|
|
||||||
assert.match(html, /aria-controls="cloudflare-temp-email-section-body"/);
|
|
||||||
assert.match(html, /id="cloudflare-temp-email-section-body" class="section-collapse-body" hidden/);
|
|
||||||
assert.match(html, /id="btn-cloudflare-temp-email-usage-guide"/);
|
assert.match(html, /id="btn-cloudflare-temp-email-usage-guide"/);
|
||||||
assert.match(html, /id="btn-cloudflare-temp-email-github"/);
|
assert.match(html, /id="btn-cloudflare-temp-email-github"/);
|
||||||
assert.match(html, /id="row-temp-email-random-subdomain-toggle"/);
|
assert.match(html, /id="row-temp-email-random-subdomain-toggle"/);
|
||||||
@@ -71,16 +68,6 @@ test('sidepanel html places cloudflare temp email controls in a standalone secti
|
|||||||
assert.doesNotMatch(html, /id="row-temp-email-random-subdomain-domain"/);
|
assert.doesNotMatch(html, /id="row-temp-email-random-subdomain-domain"/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidepanel persists cloudflare temp email section collapse state', () => {
|
|
||||||
assert.match(source, /CLOUDFLARE_TEMP_EMAIL_SECTION_EXPANDED_STORAGE_KEY = 'multipage-cloudflare-temp-email-section-expanded'/);
|
|
||||||
assert.match(source, /let cloudflareTempEmailSectionExpanded = false/);
|
|
||||||
assert.match(source, /function updateCloudflareTempEmailSectionExpandedUI\(\)/);
|
|
||||||
assert.match(source, /cloudflareTempEmailSectionBody\.hidden = !expanded/);
|
|
||||||
assert.match(source, /btnToggleCloudflareTempEmailSection\.setAttribute\('aria-expanded', String\(expanded\)\)/);
|
|
||||||
assert.match(source, /btnToggleCloudflareTempEmailSection\?\.addEventListener\('click', \(\) => \{\s*toggleCloudflareTempEmailSectionExpanded\(\)/);
|
|
||||||
assert.match(source, /initCloudflareTempEmailSectionExpandedState\(\)/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sidepanel modal message preserves line breaks and supports inline links', () => {
|
test('sidepanel modal message preserves line breaks and supports inline links', () => {
|
||||||
const css = fs.readFileSync('sidepanel/sidepanel.css', 'utf8');
|
const css = fs.readFileSync('sidepanel/sidepanel.css', 'utf8');
|
||||||
assert.match(css, /\.modal-message\s*\{[\s\S]*white-space:\s*pre-line;/);
|
assert.match(css, /\.modal-message\s*\{[\s\S]*white-space:\s*pre-line;/);
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ const inputAutoDelayMinutes = { value: '30' };
|
|||||||
const inputAutoStepDelaySeconds = { value: '10' };
|
const inputAutoStepDelaySeconds = { value: '10' };
|
||||||
const inputVerificationResendCount = { value: '6' };
|
const inputVerificationResendCount = { value: '6' };
|
||||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||||
|
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||||
|
|
||||||
function getCloudflareDomainsFromState() { return { domains: ['example.com'], activeDomain: 'example.com' }; }
|
function getCloudflareDomainsFromState() { return { domains: ['example.com'], activeDomain: 'example.com' }; }
|
||||||
function normalizeCloudflareDomainValue(value) { return String(value || '').trim(); }
|
function normalizeCloudflareDomainValue(value) { return String(value || '').trim(); }
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ const test = require('node:test');
|
|||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
|
|
||||||
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
|
||||||
|
|
||||||
function createAccountPoolUiStub() {
|
function createAccountPoolUiStub() {
|
||||||
return {
|
return {
|
||||||
createAccountPoolFormController({
|
createAccountPoolFormController({
|
||||||
@@ -62,25 +60,11 @@ test('sidepanel loads hotmail manager before sidepanel bootstrap', () => {
|
|||||||
|
|
||||||
test('sidepanel html contains collapsible hotmail form controls', () => {
|
test('sidepanel html contains collapsible hotmail form controls', () => {
|
||||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||||
assert.match(html, /id="btn-toggle-hotmail-section"/);
|
|
||||||
assert.match(html, /aria-controls="hotmail-section-body"/);
|
|
||||||
assert.match(html, /id="hotmail-section-body" class="section-collapse-body" hidden/);
|
|
||||||
assert.match(html, /id="btn-toggle-hotmail-form"/);
|
assert.match(html, /id="btn-toggle-hotmail-form"/);
|
||||||
assert.match(html, /id="hotmail-form-shell"/);
|
assert.match(html, /id="hotmail-form-shell"/);
|
||||||
assert.match(html, /id="btn-import-hotmail-accounts"[^>]*>批量导入</);
|
assert.match(html, /id="btn-import-hotmail-accounts"[^>]*>批量导入</);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidepanel keeps hotmail account pool behind a persisted section collapse', () => {
|
|
||||||
assert.match(sidepanelSource, /HOTMAIL_SECTION_EXPANDED_STORAGE_KEY = 'multipage-hotmail-section-expanded'/);
|
|
||||||
assert.match(sidepanelSource, /let hotmailSectionExpanded = false/);
|
|
||||||
assert.match(sidepanelSource, /function updateHotmailSectionExpandedUI\(\)/);
|
|
||||||
assert.match(sidepanelSource, /hotmailSectionBody\.hidden = !expanded/);
|
|
||||||
assert.match(sidepanelSource, /btnToggleHotmailSection\.setAttribute\('aria-expanded', String\(expanded\)\)/);
|
|
||||||
assert.match(sidepanelSource, /btnToggleHotmailSection\?\.addEventListener\('click', \(\) => \{\s*toggleHotmailSectionExpanded\(\)/);
|
|
||||||
assert.match(sidepanelSource, /btnToggleHotmailForm\?\.addEventListener\('click', \(\) => \{\s*setHotmailSectionExpanded\(true\)/);
|
|
||||||
assert.match(sidepanelSource, /initHotmailSectionExpandedState\(\)/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('hotmail manager exposes a factory and renders empty state', () => {
|
test('hotmail manager exposes a factory and renders empty state', () => {
|
||||||
const source = fs.readFileSync('sidepanel/hotmail-manager.js', 'utf8');
|
const source = fs.readFileSync('sidepanel/hotmail-manager.js', 'utf8');
|
||||||
const windowObject = {
|
const windowObject = {
|
||||||
|
|||||||
@@ -130,6 +130,64 @@ const inputAutoDelayMinutes = { value: '30' };
|
|||||||
const inputAutoStepDelaySeconds = { value: '' };
|
const inputAutoStepDelaySeconds = { value: '' };
|
||||||
const inputVerificationResendCount = { value: '4' };
|
const inputVerificationResendCount = { value: '4' };
|
||||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||||
|
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||||
|
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
|
||||||
|
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
|
||||||
|
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
|
||||||
|
const DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS = 4;
|
||||||
|
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||||||
|
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||||||
|
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||||||
|
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
|
||||||
|
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
|
||||||
|
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
|
||||||
|
const PHONE_CODE_POLL_MAX_ROUNDS_MIN = 1;
|
||||||
|
const PHONE_CODE_POLL_MAX_ROUNDS_MAX = 120;
|
||||||
|
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||||
|
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||||
|
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||||
|
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||||
|
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
|
||||||
|
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||||
|
const inputHeroSmsApiKey = { value: '' };
|
||||||
|
const inputHeroSmsReuseEnabled = { checked: true };
|
||||||
|
const selectHeroSmsAcquirePriority = { value: 'country' };
|
||||||
|
const inputHeroSmsMaxPrice = { value: '' };
|
||||||
|
const inputPhoneReplacementLimit = { value: '3' };
|
||||||
|
const inputPhoneCodeWaitSeconds = { value: '60' };
|
||||||
|
const inputPhoneCodeTimeoutWindows = { value: '2' };
|
||||||
|
const inputPhoneCodePollIntervalSeconds = { value: '5' };
|
||||||
|
const inputPhoneCodePollMaxRounds = { value: '4' };
|
||||||
|
const selectHeroSmsCountry = { value: '52', selectedIndex: 0, options: [{ value: '52', textContent: 'Thailand' }] };
|
||||||
|
function normalizeHeroSmsMaxPriceValue(value = '') { return String(value || '').trim(); }
|
||||||
|
function normalizeHeroSmsReuseEnabledValue(value) { return value === undefined || value === null ? true : Boolean(value); }
|
||||||
|
function normalizeHeroSmsAcquirePriority(value = '') { return String(value || '').trim().toLowerCase() === 'price' ? 'price' : 'country'; }
|
||||||
|
function normalizeHeroSmsCountryId(value) { return Math.max(1, Math.floor(Number(value) || 52)); }
|
||||||
|
function normalizeHeroSmsCountryLabel(value = '') { return String(value || '').trim() || 'Thailand'; }
|
||||||
|
function normalizePhoneVerificationReplacementLimit(value, fallback = 3) {
|
||||||
|
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
function normalizePhoneCodeWaitSecondsValue(value, fallback = 60) {
|
||||||
|
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
function normalizePhoneCodeTimeoutWindowsValue(value, fallback = 2) {
|
||||||
|
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
function normalizePhoneCodePollIntervalSecondsValue(value, fallback = 5) {
|
||||||
|
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
function normalizePhoneCodePollMaxRoundsValue(value, fallback = 12) {
|
||||||
|
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
function getSelectedHeroSmsCountryOption() { return { id: 52, label: 'Thailand' }; }
|
||||||
|
function syncHeroSmsFallbackSelectionOrderFromSelect() { return [{ id: 52, label: 'Thailand' }]; }
|
||||||
|
function getPayPalAccounts() { return []; }
|
||||||
|
function getCurrentPayPalAccount() { return null; }
|
||||||
function getCloudflareDomainsFromState() { return { domains: [], activeDomain: '' }; }
|
function getCloudflareDomainsFromState() { return { domains: [], activeDomain: '' }; }
|
||||||
function normalizeCloudflareDomainValue(value) { return String(value || '').trim(); }
|
function normalizeCloudflareDomainValue(value) { return String(value || '').trim(); }
|
||||||
function getCloudflareTempEmailDomainsFromState() { return { domains: [], activeDomain: '' }; }
|
function getCloudflareTempEmailDomainsFromState() { return { domains: [], activeDomain: '' }; }
|
||||||
@@ -322,9 +380,13 @@ const inputPhoneVerificationEnabled = { checked: false };
|
|||||||
const DEFAULT_PHONE_VERIFICATION_ENABLED = false;
|
const DEFAULT_PHONE_VERIFICATION_ENABLED = false;
|
||||||
const inputHeroSmsApiKey = { value: '' };
|
const inputHeroSmsApiKey = { value: '' };
|
||||||
const inputHeroSmsMaxPrice = { value: '' };
|
const inputHeroSmsMaxPrice = { value: '' };
|
||||||
|
const inputPhoneReplacementLimit = { value: '' };
|
||||||
const selectHeroSmsCountry = { value: '52', options: [{ value: '52' }] };
|
const selectHeroSmsCountry = { value: '52', options: [{ value: '52' }] };
|
||||||
const inputRunCount = { value: '' };
|
const inputRunCount = { value: '' };
|
||||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||||
|
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||||
|
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||||||
|
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||||||
function syncLatestState(state) { latestState = { ...latestState, ...state }; }
|
function syncLatestState(state) { latestState = { ...latestState, ...state }; }
|
||||||
function syncAutoRunState() {}
|
function syncAutoRunState() {}
|
||||||
function syncPasswordField() {}
|
function syncPasswordField() {}
|
||||||
@@ -348,7 +410,12 @@ function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) ||
|
|||||||
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
|
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
|
||||||
function formatAutoStepDelayInputValue(value) { return value == null ? '' : String(value); }
|
function formatAutoStepDelayInputValue(value) { return value == null ? '' : String(value); }
|
||||||
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
|
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
|
||||||
function normalizeHeroSmsMaxPriceValue(value) { return String(value ?? '').trim(); }
|
function normalizeHeroSmsMaxPriceValue(value = '') { return String(value || '').trim(); }
|
||||||
|
function normalizePhoneVerificationReplacementLimit(value, fallback = 3) {
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (!Number.isFinite(numeric)) return fallback;
|
||||||
|
return Math.max(PHONE_REPLACEMENT_LIMIT_MIN, Math.min(PHONE_REPLACEMENT_LIMIT_MAX, Math.floor(numeric)));
|
||||||
|
}
|
||||||
function normalizeHeroSmsCountryId() { return 52; }
|
function normalizeHeroSmsCountryId() { return 52; }
|
||||||
function getSelectedHeroSmsCountryOption() { return { label: 'Thailand' }; }
|
function getSelectedHeroSmsCountryOption() { return { label: 'Thailand' }; }
|
||||||
function updateHeroSmsPlatformDisplay() {}
|
function updateHeroSmsPlatformDisplay() {}
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ const inputAutoDelayMinutes = { value: '30' };
|
|||||||
const inputAutoStepDelaySeconds = { value: '' };
|
const inputAutoStepDelaySeconds = { value: '' };
|
||||||
const inputVerificationResendCount = { value: '4' };
|
const inputVerificationResendCount = { value: '4' };
|
||||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||||
|
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||||
function getCloudflareDomainsFromState() {
|
function getCloudflareDomainsFromState() {
|
||||||
return { domains: [], activeDomain: '' };
|
return { domains: [], activeDomain: '' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const {
|
|||||||
} = require('../mail-provider-utils');
|
} = require('../mail-provider-utils');
|
||||||
|
|
||||||
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||||
const ipProxyPanelSource = fs.readFileSync('sidepanel/ip-proxy-panel.js', 'utf8');
|
|
||||||
|
|
||||||
function extractFunction(name) {
|
function extractFunction(name) {
|
||||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||||
@@ -57,78 +56,125 @@ test('sidepanel html exposes phone verification toggle and dedicated HeroSMS row
|
|||||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||||
|
|
||||||
assert.match(html, /id="row-phone-verification-enabled"/);
|
assert.match(html, /id="row-phone-verification-enabled"/);
|
||||||
|
assert.match(html, /id="btn-toggle-phone-verification-section"/);
|
||||||
|
assert.match(html, /id="row-phone-verification-fold"/);
|
||||||
assert.match(html, /id="input-phone-verification-enabled"/);
|
assert.match(html, /id="input-phone-verification-enabled"/);
|
||||||
assert.match(html, /id="ip-proxy-section"/);
|
|
||||||
assert.match(html, /id="row-ip-proxy-enabled"/);
|
|
||||||
assert.match(html, /id="input-ip-proxy-enabled"/);
|
|
||||||
assert.match(html, /id="row-hero-sms-platform"/);
|
assert.match(html, /id="row-hero-sms-platform"/);
|
||||||
assert.match(html, /id="row-hero-sms-country"/);
|
assert.match(html, /id="row-hero-sms-country"/);
|
||||||
assert.match(html, /id="row-hero-sms-max-price"/);
|
assert.match(html, /id="row-hero-sms-country-fallback"/);
|
||||||
|
assert.match(html, /id="row-hero-sms-acquire-priority"/);
|
||||||
|
assert.match(html, /id="select-hero-sms-acquire-priority"/);
|
||||||
|
assert.match(html, /id="select-hero-sms-country"[^>]*multiple/);
|
||||||
|
assert.doesNotMatch(html, /id="select-hero-sms-country-fallback"/);
|
||||||
assert.match(html, /id="row-hero-sms-api-key"/);
|
assert.match(html, /id="row-hero-sms-api-key"/);
|
||||||
|
assert.match(html, /id="row-hero-sms-max-price"/);
|
||||||
|
assert.match(html, /id="row-hero-sms-current-number"/);
|
||||||
|
assert.match(html, /id="row-hero-sms-price-tiers"/);
|
||||||
|
assert.match(html, /id="row-hero-sms-current-code"/);
|
||||||
|
assert.match(html, /id="row-phone-replacement-limit"/);
|
||||||
|
assert.match(html, /id="row-phone-verification-resend-count"/);
|
||||||
|
assert.match(html, /id="row-phone-code-wait-seconds"/);
|
||||||
|
assert.match(html, /id="row-phone-code-timeout-windows"/);
|
||||||
|
assert.match(html, /id="row-phone-code-poll-interval-seconds"/);
|
||||||
|
assert.match(html, /id="row-phone-code-poll-max-rounds"/);
|
||||||
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
|
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidepanel renders IP proxy as a standalone card after sms verification without proxy status chrome', () => {
|
|
||||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
|
||||||
const phoneToggleIndex = html.indexOf('id="row-phone-verification-enabled"');
|
|
||||||
const ipProxySectionIndex = html.indexOf('id="ip-proxy-section"');
|
|
||||||
const ipProxyToggleIndex = html.indexOf('id="row-ip-proxy-enabled"');
|
|
||||||
const cloudflareSectionIndex = html.indexOf('id="cloudflare-temp-email-section"');
|
|
||||||
|
|
||||||
assert.match(html, /id="ip-proxy-section" class="data-card ip-proxy-card"/);
|
|
||||||
assert.match(html, /id="btn-toggle-ip-proxy-section"/);
|
|
||||||
assert.match(html, /aria-controls="row-ip-proxy-fold"/);
|
|
||||||
assert.match(html, />展开设置<\/button>/);
|
|
||||||
assert.ok(phoneToggleIndex >= 0);
|
|
||||||
assert.ok(ipProxySectionIndex > phoneToggleIndex);
|
|
||||||
assert.ok(ipProxyToggleIndex > phoneToggleIndex);
|
|
||||||
assert.ok(cloudflareSectionIndex > ipProxySectionIndex);
|
|
||||||
assert.doesNotMatch(html, /id="ip-proxy-enabled-status"/);
|
|
||||||
assert.doesNotMatch(html, /id="row-ip-proxy-runtime-status"/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('IP proxy standalone card supports persisted collapse control', () => {
|
|
||||||
assert.match(ipProxyPanelSource, /IP_PROXY_SECTION_EXPANDED_STORAGE_KEY = 'multipage-ip-proxy-section-expanded'/);
|
|
||||||
assert.match(ipProxyPanelSource, /let ipProxySectionExpanded = false/);
|
|
||||||
assert.match(ipProxyPanelSource, /const showSettings = enabled && ipProxySectionExpanded/);
|
|
||||||
assert.match(ipProxyPanelSource, /rowIpProxyFold\.style\.display = showSettings \? '' : 'none'/);
|
|
||||||
assert.match(ipProxyPanelSource, /btnToggleIpProxySection\.setAttribute\('aria-expanded', String\(showSettings\)\)/);
|
|
||||||
assert.match(sidepanelSource, /btnToggleIpProxySection\?\.addEventListener\('click', \(\) => \{\s*if \(typeof toggleIpProxySectionExpanded === 'function'\)/);
|
|
||||||
assert.match(sidepanelSource, /initIpProxySectionExpandedState\(\)/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('updatePhoneVerificationSettingsUI toggles HeroSMS rows from the sms switch', () => {
|
test('updatePhoneVerificationSettingsUI toggles HeroSMS rows from the sms switch', () => {
|
||||||
const api = new Function(`
|
const api = new Function(`
|
||||||
|
const phoneVerificationSectionExpanded = true;
|
||||||
const inputPhoneVerificationEnabled = { checked: false };
|
const inputPhoneVerificationEnabled = { checked: false };
|
||||||
|
const rowPhoneVerificationEnabled = { style: { display: 'none' } };
|
||||||
|
const rowPhoneVerificationFold = { style: { display: 'none' } };
|
||||||
|
const btnTogglePhoneVerificationSection = {
|
||||||
|
disabled: false,
|
||||||
|
textContent: '',
|
||||||
|
title: '',
|
||||||
|
setAttribute: () => {},
|
||||||
|
};
|
||||||
const rowHeroSmsPlatform = { style: { display: 'none' } };
|
const rowHeroSmsPlatform = { style: { display: 'none' } };
|
||||||
const rowHeroSmsCountry = { style: { display: 'none' } };
|
const rowHeroSmsCountry = { style: { display: 'none' } };
|
||||||
const rowHeroSmsMaxPrice = { style: { display: 'none' } };
|
const rowHeroSmsCountryFallback = { style: { display: 'none' } };
|
||||||
|
const rowHeroSmsAcquirePriority = { style: { display: 'none' } };
|
||||||
const rowHeroSmsApiKey = { style: { display: 'none' } };
|
const rowHeroSmsApiKey = { style: { display: 'none' } };
|
||||||
|
const rowHeroSmsMaxPrice = { style: { display: 'none' } };
|
||||||
|
const rowHeroSmsCurrentNumber = { style: { display: 'none' } };
|
||||||
|
const rowHeroSmsPriceTiers = { style: { display: 'none' } };
|
||||||
|
const rowHeroSmsCurrentCode = { style: { display: 'none' } };
|
||||||
|
const rowPhoneVerificationResendCount = { style: { display: 'none' } };
|
||||||
|
const rowPhoneReplacementLimit = { style: { display: 'none' } };
|
||||||
|
const rowPhoneCodeWaitSeconds = { style: { display: 'none' } };
|
||||||
|
const rowPhoneCodeTimeoutWindows = { style: { display: 'none' } };
|
||||||
|
const rowPhoneCodePollIntervalSeconds = { style: { display: 'none' } };
|
||||||
|
const rowPhoneCodePollMaxRounds = { style: { display: 'none' } };
|
||||||
|
|
||||||
${extractFunction('updatePhoneVerificationSettingsUI')}
|
${extractFunction('updatePhoneVerificationSettingsUI')}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
rowPhoneVerificationEnabled,
|
||||||
|
rowPhoneVerificationFold,
|
||||||
|
btnTogglePhoneVerificationSection,
|
||||||
inputPhoneVerificationEnabled,
|
inputPhoneVerificationEnabled,
|
||||||
rowHeroSmsPlatform,
|
rowHeroSmsPlatform,
|
||||||
rowHeroSmsCountry,
|
rowHeroSmsCountry,
|
||||||
rowHeroSmsMaxPrice,
|
rowHeroSmsCountryFallback,
|
||||||
|
rowHeroSmsAcquirePriority,
|
||||||
rowHeroSmsApiKey,
|
rowHeroSmsApiKey,
|
||||||
|
rowHeroSmsMaxPrice,
|
||||||
|
rowHeroSmsCurrentNumber,
|
||||||
|
rowHeroSmsPriceTiers,
|
||||||
|
rowHeroSmsCurrentCode,
|
||||||
|
rowPhoneVerificationResendCount,
|
||||||
|
rowPhoneReplacementLimit,
|
||||||
|
rowPhoneCodeWaitSeconds,
|
||||||
|
rowPhoneCodeTimeoutWindows,
|
||||||
|
rowPhoneCodePollIntervalSeconds,
|
||||||
|
rowPhoneCodePollMaxRounds,
|
||||||
updatePhoneVerificationSettingsUI,
|
updatePhoneVerificationSettingsUI,
|
||||||
};
|
};
|
||||||
`)();
|
`)();
|
||||||
|
|
||||||
api.updatePhoneVerificationSettingsUI();
|
api.updatePhoneVerificationSettingsUI();
|
||||||
|
assert.equal(api.rowPhoneVerificationEnabled.style.display, '');
|
||||||
|
assert.equal(api.rowPhoneVerificationFold.style.display, 'none');
|
||||||
|
assert.equal(api.btnTogglePhoneVerificationSection.disabled, true);
|
||||||
|
assert.equal(api.btnTogglePhoneVerificationSection.textContent, '展开设置');
|
||||||
assert.equal(api.rowHeroSmsPlatform.style.display, 'none');
|
assert.equal(api.rowHeroSmsPlatform.style.display, 'none');
|
||||||
assert.equal(api.rowHeroSmsCountry.style.display, 'none');
|
assert.equal(api.rowHeroSmsCountry.style.display, 'none');
|
||||||
assert.equal(api.rowHeroSmsMaxPrice.style.display, 'none');
|
assert.equal(api.rowHeroSmsCountryFallback.style.display, 'none');
|
||||||
|
assert.equal(api.rowHeroSmsAcquirePriority.style.display, 'none');
|
||||||
assert.equal(api.rowHeroSmsApiKey.style.display, 'none');
|
assert.equal(api.rowHeroSmsApiKey.style.display, 'none');
|
||||||
|
assert.equal(api.rowHeroSmsMaxPrice.style.display, 'none');
|
||||||
|
assert.equal(api.rowHeroSmsCurrentNumber.style.display, 'none');
|
||||||
|
assert.equal(api.rowHeroSmsPriceTiers.style.display, 'none');
|
||||||
|
assert.equal(api.rowHeroSmsCurrentCode.style.display, 'none');
|
||||||
|
assert.equal(api.rowPhoneVerificationResendCount.style.display, 'none');
|
||||||
|
assert.equal(api.rowPhoneReplacementLimit.style.display, 'none');
|
||||||
|
assert.equal(api.rowPhoneCodeWaitSeconds.style.display, 'none');
|
||||||
|
assert.equal(api.rowPhoneCodeTimeoutWindows.style.display, 'none');
|
||||||
|
assert.equal(api.rowPhoneCodePollIntervalSeconds.style.display, 'none');
|
||||||
|
assert.equal(api.rowPhoneCodePollMaxRounds.style.display, 'none');
|
||||||
|
|
||||||
api.inputPhoneVerificationEnabled.checked = true;
|
api.inputPhoneVerificationEnabled.checked = true;
|
||||||
api.updatePhoneVerificationSettingsUI();
|
api.updatePhoneVerificationSettingsUI();
|
||||||
|
assert.equal(api.rowPhoneVerificationFold.style.display, '');
|
||||||
|
assert.equal(api.btnTogglePhoneVerificationSection.disabled, false);
|
||||||
|
assert.equal(api.btnTogglePhoneVerificationSection.textContent, '收起设置');
|
||||||
assert.equal(api.rowHeroSmsPlatform.style.display, '');
|
assert.equal(api.rowHeroSmsPlatform.style.display, '');
|
||||||
assert.equal(api.rowHeroSmsCountry.style.display, '');
|
assert.equal(api.rowHeroSmsCountry.style.display, '');
|
||||||
assert.equal(api.rowHeroSmsMaxPrice.style.display, '');
|
assert.equal(api.rowHeroSmsCountryFallback.style.display, '');
|
||||||
|
assert.equal(api.rowHeroSmsAcquirePriority.style.display, '');
|
||||||
assert.equal(api.rowHeroSmsApiKey.style.display, '');
|
assert.equal(api.rowHeroSmsApiKey.style.display, '');
|
||||||
|
assert.equal(api.rowHeroSmsMaxPrice.style.display, '');
|
||||||
|
assert.equal(api.rowHeroSmsCurrentNumber.style.display, '');
|
||||||
|
assert.equal(api.rowHeroSmsPriceTiers.style.display, 'none');
|
||||||
|
assert.equal(api.rowHeroSmsCurrentCode.style.display, '');
|
||||||
|
assert.equal(api.rowPhoneVerificationResendCount.style.display, '');
|
||||||
|
assert.equal(api.rowPhoneReplacementLimit.style.display, '');
|
||||||
|
assert.equal(api.rowPhoneCodeWaitSeconds.style.display, '');
|
||||||
|
assert.equal(api.rowPhoneCodeTimeoutWindows.style.display, '');
|
||||||
|
assert.equal(api.rowPhoneCodePollIntervalSeconds.style.display, '');
|
||||||
|
assert.equal(api.rowPhoneCodePollMaxRounds.style.display, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('collectSettingsPayload keeps local helper sync enabled while persisting sms toggle state', () => {
|
test('collectSettingsPayload keeps local helper sync enabled while persisting sms toggle state', () => {
|
||||||
@@ -179,9 +225,35 @@ const inputAutoStepDelaySeconds = { value: '' };
|
|||||||
const inputPhoneVerificationEnabled = { checked: true };
|
const inputPhoneVerificationEnabled = { checked: true };
|
||||||
const inputVerificationResendCount = { value: '4' };
|
const inputVerificationResendCount = { value: '4' };
|
||||||
const inputHeroSmsApiKey = { value: 'demo-key' };
|
const inputHeroSmsApiKey = { value: 'demo-key' };
|
||||||
const inputHeroSmsMaxPrice = { value: '0.08' };
|
const inputHeroSmsReuseEnabled = { checked: true };
|
||||||
|
const selectHeroSmsAcquirePriority = { value: 'price' };
|
||||||
|
const inputHeroSmsMaxPrice = { value: '0.12' };
|
||||||
|
const inputPhoneReplacementLimit = { value: '5' };
|
||||||
|
const inputPhoneCodeWaitSeconds = { value: '75' };
|
||||||
|
const inputPhoneCodeTimeoutWindows = { value: '3' };
|
||||||
|
const inputPhoneCodePollIntervalSeconds = { value: '6' };
|
||||||
|
const inputPhoneCodePollMaxRounds = { value: '18' };
|
||||||
const inputAccountRunHistoryHelperBaseUrl = { value: 'http://127.0.0.1:17373' };
|
const inputAccountRunHistoryHelperBaseUrl = { value: 'http://127.0.0.1:17373' };
|
||||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||||
|
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||||
|
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
|
||||||
|
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
|
||||||
|
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
|
||||||
|
const DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS = 4;
|
||||||
|
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||||||
|
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||||||
|
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||||||
|
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
|
||||||
|
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
|
||||||
|
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
|
||||||
|
const PHONE_CODE_POLL_MAX_ROUNDS_MIN = 1;
|
||||||
|
const PHONE_CODE_POLL_MAX_ROUNDS_MAX = 120;
|
||||||
|
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||||
|
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||||
|
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||||
|
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||||
|
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||||||
|
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||||||
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
|
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
|
||||||
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||||
const selectHeroSmsCountry = {
|
const selectHeroSmsCountry = {
|
||||||
@@ -206,10 +278,20 @@ function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) ||
|
|||||||
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
|
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
|
||||||
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
|
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
|
||||||
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
|
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
|
||||||
|
${extractFunction('normalizeHeroSmsMaxPriceValue')}
|
||||||
|
${extractFunction('normalizePhoneVerificationReplacementLimit')}
|
||||||
|
${extractFunction('normalizePhoneCodeWaitSecondsValue')}
|
||||||
|
${extractFunction('normalizePhoneCodeTimeoutWindowsValue')}
|
||||||
|
${extractFunction('normalizePhoneCodePollIntervalSecondsValue')}
|
||||||
|
${extractFunction('normalizePhoneCodePollMaxRoundsValue')}
|
||||||
|
${extractFunction('normalizeHeroSmsReuseEnabledValue')}
|
||||||
|
${extractFunction('normalizeHeroSmsAcquirePriority')}
|
||||||
${extractFunction('normalizeHeroSmsCountryId')}
|
${extractFunction('normalizeHeroSmsCountryId')}
|
||||||
${extractFunction('normalizeHeroSmsCountryLabel')}
|
${extractFunction('normalizeHeroSmsCountryLabel')}
|
||||||
${extractFunction('normalizeHeroSmsMaxPriceValue')}
|
|
||||||
${extractFunction('getSelectedHeroSmsCountryOption')}
|
${extractFunction('getSelectedHeroSmsCountryOption')}
|
||||||
|
function syncHeroSmsFallbackSelectionOrderFromSelect() {
|
||||||
|
return [{ id: 52, label: 'Thailand' }, { id: 16, label: 'United Kingdom' }];
|
||||||
|
}
|
||||||
${extractFunction('collectSettingsPayload')}
|
${extractFunction('collectSettingsPayload')}
|
||||||
return { collectSettingsPayload };
|
return { collectSettingsPayload };
|
||||||
`)(normalizeIcloudTargetMailboxType, normalizeIcloudForwardMailProvider);
|
`)(normalizeIcloudTargetMailboxType, normalizeIcloudForwardMailProvider);
|
||||||
@@ -220,7 +302,15 @@ return { collectSettingsPayload };
|
|||||||
assert.equal(payload.accountRunHistoryTextEnabled, true);
|
assert.equal(payload.accountRunHistoryTextEnabled, true);
|
||||||
assert.equal(payload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
|
assert.equal(payload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
|
||||||
assert.equal(payload.heroSmsApiKey, 'demo-key');
|
assert.equal(payload.heroSmsApiKey, 'demo-key');
|
||||||
assert.equal(payload.heroSmsMaxPrice, '0.08');
|
assert.equal(payload.heroSmsReuseEnabled, true);
|
||||||
|
assert.equal(payload.heroSmsAcquirePriority, 'price');
|
||||||
|
assert.equal(payload.heroSmsMaxPrice, '0.12');
|
||||||
|
assert.equal(payload.phoneVerificationReplacementLimit, 5);
|
||||||
|
assert.equal(payload.phoneCodeWaitSeconds, 75);
|
||||||
|
assert.equal(payload.phoneCodeTimeoutWindows, 3);
|
||||||
|
assert.equal(payload.phoneCodePollIntervalSeconds, 6);
|
||||||
|
assert.equal(payload.phoneCodePollMaxRounds, 18);
|
||||||
assert.equal(payload.heroSmsCountryId, 52);
|
assert.equal(payload.heroSmsCountryId, 52);
|
||||||
assert.equal(payload.heroSmsCountryLabel, 'Thailand');
|
assert.equal(payload.heroSmsCountryLabel, 'Thailand');
|
||||||
|
assert.deepStrictEqual(payload.heroSmsCountryFallback, [{ id: 16, label: 'United Kingdom' }]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const step9ModuleSource = fs.readFileSync('background/steps/confirm-oauth.js', 'utf8');
|
||||||
|
|
||||||
|
const api = new Function('step9ModuleSource', `
|
||||||
|
const self = {};
|
||||||
|
let webNavListener = null;
|
||||||
|
let webNavCommittedListener = null;
|
||||||
|
let step8TabUpdatedListener = null;
|
||||||
|
let step8PendingReject = null;
|
||||||
|
let cleanupCalls = 0;
|
||||||
|
let timeoutCalls = 0;
|
||||||
|
let recoveryCalls = 0;
|
||||||
|
let completePayload = null;
|
||||||
|
const logs = [];
|
||||||
|
const callbackUrl = 'http://localhost:1455/auth/callback?code=abc&state=xyz';
|
||||||
|
|
||||||
|
const chrome = {
|
||||||
|
webNavigation: {
|
||||||
|
onBeforeNavigate: {
|
||||||
|
addListener(listener) {
|
||||||
|
webNavListener = listener;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof webNavListener === 'function') {
|
||||||
|
webNavListener({ tabId: 123, url: callbackUrl });
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
removeListener() {},
|
||||||
|
},
|
||||||
|
onCommitted: {
|
||||||
|
addListener(listener) {
|
||||||
|
webNavCommittedListener = listener;
|
||||||
|
},
|
||||||
|
removeListener() {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
onUpdated: {
|
||||||
|
addListener(listener) {
|
||||||
|
step8TabUpdatedListener = listener;
|
||||||
|
},
|
||||||
|
removeListener() {},
|
||||||
|
},
|
||||||
|
async update() {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function cleanupStep8NavigationListeners() {
|
||||||
|
cleanupCalls += 1;
|
||||||
|
webNavListener = null;
|
||||||
|
webNavCommittedListener = null;
|
||||||
|
step8TabUpdatedListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addLog(message) {
|
||||||
|
logs.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function throwIfStep8SettledOrStopped() {}
|
||||||
|
|
||||||
|
async function getTabId() {
|
||||||
|
return 123;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isTabAlive() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureStep8SignupPageReady() {}
|
||||||
|
|
||||||
|
async function waitForStep8Ready() {
|
||||||
|
return {
|
||||||
|
consentReady: true,
|
||||||
|
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerStep8ContentStrategy() {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForStep8ClickEffect() {
|
||||||
|
return {
|
||||||
|
progressed: true,
|
||||||
|
reason: 'url_changed',
|
||||||
|
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) {
|
||||||
|
if (options.actionLabel === 'OAuth localhost 回调') {
|
||||||
|
timeoutCalls += 1;
|
||||||
|
if (timeoutCalls === 1) {
|
||||||
|
throw new Error('步骤 9:从拿到 OAuth 登录地址开始,5 分钟内未完成OAuth localhost 回调,结束当前链路,准备从步骤 7 重新开始。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultTimeoutMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recoverOAuthLocalhostTimeout(details = {}) {
|
||||||
|
recoveryCalls += 1;
|
||||||
|
return {
|
||||||
|
...(details.state || {}),
|
||||||
|
oauthUrl: 'https://auth.openai.com/recovered-oauth',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStep8CallbackUrlFromNavigation(details, signupTabId) {
|
||||||
|
if (
|
||||||
|
Number(signupTabId) === Number(details?.tabId)
|
||||||
|
&& String(details?.url || '').includes('http://localhost:1455/auth/callback')
|
||||||
|
) {
|
||||||
|
return details.url;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStep8CallbackUrlFromTabUpdate() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStep8EffectLabel() {
|
||||||
|
return 'URL 已变化';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareStep8DebuggerClick() {
|
||||||
|
return { rect: { centerX: 10, centerY: 10 } };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickWithDebugger() {}
|
||||||
|
async function reloadStep8ConsentPage() {}
|
||||||
|
async function reuseOrCreateTab() { return 123; }
|
||||||
|
async function sleepWithStop() {}
|
||||||
|
|
||||||
|
function setWebNavListener(listener) { webNavListener = listener; }
|
||||||
|
function getWebNavListener() { return webNavListener; }
|
||||||
|
function setWebNavCommittedListener(listener) { webNavCommittedListener = listener; }
|
||||||
|
function getWebNavCommittedListener() { return webNavCommittedListener; }
|
||||||
|
function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listener; }
|
||||||
|
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
|
||||||
|
function setStep8PendingReject(handler) { step8PendingReject = handler; }
|
||||||
|
|
||||||
|
async function completeStepFromBackground(step, payload) {
|
||||||
|
completePayload = { step, payload };
|
||||||
|
}
|
||||||
|
|
||||||
|
const STEP8_CLICK_RETRY_DELAY_MS = 200;
|
||||||
|
const STEP8_READY_WAIT_TIMEOUT_MS = 30000;
|
||||||
|
const STEP8_MAX_ROUNDS = 2;
|
||||||
|
const STEP8_STRATEGIES = [
|
||||||
|
{ mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
|
||||||
|
{ mode: 'debugger', label: 'debugger click' },
|
||||||
|
];
|
||||||
|
|
||||||
|
${step9ModuleSource}
|
||||||
|
|
||||||
|
const executor = self.MultiPageBackgroundStep9.createStep9Executor({
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
cleanupStep8NavigationListeners,
|
||||||
|
clickWithDebugger,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureStep8SignupPageReady,
|
||||||
|
getOAuthFlowStepTimeoutMs,
|
||||||
|
getStep8CallbackUrlFromNavigation,
|
||||||
|
getStep8CallbackUrlFromTabUpdate,
|
||||||
|
getStep8EffectLabel,
|
||||||
|
getTabId,
|
||||||
|
getWebNavCommittedListener,
|
||||||
|
getWebNavListener,
|
||||||
|
getStep8TabUpdatedListener,
|
||||||
|
isTabAlive,
|
||||||
|
prepareStep8DebuggerClick,
|
||||||
|
recoverOAuthLocalhostTimeout,
|
||||||
|
reloadStep8ConsentPage,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
setStep8PendingReject,
|
||||||
|
setStep8TabUpdatedListener,
|
||||||
|
setWebNavCommittedListener,
|
||||||
|
setWebNavListener,
|
||||||
|
sleepWithStop,
|
||||||
|
STEP8_CLICK_RETRY_DELAY_MS,
|
||||||
|
STEP8_MAX_ROUNDS,
|
||||||
|
STEP8_READY_WAIT_TIMEOUT_MS,
|
||||||
|
STEP8_STRATEGIES,
|
||||||
|
throwIfStep8SettledOrStopped,
|
||||||
|
triggerStep8ContentStrategy,
|
||||||
|
waitForStep8ClickEffect,
|
||||||
|
waitForStep8Ready,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
executeStep9: executor.executeStep9,
|
||||||
|
snapshot() {
|
||||||
|
return {
|
||||||
|
cleanupCalls,
|
||||||
|
timeoutCalls,
|
||||||
|
recoveryCalls,
|
||||||
|
completePayload,
|
||||||
|
hasPendingReject: Boolean(step8PendingReject),
|
||||||
|
logs,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`)(step9ModuleSource);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await api.executeStep9({
|
||||||
|
oauthUrl: 'https://auth.openai.com/original-oauth',
|
||||||
|
visibleStep: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = api.snapshot();
|
||||||
|
assert.strictEqual(snapshot.timeoutCalls, 2, 'step9 should retry timeout budget check after recovery');
|
||||||
|
assert.strictEqual(snapshot.recoveryCalls, 1, 'step9 should call timeout recovery hook exactly once');
|
||||||
|
assert.strictEqual(snapshot.cleanupCalls >= 1, true, 'step9 should cleanup navigation listeners');
|
||||||
|
assert.strictEqual(snapshot.hasPendingReject, false, 'step9 should clear pending reject after completion');
|
||||||
|
assert.deepStrictEqual(snapshot.completePayload, {
|
||||||
|
step: 9,
|
||||||
|
payload: {
|
||||||
|
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('step9 timeout recovery tests passed');
|
||||||
|
})().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user