fix: resolve CPA auth hardening conflicts with dev

This commit is contained in:
QLHazyCoder
2026-04-29 12:24:15 +08:00
45 changed files with 5804 additions and 1289 deletions
+11
View File
@@ -14,6 +14,7 @@
cancelPendingCommands,
clearStopRequest,
createAutoRunSessionId,
ensureHotmailMailboxReadyForAutoRunRound,
getAutoRunStatusPayload,
getErrorMessage,
getFirstUnfinishedStep,
@@ -391,6 +392,7 @@
inbucketMailbox: prevState.inbucketMailbox,
cloudflareDomain: prevState.cloudflareDomain,
cloudflareDomains: prevState.cloudflareDomains,
reusablePhoneActivation: prevState.reusablePhoneActivation,
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
autoRunSessionId: sessionId,
tabRegistry: {},
@@ -439,6 +441,15 @@
sessionId,
});
if (!useExistingProgress && startStep === 1 && typeof ensureHotmailMailboxReadyForAutoRunRound === 'function') {
await ensureHotmailMailboxReadyForAutoRunRound({
targetRun,
totalRuns,
attemptRun,
sessionId,
});
}
await runAutoSequenceFromStep(startStep, {
targetRun,
totalRuns,
+10 -10
View File
@@ -537,6 +537,16 @@
}
}
if (typeof ensureContentScriptReadyOnTab === 'function') {
await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, tabId, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
timeoutMs: 20000,
retryDelayMs: 800,
logMessage: '步骤 0:2925 登录页内容脚本未就绪,正在等待页面稳定后继续登录...',
});
}
if (!forceRelogin && !isMail2925LoginUrl(openedUrl) && !normalizedExpectedMailboxEmail) {
await addLog('2925:当前邮箱页未跳转到登录页,将直接复用已登录会话。', 'info');
return buildSuccessPayload();
@@ -553,16 +563,6 @@
});
}
if (typeof ensureContentScriptReadyOnTab === 'function') {
await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, tabId, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
timeoutMs: 20000,
retryDelayMs: 800,
logMessage: '步骤 0:2925 登录页内容脚本未就绪,正在等待页面稳定后继续登录...',
});
}
if (forceRelogin && typeof sleepWithStop === 'function') {
await addLog('2925:登录页已打开,等待 3 秒后开始检查输入框并执行登录...', 'info');
await sleepWithStop(3000);
+17 -15
View File
@@ -32,11 +32,14 @@
executeStepViaCompletionSignal,
exportSettingsBundle,
fetchGeneratedEmail,
finalizePhoneActivationAfterSuccessfulFlow,
finalizeStep3Completion,
finalizeIcloudAliasAfterSuccessfulFlow,
findHotmailAccount,
findPayPalAccount,
flushCommand,
getCurrentLuckmailPurchase,
getCurrentPayPalAccount,
getCurrentMail2925Account,
getPendingAutoRunTimerPlan,
getSourceLabel,
@@ -62,6 +65,7 @@
refreshIpProxyPool,
normalizeHotmailAccounts,
normalizeMail2925Accounts,
normalizePayPalAccounts,
normalizeRunCount,
AUTO_RUN_TIMER_KIND_SCHEDULED_START,
notifyStepComplete,
@@ -79,6 +83,7 @@
selectLuckmailPurchase,
switchIpProxy,
changeIpProxyExit,
setCurrentPayPalAccount,
setCurrentMail2925Account,
setCurrentHotmailAccount,
setContributionMode,
@@ -99,7 +104,9 @@
deleteMail2925Account,
deleteMail2925Accounts,
syncHotmailAccounts,
syncPayPalAccounts,
testHotmailAccountMailAccess,
upsertPayPalAccount,
upsertMail2925Account,
upsertHotmailAccount,
verifyHotmailAccount,
@@ -135,20 +142,6 @@
}
}
<<<<<<< HEAD
function resolveLastStepIdForState(state = {}) {
if (typeof getLastStepIdForState === 'function') {
const fromResolver = Number(getLastStepIdForState(state));
if (Number.isFinite(fromResolver) && fromResolver > 0) {
return fromResolver;
}
}
const stepIds = typeof getStepIdsForState === 'function'
? (getStepIdsForState(state) || [])
: Object.keys(state?.stepStatuses || {}).map((step) => Number(step)).filter(Number.isFinite);
const sorted = stepIds.slice().sort((left, right) => left - right);
return Math.max(10, sorted[sorted.length - 1] || 0);
=======
function getStepKeyForState(step, state = {}) {
if (typeof getStepDefinitionForState === 'function') {
return String(getStepDefinitionForState(step, state)?.key || '').trim();
@@ -220,7 +213,6 @@
if (typeof finalizePhoneActivationAfterSuccessfulFlow === 'function') {
await finalizePhoneActivationAfterSuccessfulFlow(latestState);
}
>>>>>>> 705c749 (fix(flow): restore dynamic router handling and downstream reset safety)
}
async function handleStepData(step, payload) {
@@ -800,6 +792,16 @@
return { ok: true, account };
}
case 'UPSERT_PAYPAL_ACCOUNT': {
const account = await upsertPayPalAccount(message.payload || {});
return { ok: true, account };
}
case 'SELECT_PAYPAL_ACCOUNT': {
const account = await setCurrentPayPalAccount(String(message.payload?.accountId || ''));
return { ok: true, account };
}
case 'DELETE_HOTMAIL_ACCOUNT': {
await deleteHotmailAccount(String(message.payload?.accountId || ''));
return { ok: true };
+110
View File
@@ -0,0 +1,110 @@
(function attachBackgroundPayPalAccountStore(root, factory) {
root.MultiPageBackgroundPayPalAccountStore = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPayPalAccountStoreModule() {
function createPayPalAccountStore(deps = {}) {
const {
broadcastDataUpdate,
findPayPalAccount,
getState,
normalizePayPalAccount,
normalizePayPalAccounts,
setPersistentSettings,
setState,
upsertPayPalAccountInList,
} = deps;
async function syncSelectedPayPalAccountState(account = null) {
const updates = account
? {
currentPayPalAccountId: account.id,
paypalEmail: String(account.email || '').trim(),
paypalPassword: String(account.password || ''),
}
: {
currentPayPalAccountId: '',
paypalEmail: '',
paypalPassword: '',
};
await setPersistentSettings(updates);
await setState({
currentPayPalAccountId: updates.currentPayPalAccountId || null,
paypalEmail: updates.paypalEmail,
paypalPassword: updates.paypalPassword,
});
broadcastDataUpdate({
currentPayPalAccountId: updates.currentPayPalAccountId || null,
paypalEmail: updates.paypalEmail,
paypalPassword: updates.paypalPassword,
});
}
async function syncPayPalAccounts(accounts) {
const normalized = normalizePayPalAccounts(accounts);
await setPersistentSettings({ paypalAccounts: normalized });
await setState({ paypalAccounts: normalized });
broadcastDataUpdate({ paypalAccounts: normalized });
const state = await getState();
if (state.currentPayPalAccountId && !findPayPalAccount(normalized, state.currentPayPalAccountId)) {
await syncSelectedPayPalAccountState(null);
}
return normalized;
}
function getCurrentPayPalAccount(state = {}) {
return findPayPalAccount(state.paypalAccounts, state.currentPayPalAccountId) || null;
}
async function upsertPayPalAccount(input = {}) {
const state = await getState();
const accounts = normalizePayPalAccounts(state.paypalAccounts);
const normalizedEmail = String(input?.email || '').trim().toLowerCase();
const existing = input?.id
? findPayPalAccount(accounts, input.id)
: accounts.find((account) => account.email === normalizedEmail) || null;
const normalized = normalizePayPalAccount({
...(existing || {}),
...input,
id: input?.id || existing?.id || crypto.randomUUID(),
createdAt: existing?.createdAt || Date.now(),
updatedAt: Date.now(),
});
const nextAccounts = typeof upsertPayPalAccountInList === 'function'
? upsertPayPalAccountInList(accounts, normalized)
: accounts.concat(normalized);
await syncPayPalAccounts(nextAccounts);
if (state.currentPayPalAccountId === normalized.id) {
await syncSelectedPayPalAccountState(normalized);
}
return normalized;
}
async function setCurrentPayPalAccount(accountId) {
const state = await getState();
const accounts = normalizePayPalAccounts(state.paypalAccounts);
const account = findPayPalAccount(accounts, accountId);
if (!account) {
throw new Error('未找到对应的 PayPal 账号。');
}
await syncSelectedPayPalAccountState(account);
return account;
}
return {
getCurrentPayPalAccount,
setCurrentPayPalAccount,
syncPayPalAccounts,
upsertPayPalAccount,
};
}
return {
createPayPalAccountStore,
};
});
+94 -145
View File
@@ -21,13 +21,13 @@
const PHONE_ACTIVATION_STATE_KEY = 'currentPhoneActivation';
const REUSABLE_PHONE_ACTIVATION_STATE_KEY = 'reusablePhoneActivation';
const PENDING_PHONE_ACTIVATION_CONFIRMATION_STATE_KEY = 'pendingPhoneActivationConfirmation';
const DEFAULT_PHONE_POLL_INTERVAL_MS = 5000;
const DEFAULT_PHONE_POLL_TIMEOUT_MS = 180000;
const DEFAULT_PHONE_REQUEST_TIMEOUT_MS = 20000;
const DEFAULT_PHONE_SUBMIT_ATTEMPTS = 3;
const DEFAULT_PHONE_CODE_WAIT_WINDOW_MS = 60000;
const DEFAULT_PHONE_NUMBER_MAX_USES = 3;
const DEFAULT_PHONE_PRICE_LOOKUP_ATTEMPTS = 3;
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::';
@@ -47,6 +47,18 @@
return String(value || '').trim();
}
function normalizeManualHeroSmsMaxPrice(value) {
const trimmed = String(value ?? '').trim();
if (!trimmed) {
return null;
}
const price = Number(trimmed);
if (!Number.isFinite(price) || price <= 0) {
return null;
}
return String(price);
}
function normalizeUseCount(value) {
return Math.max(0, Math.floor(Number(value) || 0));
}
@@ -236,13 +248,19 @@
}
}
function resolvePhoneConfig(state = {}) {
function resolvePhoneConfig(state = {}, options = {}) {
const apiKey = normalizeApiKey(state.heroSmsApiKey);
if (!apiKey) {
throw new Error('HeroSMS API key is missing. Save it in the side panel before running the phone flow.');
}
const requireMaxPrice = Boolean(options.requireMaxPrice);
const maxPrice = normalizeManualHeroSmsMaxPrice(state.heroSmsMaxPrice);
if (requireMaxPrice && !maxPrice) {
throw new Error('HeroSMS maxPrice is missing. Fill it in below the country selector before running the phone flow.');
}
return {
apiKey,
...(maxPrice ? { maxPrice } : {}),
baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL),
};
}
@@ -289,91 +307,10 @@
return activation?.statusAction === 'getStatusV2' ? 'getStatusV2' : 'getStatus';
}
function normalizeHeroSmsPrice(value) {
const price = Number(value);
if (!Number.isFinite(price) || price < 0) {
return null;
}
return price;
}
function collectHeroSmsPriceCandidates(payload, candidates = []) {
if (Array.isArray(payload)) {
payload.forEach((entry) => collectHeroSmsPriceCandidates(entry, candidates));
return candidates;
}
if (!payload || typeof payload !== 'object') {
return candidates;
}
const cost = normalizeHeroSmsPrice(payload.cost);
if (cost !== null) {
const count = Number(payload.count);
const physicalCount = Number(payload.physicalCount);
const hasCount = Number.isFinite(count);
const hasPhysicalCount = Number.isFinite(physicalCount);
if ((!hasCount && !hasPhysicalCount) || count > 0 || physicalCount > 0) {
candidates.push(cost);
}
}
Object.values(payload).forEach((value) => collectHeroSmsPriceCandidates(value, candidates));
return candidates;
}
function findLowestHeroSmsPrice(payload) {
const candidates = collectHeroSmsPriceCandidates(payload, []);
if (!candidates.length) {
return null;
}
return Math.min(...candidates);
}
function isHeroSmsNoNumbersPayload(payload) {
return /\bNO_NUMBERS\b/i.test(describeHeroSmsPayload(payload));
}
function extractHeroSmsWrongMaxPrice(payload) {
if (payload && typeof payload === 'object') {
const title = String(payload.title || '').trim();
const minPrice = normalizeHeroSmsPrice(payload.info?.min);
if (/^WRONG_MAX_PRICE$/i.test(title) && minPrice !== null) {
return minPrice;
}
}
const text = describeHeroSmsPayload(payload);
const match = text.match(/\bWRONG_MAX_PRICE:(\d+(?:\.\d+)?)\b/i);
if (!match) {
return null;
}
return normalizeHeroSmsPrice(match[1]);
}
function isNetworkFetchFailure(error) {
const message = String(error?.message || '').trim();
return /failed to fetch|networkerror|load failed/i.test(message);
}
async function resolveCheapestPhoneActivationPrice(config, countryConfig) {
for (let attempt = 1; attempt <= DEFAULT_PHONE_PRICE_LOOKUP_ATTEMPTS; attempt += 1) {
try {
const payload = await fetchHeroSmsPayload(config, {
action: 'getPrices',
service: HERO_SMS_SERVICE_CODE,
country: countryConfig.id,
}, 'HeroSMS getPrices');
const price = findLowestHeroSmsPrice(payload);
if (price !== null) {
return price;
}
} catch (_) {
// Best-effort lookup only.
}
}
return null;
}
async function fetchPhoneActivationPayload(config, countryConfig, action, options = {}) {
const query = {
action,
@@ -387,49 +324,10 @@
return fetchHeroSmsPayload(config, query, `HeroSMS ${action}`);
}
async function requestPhoneActivationWithPrice(config, countryConfig, action, maxPrice) {
let nextMaxPrice = maxPrice;
let retriedWithUpdatedPrice = false;
let retriedWithoutPrice = false;
while (true) {
try {
return await fetchPhoneActivationPayload(config, countryConfig, action, {
maxPrice: nextMaxPrice,
});
} catch (error) {
const updatedMaxPrice = extractHeroSmsWrongMaxPrice(error?.payload || error?.message);
if (
nextMaxPrice !== null
&& nextMaxPrice !== undefined
&& !retriedWithUpdatedPrice
&& updatedMaxPrice !== null
) {
nextMaxPrice = updatedMaxPrice;
retriedWithUpdatedPrice = true;
continue;
}
if (
nextMaxPrice !== null
&& nextMaxPrice !== undefined
&& !retriedWithoutPrice
&& isNetworkFetchFailure(error)
) {
nextMaxPrice = null;
retriedWithoutPrice = true;
continue;
}
throw error;
}
}
}
async function requestPhoneActivation(state = {}) {
const config = resolvePhoneConfig(state);
const config = resolvePhoneConfig(state, { requireMaxPrice: true });
const countryConfig = resolveCountryConfig(state);
const maxPrice = await resolveCheapestPhoneActivationPrice(config, countryConfig);
const maxPrice = config.maxPrice;
const buildFallbackActivation = (requestAction) => ({
countryId: countryConfig.id,
...(requestAction === 'getNumberV2' ? { statusAction: 'getStatusV2' } : {}),
@@ -438,19 +336,19 @@
let payload;
try {
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
} catch (error) {
if (!isHeroSmsNoNumbersPayload(error?.payload || error?.message)) {
throw error;
}
requestAction = 'getNumberV2';
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
}
let activation = parseActivationPayload(payload, buildFallbackActivation(requestAction));
if (!activation && requestAction === 'getNumber' && isHeroSmsNoNumbersPayload(payload)) {
requestAction = 'getNumberV2';
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
activation = parseActivationPayload(payload, buildFallbackActivation(requestAction));
}
@@ -496,7 +394,7 @@
}
async function completePhoneActivation(state = {}, activation) {
await setPhoneActivationStatus(state, activation, 6, 'HeroSMS setStatus(6)');
await setPhoneActivationStatus(state, activation, 3, 'HeroSMS setStatus(3)');
}
async function cancelPhoneActivation(state = {}, activation) {
@@ -738,6 +636,18 @@
await persistReusableActivation(null);
}
function incrementActivationUseCount(activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
return null;
}
return {
...normalizedActivation,
successfulUses: Math.min(normalizedActivation.successfulUses + 1, normalizedActivation.maxUses),
};
}
async function acquirePhoneActivation(state = {}) {
const countryConfig = resolveCountryConfig(state);
const reusableActivation = normalizeActivation(state[REUSABLE_PHONE_ACTIVATION_STATE_KEY]);
@@ -769,28 +679,53 @@
return activation;
}
async function markActivationReusableAfterSuccess(activation) {
async function syncReusableActivationAfterUse(activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
await clearReusableActivation();
return;
}
const successfulUses = normalizedActivation.successfulUses + 1;
if (successfulUses >= normalizedActivation.maxUses) {
if (normalizedActivation.successfulUses >= normalizedActivation.maxUses) {
await clearReusableActivation();
return;
}
await persistReusableActivation({
...normalizedActivation,
successfulUses,
await persistReusableActivation(normalizedActivation);
}
async function persistPendingPhoneActivationConfirmation(activation) {
await setState({
[PENDING_PHONE_ACTIVATION_CONFIRMATION_STATE_KEY]: activation || null,
});
}
async function clearPendingPhoneActivationConfirmation() {
await persistPendingPhoneActivationConfirmation(null);
}
async function finalizePendingPhoneActivationConfirmation(stateOverride = null) {
const state = stateOverride || await getState();
const pendingActivation = normalizeActivation(state[PENDING_PHONE_ACTIVATION_CONFIRMATION_STATE_KEY]);
if (!pendingActivation) {
return null;
}
const committedActivation = incrementActivationUseCount(pendingActivation);
if (!committedActivation) {
await clearPendingPhoneActivationConfirmation();
await clearReusableActivation();
return null;
}
await syncReusableActivationAfterUse(committedActivation);
await clearPendingPhoneActivationConfirmation();
return committedActivation;
}
async function waitForPhoneCodeOrRotateNumber(tabId, state, activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
let currentActivation = normalizeActivation(activation);
if (!currentActivation) {
throw new Error('Phone activation is missing.');
}
@@ -799,11 +734,11 @@
for (let windowIndex = 1; windowIndex <= 2; windowIndex += 1) {
await addLog(
`Step 9: waiting up to 60 seconds for SMS on ${normalizedActivation.phoneNumber} (${windowIndex}/2).`,
`Step 9: waiting up to 60 seconds for SMS on ${currentActivation.phoneNumber} (${windowIndex}/2).`,
'info'
);
try {
const code = await pollPhoneActivationCode(state, normalizedActivation, {
const code = await pollPhoneActivationCode(state, currentActivation, {
actionLabel: windowIndex === 1
? 'poll phone verification code from HeroSMS'
: 'poll resent phone verification code from HeroSMS',
@@ -820,7 +755,7 @@
lastLoggedStatus = statusText;
lastLoggedPollCount = pollCount;
await addLog(
`Step 9: HeroSMS status for ${normalizedActivation.phoneNumber}: ${statusText} (${Math.ceil(elapsedMs / 1000)}s elapsed).`,
`Step 9: HeroSMS status for ${currentActivation.phoneNumber}: ${statusText} (${Math.ceil(elapsedMs / 1000)}s elapsed).`,
'info'
);
},
@@ -836,10 +771,10 @@
if (windowIndex === 1) {
await addLog(
`Step 9: no SMS arrived for ${normalizedActivation.phoneNumber} within 60 seconds, requesting another SMS.`,
`Step 9: no SMS arrived for ${currentActivation.phoneNumber} within 60 seconds, requesting another SMS.`,
'warn'
);
await requestAdditionalPhoneSms(state, normalizedActivation);
await requestAdditionalPhoneSms(state, currentActivation);
try {
await resendPhoneVerificationCode(tabId);
await addLog('Step 9: clicked "Resend text message" on the phone verification page.', 'info');
@@ -850,10 +785,10 @@
}
await addLog(
`Step 9: still no SMS for ${normalizedActivation.phoneNumber} 60 seconds after resend, restarting from step 7 with a new number.`,
`Step 9: still no SMS for ${currentActivation.phoneNumber} 60 seconds after resend, restarting from step 7 with a new number.`,
'warn'
);
throw buildPhoneRestartStep7Error(normalizedActivation.phoneNumber);
throw buildPhoneRestartStep7Error(currentActivation.phoneNumber);
}
}
@@ -875,6 +810,9 @@
}
if (pageState?.addPhonePage) {
if (normalizeActivation(state[PENDING_PHONE_ACTIVATION_CONFIRMATION_STATE_KEY])) {
await clearPendingPhoneActivationConfirmation();
}
if (activation) {
await cancelPhoneActivation(state, activation);
await clearCurrentActivation();
@@ -965,8 +903,18 @@
continue;
}
await completePhoneActivation(state, activation);
await markActivationReusableAfterSuccess(activation);
try {
await completePhoneActivation(state, activation);
await persistReusableActivation(activation);
await persistPendingPhoneActivationConfirmation(activation);
} catch (activationStatusError) {
await clearReusableActivation();
await clearPendingPhoneActivationConfirmation();
await addLog(
`Step 9: phone verification succeeded, but HeroSMS setStatus(3) failed. The next flow will request a new number. ${activationStatusError.message}`,
'warn'
);
}
shouldCancelActivation = false;
await clearCurrentActivation();
await addLog('Step 9: phone verification finished, waiting for OAuth consent.', 'ok');
@@ -988,6 +936,7 @@
return {
completePhoneVerificationFlow,
finalizePendingPhoneActivationConfirmation,
normalizeActivation,
pollPhoneActivationCode,
reactivatePhoneActivation,
+67 -3
View File
@@ -27,6 +27,7 @@
reuseOrCreateTab,
setState,
shouldUseCustomRegistrationEmail,
sleepWithStop,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
throwIfStopped,
@@ -167,10 +168,26 @@
});
}
async function runStep8Attempt(state) {
function getStep8ResendIntervalMs(state = {}) {
const mail = getMailConfig(state);
if (mail?.provider === LUCKMAIL_PROVIDER) {
return 15000;
}
if (mail?.provider === HOTMAIL_PROVIDER || mail?.provider === '2925') {
return 0;
}
return Math.max(0, Number(STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS) || 0);
}
async function runStep8Attempt(state, runtime = {}) {
const visibleStep = getVisibleStep(state, 8);
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const stateLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
let latestResendAt = Math.max(0, Number(runtime?.stickyLastResendAt) || 0, stateLastResendAt);
const notifyResendRequestedAt = typeof runtime?.onResendRequestedAt === 'function'
? runtime.onResendRequestedAt
: null;
const stepStartedAt = Date.now();
const verificationFilterAfterTimestamp = mail.provider === '2925'
@@ -267,6 +284,16 @@
disableTimeBudgetCap: mail.provider === '2925',
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || '', visibleStep),
requestFreshCodeFirst: false,
lastResendAt: latestResendAt,
onResendRequestedAt: async (requestedAt) => {
const numericRequestedAt = Number(requestedAt) || 0;
if (numericRequestedAt > 0) {
latestResendAt = Math.max(latestResendAt, numericRequestedAt);
}
if (notifyResendRequestedAt) {
await notifyResendRequestedAt(latestResendAt);
}
},
targetEmail: fixedTargetEmail,
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
? 15000
@@ -274,6 +301,9 @@
? 0
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS),
});
return {
lastResendAt: latestResendAt,
};
}
function isStep8RestartStep7Error(error) {
@@ -285,10 +315,22 @@
let currentState = state;
let mailPollingAttempt = 1;
let lastMailPollingError = null;
let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
while (true) {
try {
await runStep8Attempt(currentState);
const result = await runStep8Attempt(currentState, {
stickyLastResendAt,
onResendRequestedAt: async (requestedAt) => {
const numericRequestedAt = Number(requestedAt) || 0;
if (numericRequestedAt > 0) {
stickyLastResendAt = Math.max(stickyLastResendAt, numericRequestedAt);
}
},
});
if (Number(result?.lastResendAt) > 0) {
stickyLastResendAt = Math.max(stickyLastResendAt, Number(result.lastResendAt) || 0);
}
return;
} catch (err) {
const visibleStep = getVisibleStep(currentState, 8);
@@ -323,7 +365,29 @@
`步骤 ${visibleStep}:认证页仍保持在验证码页,将在当前链路直接重试(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}),不回到步骤 ${authLoginStep}`,
'warn'
);
currentState = await getState();
const latestState = await getState();
const latestStateResendAt = Number(latestState?.loginVerificationRequestedAt) || 0;
if (latestStateResendAt > 0) {
stickyLastResendAt = Math.max(stickyLastResendAt, latestStateResendAt);
}
currentState = latestState;
if (stickyLastResendAt > 0 && (!latestStateResendAt || latestStateResendAt < stickyLastResendAt)) {
currentState = {
...latestState,
loginVerificationRequestedAt: stickyLastResendAt,
};
}
const resendIntervalMs = getStep8ResendIntervalMs(currentState);
const remainingBeforeRetryMs = stickyLastResendAt > 0 && resendIntervalMs > 0
? Math.max(0, resendIntervalMs - (Date.now() - stickyLastResendAt))
: 0;
if (remainingBeforeRetryMs > 0 && typeof sleepWithStop === 'function') {
await addLog(
`步骤 ${visibleStep}:上轮已触发重发验证码,为避免重复重发,先等待 ${Math.ceil(remainingBeforeRetryMs / 1000)} 秒后继续当前链路重试。`,
'info'
);
await sleepWithStop(Math.min(remainingBeforeRetryMs, 3000));
}
continue;
}
await addLog(
+17 -4
View File
@@ -99,17 +99,30 @@
return result || {};
}
function resolvePayPalCredentials(state = {}) {
const currentId = String(state?.currentPayPalAccountId || '').trim();
const accounts = Array.isArray(state?.paypalAccounts) ? state.paypalAccounts : [];
const selectedAccount = currentId
? accounts.find((account) => String(account?.id || '').trim() === currentId) || null
: null;
return {
email: String(selectedAccount?.email || state?.paypalEmail || '').trim(),
password: String(selectedAccount?.password || state?.paypalPassword || ''),
};
}
async function submitLogin(tabId, state = {}) {
if (!state.paypalPassword) {
throw new Error('步骤 8:未配置 PayPal 密码,请先在侧边栏填写。');
const credentials = resolvePayPalCredentials(state);
if (!credentials.password) {
throw new Error('步骤 8:未配置可用的 PayPal 账号,请先在侧边栏添加并选择账号。');
}
await addLog('步骤 8:正在填写 PayPal 登录信息并提交...', 'info');
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
type: 'PAYPAL_SUBMIT_LOGIN',
source: 'background',
payload: {
email: state.paypalEmail || '',
password: state.paypalPassword || '',
email: credentials.email,
password: credentials.password,
},
});
if (result?.error) {
+11 -1
View File
@@ -920,8 +920,18 @@
const maxSubmitAttempts = mail.provider === LUCKMAIL_PROVIDER ? 3 : 15;
const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
let lastResendAt = Number(options.lastResendAt) || 0;
const externalOnResendRequestedAt = typeof options.onResendRequestedAt === 'function'
? options.onResendRequestedAt
: null;
const updateFilterAfterTimestampForVerificationStep = async (_requestedAt) => {
const updateFilterAfterTimestampForVerificationStep = async (requestedAt) => {
if (externalOnResendRequestedAt) {
try {
await externalOnResendRequestedAt(requestedAt);
} catch (_) {
// Keep resend flow best-effort; state sync callback failures should not break verification.
}
}
return nextFilterAfterTimestamp;
};