feat(flow): stabilize step7-9, expand HeroSMS, and update usage tutorial

This commit is contained in:
daniellee2015
2026-04-29 00:51:05 +08:00
parent 9e26b16f20
commit 4c091a3d32
21 changed files with 5265 additions and 1021 deletions
+346 -1
View File
@@ -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
+33 -8
View File
@@ -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}:已重新打开认证页,正在准备调试器点击...`);
} }
+27 -1
View File
@@ -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) {
+119
View File
@@ -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
View File
@@ -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`)已经开启。
![2026 04 25 003703](https://apikey.qzz.io/content-assets/library/2026/04/20260424-163745--2026-04-25-003703--21e892504b82.png) ![2026 04 25 003703](https://apikey.qzz.io/content-assets/library/2026/04/20260424-163745--2026-04-25-003703--21e892504b82.png)
### 第十部分:订阅节点与自建推荐 ### 第十部分:订阅节点与自建推荐
如果你还没有订阅节点或想要寻找稳定、便宜的科学上网方式,可以参考以下三种方案获取。 如果你还没有订阅节点或想要寻找稳定、便宜的科学上网方式,可以参考以下三种方案获取。
+13 -23
View File
@@ -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
View File
@@ -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
View File
@@ -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----刷新令牌&#10;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----刷新令牌&#10;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
View File
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -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'
+13
View File
@@ -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 () => {
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(); }
-16
View File
@@ -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 = {
+68 -1
View File
@@ -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' }]);
}); });
+231
View File
@@ -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);
});