diff --git a/background.js b/background.js index 4c24a89..de43b22 100644 --- a/background.js +++ b/background.js @@ -481,6 +481,7 @@ const PERSISTED_SETTING_DEFAULTS = { mail2925Accounts: [], paypalAccounts: [], heroSmsApiKey: '', + heroSmsMaxPrice: '', heroSmsCountryId: HERO_SMS_COUNTRY_ID, heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL, }; @@ -563,6 +564,7 @@ const DEFAULT_STATE = { currentLuckmailMailCursor: null, currentPhoneActivation: null, reusablePhoneActivation: null, + pendingPhoneActivationConfirmation: null, autoRunning: false, // 当前是否处于自动运行中。 autoRunPhase: 'idle', // 当前自动运行阶段。 autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。 @@ -1325,6 +1327,8 @@ function normalizePersistentSettingValue(key, value) { return normalizePayPalAccounts(value); case 'heroSmsApiKey': return String(value || ''); + case 'heroSmsMaxPrice': + return String(value || '').trim(); case 'heroSmsCountryId': return Math.max(1, Math.floor(Number(value) || HERO_SMS_COUNTRY_ID)); case 'heroSmsCountryLabel': @@ -5292,6 +5296,13 @@ async function finalizeIcloudAliasAfterSuccessfulFlow(state) { } } +async function finalizePhoneActivationAfterSuccessfulFlow(state) { + if (typeof phoneVerificationHelpers?.finalizePendingPhoneActivationConfirmation !== 'function') { + return null; + } + return phoneVerificationHelpers.finalizePendingPhoneActivationConfirmation(state); +} + // ============================================================ // Tab Registry // ============================================================ @@ -6027,6 +6038,7 @@ function getDownstreamStateResets(step, state = {}) { loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, + pendingPhoneActivationConfirmation: null, lastSignupCode: null, lastLoginCode: null, localhostUrl: null, @@ -6041,6 +6053,7 @@ function getDownstreamStateResets(step, state = {}) { loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, + pendingPhoneActivationConfirmation: null, lastSignupCode: null, lastLoginCode: null, localhostUrl: null, @@ -6054,6 +6067,7 @@ function getDownstreamStateResets(step, state = {}) { loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, + pendingPhoneActivationConfirmation: null, lastSignupCode: null, lastLoginCode: null, localhostUrl: null, @@ -6076,11 +6090,13 @@ function getDownstreamStateResets(step, state = {}) { loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, + pendingPhoneActivationConfirmation: null, localhostUrl: null, }; } if (step === 9) { return { + pendingPhoneActivationConfirmation: null, plusReturnUrl: '', localhostUrl: null, }; @@ -6091,11 +6107,13 @@ function getDownstreamStateResets(step, state = {}) { loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, + pendingPhoneActivationConfirmation: null, localhostUrl: null, }; } if (stepKey === 'confirm-oauth') { return { + pendingPhoneActivationConfirmation: null, localhostUrl: null, }; } @@ -6946,6 +6964,7 @@ async function handleStepData(step, payload) { if ((shouldUseCustomRegistrationEmail(latestState) || shouldClearCustomPoolEmail) && latestState.email) { await setEmailStateSilently(null); } + await finalizePhoneActivationAfterSuccessfulFlow(latestState); break; } } @@ -8824,6 +8843,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter executeStepViaCompletionSignal, exportSettingsBundle, fetchGeneratedEmail, + finalizePhoneActivationAfterSuccessfulFlow, finalizeStep3Completion: async () => { const currentState = await getState(); const signupTabId = await getTabId('signup-page'); diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index e1a6d92..25270f2 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -392,6 +392,7 @@ inbucketMailbox: prevState.inbucketMailbox, cloudflareDomain: prevState.cloudflareDomain, cloudflareDomains: prevState.cloudflareDomains, + reusablePhoneActivation: prevState.reusablePhoneActivation, autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), autoRunSessionId: sessionId, tabRegistry: {}, diff --git a/background/message-router.js b/background/message-router.js index 0944a3d..1af516a 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -32,6 +32,7 @@ executeStepViaCompletionSignal, exportSettingsBundle, fetchGeneratedEmail, + finalizePhoneActivationAfterSuccessfulFlow, finalizeStep3Completion, finalizeIcloudAliasAfterSuccessfulFlow, findHotmailAccount, @@ -209,6 +210,9 @@ }); } await finalizeIcloudAliasAfterSuccessfulFlow(latestState); + if (typeof finalizePhoneActivationAfterSuccessfulFlow === 'function') { + await finalizePhoneActivationAfterSuccessfulFlow(latestState); + } } async function handleStepData(step, payload) { diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index ca16eae..e8b7eb8 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -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, diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 0af5cd4..9f59517 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -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( diff --git a/background/verification-flow.js b/background/verification-flow.js index dcade7a..f41d9fa 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -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; }; diff --git a/scripts/hotmail_helper.py b/scripts/hotmail_helper.py index 806ecad..768b480 100644 --- a/scripts/hotmail_helper.py +++ b/scripts/hotmail_helper.py @@ -123,6 +123,48 @@ def log_info(message): print(f"[HotmailHelper] {message}", flush=True) +def is_openai_sender(address): + sender = str(address or "").strip().lower() + return "openai" in sender + + +def get_message_body_content(message): + body = message.get("body") or {} + if not isinstance(body, dict): + return "" + return str(body.get("content") or "").strip() + + +def log_openai_messages(messages, transport=""): + for message in messages or []: + sender_info = message.get("from", {}).get("emailAddress", {}) or {} + sender = str(sender_info.get("address") or "").strip() + if not is_openai_sender(sender): + continue + + sender_name = str(sender_info.get("name") or "").strip() + mailbox = str(message.get("mailbox") or "").strip() or "INBOX" + subject = str(message.get("subject") or "").strip() + transport_label = str(transport or "").strip() or "unknown" + base = ( + f"transport={transport_label} mailbox={mailbox} sender={sender} " + f"senderName={sender_name or '-'} subject={subject}" + ) + + log_info(f"openai mail received {base}") + + body_content = get_message_body_content(message) + if body_content: + log_info(f"openai mail full body start {base}") + print(body_content, flush=True) + log_info("openai mail full body end") + continue + + preview = str(message.get("bodyPreview") or "").strip() + if preview: + log_info(f"openai mail preview {base} preview={compact_text(preview, 1000)}") + + def get_proxy_debug_context(): names = ["all_proxy", "http_proxy", "https_proxy", "ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY"] parts = [] @@ -481,6 +523,9 @@ def normalize_message(message_id, raw_bytes, mailbox): } }, "bodyPreview": body[:500], + "body": { + "content": body, + }, "receivedDateTime": to_iso_string(timestamp_ms), "receivedTimestamp": timestamp_ms, } @@ -680,6 +725,7 @@ def collect_messages(email_addr, client_id, refresh_token, mailboxes, top): try: log_info(f"message collection start transport={transport_name}") result = collector(email_addr, client_id, refresh_token, mailboxes, top) + log_openai_messages(result.get("messages") or [], transport=transport_name) log_info( f"message collection success transport={transport_name} " f"tokenEndpoint={result['token_payload'].get('token_endpoint', '')}" @@ -722,6 +768,10 @@ def select_latest_code(messages, sender_filters, subject_filters, exclude_codes, preview = str(message.get("bodyPreview", "")) combined = " ".join([sender, subject.lower(), preview.lower()]) code = extract_code(" ".join([subject, preview, sender])) + if not code: + body_content = get_message_body_content(message) + if body_content: + code = extract_code(" ".join([subject, body_content, sender])) if not code or code in excluded: return None diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index c79b39c..6fa6a12 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -425,6 +425,11 @@ +