// background.js — Service Worker: orchestration, state, tab management, message routing importScripts( 'managed-alias-utils.js', 'mail2925-utils.js', 'paypal-utils.js', 'gopay-utils.js', 'phone-sms/providers/hero-sms.js', 'phone-sms/providers/five-sim.js', 'phone-sms/providers/registry.js', 'background/phone-verification-flow.js', 'background/account-run-history.js', 'background/contribution-oauth.js', 'background/mail-2925-session.js', 'background/paypal-account-store.js', 'background/ip-proxy-provider-711proxy.js', 'background/ip-proxy-core.js', 'background/panel-bridge.js', 'background/generated-email-helpers.js', 'background/signup-flow-helpers.js', 'background/message-router.js', 'background/verification-flow.js', 'background/auto-run-controller.js', 'background/tab-runtime.js', 'background/navigation-utils.js', 'background/logging-status.js', 'background/steps/registry.js', 'data/step-definitions.js', 'data/address-sources.js', 'background/steps/open-chatgpt.js', 'background/steps/submit-signup-email.js', 'background/steps/fill-password.js', 'background/steps/fetch-signup-code.js', 'background/steps/fill-profile.js', 'background/steps/clear-login-cookies.js', 'background/steps/create-plus-checkout.js', 'background/steps/fill-plus-checkout.js', 'background/steps/gopay-manual-confirm.js', 'background/steps/paypal-approve.js', 'background/steps/gopay-approve.js', 'background/steps/plus-return-confirm.js', 'background/steps/oauth-login.js', 'background/steps/fetch-login-code.js', 'background/steps/confirm-oauth.js', 'background/steps/platform-verify.js', 'data/names.js', 'hotmail-utils.js', 'microsoft-email.js', 'luckmail-utils.js', 'cloudflare-temp-email-utils.js', 'icloud-utils.js', 'mail-provider-utils.js', 'content/activation-utils.js' ); const NORMAL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled: false }) || []; const PLUS_PAYPAL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled: true, plusPaymentMethod: 'paypal', }) || NORMAL_STEP_DEFINITIONS; const PLUS_GOPAY_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled: true, plusPaymentMethod: 'gopay', }) || PLUS_PAYPAL_STEP_DEFINITIONS; const PLUS_STEP_DEFINITIONS = PLUS_PAYPAL_STEP_DEFINITIONS; const ALL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getAllSteps?.() || [ ...NORMAL_STEP_DEFINITIONS, ...PLUS_PAYPAL_STEP_DEFINITIONS, ...PLUS_GOPAY_STEP_DEFINITIONS, ]; const STEP_IDS = Array.from(new Set(ALL_STEP_DEFINITIONS .map((definition) => Number(definition?.id)) .filter(Number.isFinite))) .sort((left, right) => left - right); const NORMAL_STEP_IDS = NORMAL_STEP_DEFINITIONS .map((definition) => Number(definition?.id)) .filter(Number.isFinite) .sort((left, right) => left - right); const PLUS_PAYPAL_STEP_IDS = PLUS_PAYPAL_STEP_DEFINITIONS .map((definition) => Number(definition?.id)) .filter(Number.isFinite) .sort((left, right) => left - right); const PLUS_GOPAY_STEP_IDS = PLUS_GOPAY_STEP_DEFINITIONS .map((definition) => Number(definition?.id)) .filter(Number.isFinite) .sort((left, right) => left - right); const PLUS_STEP_IDS = PLUS_PAYPAL_STEP_IDS; const LAST_STEP_ID = Math.max( NORMAL_STEP_IDS[NORMAL_STEP_IDS.length - 1] || 10, PLUS_PAYPAL_STEP_IDS[PLUS_PAYPAL_STEP_IDS.length - 1] || 10, PLUS_GOPAY_STEP_IDS[PLUS_GOPAY_STEP_IDS.length - 1] || 10 ); const FINAL_OAUTH_CHAIN_START_STEP = 7; const { extractVerificationCodeFromMessage, filterHotmailAccountsByUsage, getLatestHotmailMessage, getHotmailMailApiRequestConfig, getHotmailVerificationPollConfig, getHotmailVerificationRequestTimestamp, normalizeHotmailServiceMode, normalizeHotmailMailApiMessages, pickHotmailAccountForRun, pickVerificationMessage, pickVerificationMessageWithFallback, pickVerificationMessageWithTimeFallback, shouldClearHotmailCurrentSelection, } = self.HotmailUtils; const { MAIL2925_LIMIT_COOLDOWN_MS, findMail2925Account, getMail2925AccountStatus, normalizeMail2925Account, normalizeMail2925Accounts, parseMail2925ImportText, pickMail2925AccountForRun, upsertMail2925AccountInList, } = self.Mail2925Utils; const { fetchMicrosoftMailboxMessages, } = self.MultiPageMicrosoftEmail; const { DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, DEFAULT_LUCKMAIL_BASE_URL, DEFAULT_LUCKMAIL_EMAIL_TYPE, buildLuckmailBaselineCursor, buildLuckmailMailCursor, filterReusableLuckmailPurchases, isLuckmailMailNewerThanCursor, isLuckmailPurchaseReusable, isLuckmailPurchaseForProject, isLuckmailPurchasePreserved, normalizeLuckmailBaseUrl, normalizeLuckmailEmailType, normalizeLuckmailMailCursor, normalizeLuckmailProjectName, normalizeLuckmailPurchase, normalizeLuckmailPurchaseId, normalizeLuckmailPurchaseListPage, normalizeLuckmailPurchases, normalizeLuckmailTags, normalizeLuckmailTokenCode, normalizeLuckmailTokenMail, normalizeLuckmailTokenMails, normalizeLuckmailUsedPurchases, normalizeTimestamp: normalizeLuckmailTimestamp, pickLuckmailVerificationMail, } = self.LuckMailUtils; const { DEFAULT_MAIL_PAGE_SIZE: CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE, buildCloudflareTempEmailHeaders, getCloudflareTempEmailAddressFromResponse, joinCloudflareTempEmailUrl, normalizeCloudflareTempEmailAddress, normalizeCloudflareTempEmailBaseUrl, normalizeCloudflareTempEmailDomain, normalizeCloudflareTempEmailDomains, normalizeCloudflareTempEmailMailApiMessages, } = self.CloudflareTempEmailUtils; const { findIcloudAliasByEmail, getConfiguredIcloudHostPreference, getIcloudHostHintFromMessage, getIcloudLoginUrlForHost, getIcloudMailUrlForHost, getIcloudSetupUrlForHost, normalizeBooleanMap, normalizeIcloudAliasList, normalizeIcloudAliasRecord, normalizeIcloudHost, pickReusableIcloudAlias, toNormalizedEmailSet, } = self.IcloudUtils; const { getIcloudForwardMailConfig: getSharedIcloudForwardMailConfig, normalizeIcloudForwardMailProvider, normalizeIcloudTargetMailboxType, } = self.MailProviderUtils; const { isRecoverableStep9AuthFailure, } = self.MultiPageActivationUtils; const LOG_PREFIX = '[MultiPage:bg]'; const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill'; const ICLOUD_SETUP_URLS = [ 'https://setup.icloud.com/setup/ws/1', 'https://setup.icloud.com.cn/setup/ws/1', ]; const ICLOUD_LOGIN_URLS = [ 'https://www.icloud.com/', 'https://www.icloud.com.cn/', ]; const ICLOUD_REQUEST_TIMEOUT_MS = 15000; const ICLOUD_LIST_MAX_ATTEMPTS = 3; const ICLOUD_WRITE_MAX_ATTEMPTS = 2; const ICLOUD_RETRY_DELAYS_MS = [1000, 2500, 5000]; const ICLOUD_TAB_URL_PATTERNS = [ 'https://www.icloud.com/*', 'https://www.icloud.com.cn/*', 'https://setup.icloud.com/*', 'https://setup.icloud.com.cn/*', 'https://*.icloud.com/*', 'https://*.icloud.com.cn/*', ]; const ICLOUD_MAILDOMAINWS_CLIENT_BUILD_NUMBER = '2206Hotfix11'; const ICLOUD_ALIAS_CACHE_MAX_AGE_MS = 6 * 60 * 60 * 1000; const ICLOUD_TRANSIENT_RETRY_MAX_ATTEMPTS = 2; const ICLOUD_TRANSIENT_RETRY_DELAY_MS = 1200; const ICLOUD_PROVIDER = 'icloud'; const GMAIL_PROVIDER = 'gmail'; const GMAIL_ALIAS_GENERATOR = 'gmail-alias'; const HOTMAIL_PROVIDER = 'hotmail-api'; const LUCKMAIL_PROVIDER = 'luckmail-api'; const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email'; const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email'; const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool'; const HOTMAIL_MAILBOXES = ['INBOX', 'Junk']; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; const CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX = 'CF_SECURITY_BLOCKED::'; const CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE = '您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器'; const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::'; const HUMAN_STEP_DELAY_MIN = 700; const HUMAN_STEP_DELAY_MAX = 2200; const STEP6_MAX_ATTEMPTS = 3; const STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS = 8; const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000; const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000; const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000; const DEFAULT_SUB2API_URL = 'https://sub2api.hisence.fun/admin/accounts'; const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts'; const DEFAULT_SUB2API_GROUP_NAME = 'codex'; const DEFAULT_SUB2API_PROXY_NAME = ''; const CONTRIBUTION_SOURCE_CPA = 'cpa'; const CONTRIBUTION_SOURCE_SUB2API = 'sub2api'; const CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME = 'codex号池'; const CONTRIBUTION_SUB2API_PLUS_GROUP_NAME = 'openai-plus'; const DEFAULT_SUB2API_REDIRECT_URI = 'http://localhost:1455/auth/callback'; const DEFAULT_IP_PROXY_SERVICE = '711proxy'; const IP_PROXY_SERVICE_VALUES = ['711proxy', 'lumiproxy', 'iproyal', 'omegaproxy']; const IP_PROXY_ENABLED_SERVICE_VALUES = ['711proxy']; const DEFAULT_IP_PROXY_MODE = 'account'; const IP_PROXY_MODE_VALUES = ['api', 'account']; const DEFAULT_IP_PROXY_PROTOCOL = 'http'; const IP_PROXY_PROTOCOL_VALUES = ['http', 'https', 'socks4', 'socks5']; const IP_PROXY_FETCH_TIMEOUT_MS = 20000; const IP_PROXY_SETTINGS_SCOPE = 'regular'; const IP_PROXY_BYPASS_LIST = ['', 'localhost', '127.0.0.1']; const IP_PROXY_ROUTE_ALL_TRAFFIC = true; const IP_PROXY_FORCE_DIRECT_HOST_PATTERNS = [ 'pm-redirects.stripe.com', '*.pm-redirects.stripe.com', 'hwork.pro', '*.hwork.pro', 'auth.openai.com', 'auth0.openai.com', 'accounts.openai.com', 'luckyous.com', '*.luckyous.com', ]; const IP_PROXY_FORCE_DIRECT_FALLBACK = 'PROXY 127.0.0.1:7897'; const IP_PROXY_ACCOUNT_LIST_ENABLED = false; const IP_PROXY_INIT_ENABLE_EXIT_PROBE = false; const IP_PROXY_INIT_SUPPRESS_AUTH_REBIND = true; const IP_PROXY_INIT_AUTO_APPLY = false; const IP_PROXY_TARGET_HOST_PATTERNS = [ 'openai.com', '*.openai.com', 'chatgpt.com', '*.chatgpt.com', 'ipwho.is', '*.ipwho.is', 'ipapi.co', '*.ipapi.co', 'ipinfo.io', '*.ipinfo.io', 'api.ipify.org', 'api64.ipify.org', 'api.ip.cc', 'ifconfig.me', 'checkip.amazonaws.com', 'ipv4.icanhazip.com', 'ident.me', 'httpbin.org', 'ip-api.com', 'myip.ipip.net', ]; const AUTO_RUN_TIMER_ALARM_NAME = 'auto-run-timer'; const IP_PROXY_AUTO_SYNC_ALARM_NAME = 'ip-proxy-auto-sync'; const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start'; const AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS = 'between_rounds'; const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry'; const IP_PROXY_AUTO_SYNC_INTERVAL_MIN_MINUTES = 1; const IP_PROXY_AUTO_SYNC_INTERVAL_MAX_MINUTES = 1440; const IP_PROXY_AUTO_SYNC_DEFAULT_INTERVAL_MINUTES = 15; const AUTO_RUN_DELAY_MIN_MINUTES = 1; const AUTO_RUN_DELAY_MAX_MINUTES = 1440; const AUTO_RUN_RETRY_DELAY_MS = 3000; const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3; const AUTO_STEP_DELAY_MIN_ALLOWED_SECONDS = 0; const AUTO_STEP_DELAY_MAX_ALLOWED_SECONDS = 600; const VERIFICATION_RESEND_COUNT_MIN = 0; const VERIFICATION_RESEND_COUNT_MAX = 20; 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_VERIFICATION_RESEND_COUNT_KEYS = ['signupVerificationResendCount', 'loginVerificationResendCount']; const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit'; const MAIL_2925_MODE_PROVIDE = 'provide'; const MAIL_2925_MODE_RECEIVE = 'receive'; const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE; const HOTMAIL_SERVICE_MODE_REMOTE = 'remote'; const HOTMAIL_SERVICE_MODE_LOCAL = 'local'; const DEFAULT_HOTMAIL_REMOTE_BASE_URL = ''; const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373'; const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = DEFAULT_HOTMAIL_LOCAL_BASE_URL; const HOTMAIL_LOCAL_HELPER_TIMEOUT_MS = 45000; const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai'; const DEFAULT_HERO_SMS_BASE_URL = 'https://hero-sms.com/stubs/handler_api.php'; const HERO_SMS_SERVICE_CODE = 'dr'; const HERO_SMS_SERVICE_LABEL = 'OpenAI'; const HERO_SMS_COUNTRY_ID = 52; const HERO_SMS_COUNTRY_LABEL = 'Thailand'; const PHONE_SMS_PROVIDER_HERO = 'hero-sms'; const PHONE_SMS_PROVIDER_5SIM = '5sim'; const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms'; const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO; const DEFAULT_PHONE_SMS_PROVIDER_ORDER = Object.freeze([ PHONE_SMS_PROVIDER_HERO, PHONE_SMS_PROVIDER_5SIM, PHONE_SMS_PROVIDER_NEXSMS, ]); const DEFAULT_FIVE_SIM_BASE_URL = 'https://5sim.net/v1'; const DEFAULT_FIVE_SIM_PRODUCT = 'openai'; const DEFAULT_FIVE_SIM_OPERATOR = 'any'; const DEFAULT_FIVE_SIM_COUNTRY_ORDER = Object.freeze(['thailand']); const DEFAULT_NEX_SMS_BASE_URL = 'https://api.nexsms.net'; const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot'; const DEFAULT_NEX_SMS_COUNTRY_ORDER = Object.freeze([1]); const DEFAULT_HERO_SMS_REUSE_ENABLED = true; const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country'; const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price'; const HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH = 'price_high'; const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY; const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms'; const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim'; const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS; const FIVE_SIM_COUNTRY_ID = 'vietnam'; const FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)'; const FIVE_SIM_SUPPORTED_COUNTRY_IDS = ['indonesia', 'thailand', 'vietnam']; const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(FIVE_SIM_SUPPORTED_COUNTRY_IDS); const HERO_SMS_SUPPORTED_COUNTRY_IDS = [6, 52, 10]; const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(HERO_SMS_SUPPORTED_COUNTRY_IDS.map(String)); const FIVE_SIM_OPERATOR = 'any'; const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL; const DISPLAY_TIMEZONE = 'Asia/Shanghai'; const MICROSOFT_TOKEN_DNR_RULE_ID = 1001; const PERSISTENT_ALIAS_STATE_KEYS = [ 'manualAliasUsage', 'preservedAliases', 'icloudAliasCache', 'icloudAliasCacheAt', ]; const ACCOUNT_RUN_HISTORY_STORAGE_KEY = 'accountRunHistory'; const CONTRIBUTION_RUNTIME_DEFAULTS = self.MultiPageBackgroundContributionOAuth?.RUNTIME_DEFAULTS || { contributionMode: false, contributionModeExpected: false, contributionSource: CONTRIBUTION_SOURCE_SUB2API, contributionTargetGroupName: CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME, contributionNickname: '', contributionQq: '', contributionSessionId: '', contributionAuthUrl: '', contributionAuthState: '', contributionCallbackUrl: '', contributionStatus: '', contributionStatusMessage: '', contributionLastPollAt: 0, contributionCallbackStatus: 'idle', contributionCallbackMessage: '', contributionAuthOpenedAt: 0, contributionAuthTabId: 0, }; const CONTRIBUTION_RUNTIME_KEYS = self.MultiPageBackgroundContributionOAuth?.RUNTIME_KEYS || Object.keys(CONTRIBUTION_RUNTIME_DEFAULTS); function isPlusModeState(state = {}) { return Boolean(state?.plusModeEnabled); } function normalizePlusPaymentMethod(value = '') { return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal'; } function normalizeContributionModeSource(value = '') { const normalized = String(value || '').trim().toLowerCase(); return normalized === CONTRIBUTION_SOURCE_SUB2API ? CONTRIBUTION_SOURCE_SUB2API : CONTRIBUTION_SOURCE_CPA; } function resolveContributionModeRoutingState(state = {}) { const currentStatus = String(state?.contributionStatus || '').trim().toLowerCase(); const currentSource = normalizeContributionModeSource(state?.contributionSource); const hasActiveSession = Boolean( String(state?.contributionSessionId || '').trim() && currentStatus && !['auto_approved', 'auto_rejected', 'expired', 'error'].includes(currentStatus) ); if (hasActiveSession) { return { source: currentSource, targetGroupName: currentSource === CONTRIBUTION_SOURCE_SUB2API ? (String(state?.contributionTargetGroupName || '').trim() || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME) : '', }; } const source = CONTRIBUTION_SOURCE_SUB2API; return { source, targetGroupName: isPlusModeState(state) ? CONTRIBUTION_SUB2API_PLUS_GROUP_NAME : (String(state?.contributionTargetGroupName || '').trim() || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME), }; } function getStepDefinitionsForState(state = {}) { if (!isPlusModeState(state)) { return NORMAL_STEP_DEFINITIONS; } return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_DEFINITIONS : PLUS_PAYPAL_STEP_DEFINITIONS; } function getStepIdsForState(state = {}) { if (!isPlusModeState(state)) { return NORMAL_STEP_IDS; } return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_IDS : PLUS_PAYPAL_STEP_IDS; } function getLastStepIdForState(state = {}) { const ids = getStepIdsForState(state); return ids[ids.length - 1] || 10; } function getAuthChainStartStepId(state = {}) { return isPlusModeState(state) ? 10 : FINAL_OAUTH_CHAIN_START_STEP; } function getStepDefinitionForState(step, state = {}) { const numericStep = Number(step); return getStepDefinitionsForState(state).find((definition) => Number(definition.id) === numericStep) || null; } initializeSessionStorageAccess(); setupDeclarativeNetRequestRules(); function setupDeclarativeNetRequestRules() { if (!chrome.declarativeNetRequest?.updateDynamicRules) { return; } chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: [MICROSOFT_TOKEN_DNR_RULE_ID], addRules: [{ id: MICROSOFT_TOKEN_DNR_RULE_ID, priority: 1, action: { type: 'modifyHeaders', requestHeaders: [ { header: 'Origin', operation: 'remove' }, ], }, condition: { urlFilter: 'login.microsoftonline.com/*/oauth2/v2.0/token', resourceTypes: ['xmlhttprequest'], }, }], }).catch((error) => { console.warn(LOG_PREFIX, 'Failed to setup declarativeNetRequest rules:', error?.message || error); }); } // ============================================================ // 状态管理(chrome.storage.session + chrome.storage.local) // ============================================================ const PERSISTED_SETTING_DEFAULTS = { panelMode: 'cpa', vpsUrl: '', vpsPassword: '', localCpaStep9Mode: DEFAULT_LOCAL_CPA_STEP9_MODE, sub2apiUrl: DEFAULT_SUB2API_URL, sub2apiEmail: '', sub2apiPassword: '', sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME, sub2apiDefaultProxyName: DEFAULT_SUB2API_PROXY_NAME, ipProxyEnabled: false, ipProxyService: DEFAULT_IP_PROXY_SERVICE, ipProxyMode: DEFAULT_IP_PROXY_MODE, ipProxyApiUrl: '', ipProxyServiceProfiles: {}, ipProxyAccountList: '', ipProxyAccountSessionPrefix: '', ipProxyAccountLifeMinutes: '', ipProxyPoolTargetCount: '20', ipProxyAutoSyncEnabled: false, ipProxyAutoSyncIntervalMinutes: IP_PROXY_AUTO_SYNC_DEFAULT_INTERVAL_MINUTES, ipProxyHost: '', ipProxyPort: '', ipProxyProtocol: DEFAULT_IP_PROXY_PROTOCOL, ipProxyUsername: '', ipProxyPassword: '', ipProxyRegion: '', codex2apiUrl: DEFAULT_CODEX2API_URL, codex2apiAdminKey: '', customPassword: '', plusModeEnabled: false, plusPaymentMethod: DEFAULT_PLUS_PAYMENT_METHOD, paypalEmail: '', paypalPassword: '', currentPayPalAccountId: '', gopayCountryCode: '+86', gopayPhone: '', gopayOtp: '', gopayPin: '', autoRunSkipFailures: false, autoRunFallbackThreadIntervalMinutes: 0, oauthFlowTimeoutEnabled: true, autoRunDelayEnabled: false, autoRunDelayMinutes: 30, autoStepDelaySeconds: null, phoneVerificationEnabled: false, phoneSmsProvider: DEFAULT_PHONE_SMS_PROVIDER, phoneSmsProviderOrder: [], 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', mail2925Mode: DEFAULT_MAIL_2925_MODE, mail2925UseAccountPool: false, emailGenerator: 'duck', customMailProviderPool: [], customEmailPool: [], customEmailPoolEntries: [], autoDeleteUsedIcloudAlias: false, icloudHostPreference: 'auto', icloudTargetMailboxType: 'icloud-inbox', icloudForwardMailProvider: 'qq', icloudFetchMode: 'reuse_existing', accountRunHistoryTextEnabled: true, accountRunHistoryHelperBaseUrl: DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL, gmailBaseEmail: '', mail2925BaseEmail: '', currentMail2925AccountId: '', emailPrefix: '', inbucketHost: '', inbucketMailbox: '', hotmailServiceMode: HOTMAIL_SERVICE_MODE_LOCAL, hotmailRemoteBaseUrl: DEFAULT_HOTMAIL_REMOTE_BASE_URL, hotmailLocalBaseUrl: DEFAULT_HOTMAIL_LOCAL_BASE_URL, luckmailApiKey: '', luckmailBaseUrl: DEFAULT_LUCKMAIL_BASE_URL, luckmailEmailType: DEFAULT_LUCKMAIL_EMAIL_TYPE, luckmailDomain: '', luckmailUsedPurchases: {}, luckmailPreserveTagId: 0, luckmailPreserveTagName: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, cloudflareDomain: '', cloudflareDomains: [], cloudflareTempEmailBaseUrl: '', cloudflareTempEmailAdminAuth: '', cloudflareTempEmailCustomAuth: '', cloudflareTempEmailReceiveMailbox: '', cloudflareTempEmailUseRandomSubdomain: false, cloudflareTempEmailDomain: '', cloudflareTempEmailDomains: [], hotmailAccounts: [], mail2925Accounts: [], paypalAccounts: [], phoneSmsProvider: DEFAULT_PHONE_SMS_PROVIDER, heroSmsApiKey: '', heroSmsReuseEnabled: DEFAULT_HERO_SMS_REUSE_ENABLED, heroSmsAcquirePriority: DEFAULT_HERO_SMS_ACQUIRE_PRIORITY, heroSmsMaxPrice: '', heroSmsPreferredPrice: '', heroSmsCountryId: HERO_SMS_COUNTRY_ID, heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL, heroSmsCountryFallback: [], fiveSimApiKey: '', fiveSimCountryId: FIVE_SIM_COUNTRY_ID, fiveSimCountryLabel: FIVE_SIM_COUNTRY_LABEL, fiveSimCountryFallback: [], fiveSimCountryOrder: [FIVE_SIM_COUNTRY_ID], fiveSimMaxPrice: '', fiveSimOperator: FIVE_SIM_OPERATOR, }; const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS); const SETTINGS_EXPORT_SCHEMA_VERSION = 1; const SETTINGS_EXPORT_FILENAME_PREFIX = 'multipage-settings'; const STEP6_PRE_LOGIN_COOKIE_CLEAR_DELAY_MS = 25000; const PRE_LOGIN_COOKIE_CLEAR_DOMAINS = [ 'chatgpt.com', 'chat.openai.com', 'openai.com', 'auth.openai.com', 'auth0.openai.com', 'accounts.openai.com', ]; const PRE_LOGIN_COOKIE_CLEAR_ORIGINS = [ 'https://chatgpt.com', 'https://chat.openai.com', 'https://auth.openai.com', 'https://auth0.openai.com', 'https://accounts.openai.com', 'https://openai.com', ]; const DEFAULT_STATE = { currentStep: 0, // 当前流程执行到的步骤编号。 stepStatuses: Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending'])), ...CONTRIBUTION_RUNTIME_DEFAULTS, oauthUrl: null, // 运行时抓取到的 OAuth 地址,不要手动预填。 email: null, // 运行时邮箱,由程序自动获取并写入,不能手动预填。 password: null, // 运行时实际密码,由 customPassword 或程序自动生成后写入。 accounts: [], // 已生成账号记录:{ email, password, createdAt }。 accountRunHistory: [], // 账号运行历史快照,实际持久化在 chrome.storage.local。 manualAliasUsage: {}, preservedAliases: {}, icloudAliasCache: [], icloudAliasCacheAt: 0, lastEmailTimestamp: null, // 最近一次获取到邮箱数据的运行时时间戳。 lastSignupCode: null, // 注册验证码,运行时由程序自动读取并写入。 lastLoginCode: null, // 登录验证码,运行时由程序自动读取并写入。 localhostUrl: null, // 运行时捕获到的 localhost 回调地址,不要手动预填。 cpaOAuthState: null, // CPA OAuth state。 cpaManagementOrigin: null, // CPA 管理接口 origin。 sub2apiSessionId: null, // SUB2API OpenAI Auth 会话 ID。 sub2apiOAuthState: null, // SUB2API OpenAI Auth state。 sub2apiGroupId: null, // SUB2API 目标分组 ID。 sub2apiDraftName: null, // SUB2API 本轮预生成的账号名称。 sub2apiProxyId: null, // SUB2API 本轮使用的代理 ID。 codex2apiSessionId: null, // Codex2API OAuth 会话 ID。 codex2apiOAuthState: null, // Codex2API OAuth state。 plusCheckoutTabId: null, // Plus checkout / PayPal 标签页 ID。 plusCheckoutUrl: null, // Plus checkout 运行时短链,不写入持久配置。 plusCheckoutCountry: 'DE', plusCheckoutCurrency: 'EUR', plusBillingCountryText: '', plusBillingAddress: null, plusPaypalApprovedAt: null, plusGoPayApprovedAt: null, plusReturnUrl: '', plusManualConfirmationPending: false, plusManualConfirmationRequestId: '', plusManualConfirmationStep: 0, plusManualConfirmationMethod: '', plusManualConfirmationTitle: '', plusManualConfirmationMessage: '', flowStartTime: null, // 当前流程开始时间。 tabRegistry: {}, // 程序维护的标签页注册表。 sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。 logs: [], // 侧边栏展示的运行日志。 ...PERSISTED_SETTING_DEFAULTS, // 合并 chrome.storage.local 中持久化保存的用户配置。 ipProxyApiPool: [], ipProxyApiCurrentIndex: 0, ipProxyApiCurrent: null, ipProxyAccountPool: [], ipProxyAccountCurrentIndex: 0, ipProxyAccountCurrent: null, ipProxyPool: [], ipProxyCurrentIndex: 0, ipProxyCurrent: null, luckmailApiKey: '', luckmailBaseUrl: DEFAULT_LUCKMAIL_BASE_URL, luckmailEmailType: DEFAULT_LUCKMAIL_EMAIL_TYPE, luckmailDomain: '', luckmailUsedPurchases: {}, luckmailPreserveTagId: 0, luckmailPreserveTagName: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, currentLuckmailPurchase: null, currentLuckmailMailCursor: null, currentPhoneActivation: null, currentPhoneVerificationCode: '', currentPhoneVerificationCountdownEndsAt: 0, currentPhoneVerificationCountdownWindowIndex: 0, currentPhoneVerificationCountdownWindowTotal: 0, reusablePhoneActivation: null, phoneReusableActivationPool: [], heroSmsLastPriceTiers: [], heroSmsLastPriceCountryId: 0, heroSmsLastPriceCountryLabel: '', heroSmsLastPriceUserLimit: '', heroSmsLastPriceAt: 0, pendingPhoneActivationConfirmation: null, autoRunning: false, // 当前是否处于自动运行中。 autoRunPhase: 'idle', // 当前自动运行阶段。 autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。 autoRunTotalRuns: 1, // 自动运行计划总轮数。 autoRunAttemptRun: 0, // 当前轮次的重试序号。 autoRunSessionId: 0, autoRunRoundSummaries: [], // 自动运行轮次摘要。 scheduledAutoRunAt: null, // 自动运行计划启动时间戳。 autoRunTimerPlan: null, // 自动运行可恢复计时计划快照。 autoRunCountdownAt: null, autoRunCountdownTitle: '', autoRunCountdownNote: '', signupVerificationRequestedAt: null, loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, currentPayPalAccountId: null, currentHotmailAccountId: null, currentMail2925AccountId: null, preferredIcloudHost: '', ipProxyApplied: false, ipProxyAppliedReason: 'disabled', ipProxyAppliedAt: 0, ipProxyAppliedHost: '', ipProxyAppliedPort: 0, ipProxyAppliedRegion: '', ipProxyAppliedHasAuth: false, ipProxyAppliedProvider: DEFAULT_IP_PROXY_SERVICE, ipProxyAppliedError: '', ipProxyAppliedWarning: '', ipProxyAppliedExitIp: '', ipProxyAppliedExitRegion: '', ipProxyAppliedExitDetecting: false, ipProxyAppliedExitError: '', ipProxyAppliedExitSource: '', }; function normalizeAutoRunDelayMinutes(value) { const numeric = Number(value); if (!Number.isFinite(numeric)) { return PERSISTED_SETTING_DEFAULTS.autoRunDelayMinutes; } return Math.min( AUTO_RUN_DELAY_MAX_MINUTES, Math.max(AUTO_RUN_DELAY_MIN_MINUTES, Math.floor(numeric)) ); } function normalizeAutoRunFallbackThreadIntervalMinutes(value) { const rawValue = String(value ?? '').trim(); if (!rawValue) { return 0; } const numeric = Number(rawValue); if (!Number.isFinite(numeric)) { return 0; } return Math.min( AUTO_RUN_DELAY_MAX_MINUTES, Math.max(0, Math.floor(numeric)) ); } function normalizeIpProxyAutoSyncIntervalMinutes(value, fallback = IP_PROXY_AUTO_SYNC_DEFAULT_INTERVAL_MINUTES) { const rawValue = String(value ?? '').trim(); if (!rawValue) { return Math.min( IP_PROXY_AUTO_SYNC_INTERVAL_MAX_MINUTES, Math.max(IP_PROXY_AUTO_SYNC_INTERVAL_MIN_MINUTES, Math.floor(Number(fallback) || IP_PROXY_AUTO_SYNC_DEFAULT_INTERVAL_MINUTES)) ); } const numeric = Number(rawValue); if (!Number.isFinite(numeric)) { return Math.min( IP_PROXY_AUTO_SYNC_INTERVAL_MAX_MINUTES, Math.max(IP_PROXY_AUTO_SYNC_INTERVAL_MIN_MINUTES, Math.floor(Number(fallback) || IP_PROXY_AUTO_SYNC_DEFAULT_INTERVAL_MINUTES)) ); } return Math.min( IP_PROXY_AUTO_SYNC_INTERVAL_MAX_MINUTES, Math.max(IP_PROXY_AUTO_SYNC_INTERVAL_MIN_MINUTES, Math.floor(numeric)) ); } function normalizeAutoStepDelaySeconds(value, fallback = null) { const rawValue = String(value ?? '').trim(); if (!rawValue) { return fallback; } const numeric = Number(rawValue); if (!Number.isFinite(numeric)) { return fallback; } return Math.min( AUTO_STEP_DELAY_MAX_ALLOWED_SECONDS, Math.max(AUTO_STEP_DELAY_MIN_ALLOWED_SECONDS, Math.floor(numeric)) ); } function normalizeVerificationResendCount(value, fallback) { const rawValue = String(value ?? '').trim(); if (!rawValue) { return fallback; } const numeric = Number(rawValue); if (!Number.isFinite(numeric)) { return fallback; } return Math.min( VERIFICATION_RESEND_COUNT_MAX, Math.max(VERIFICATION_RESEND_COUNT_MIN, Math.floor(numeric)) ); } 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 = '') { const normalized = String(value || '').trim().toLowerCase(); if (normalized === HERO_SMS_ACQUIRE_PRIORITY_PRICE) { return HERO_SMS_ACQUIRE_PRIORITY_PRICE; } if (normalized === HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH) { return HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH; } return 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 || !HERO_SMS_SUPPORTED_COUNTRY_ID_SET.has(String(countryId)) || seenIds.has(countryId)) { continue; } seenIds.add(countryId); normalized.push({ id: countryId, label: countryLabel || `Country #${countryId}`, }); if (normalized.length >= 20) { break; } } return normalized; } function normalizePhoneSmsProvider(value = '') { const rootScope = typeof self !== 'undefined' ? self : globalThis; if (rootScope.PhoneSmsProviderRegistry?.normalizeProviderId) { return rootScope.PhoneSmsProviderRegistry.normalizeProviderId(value); } const normalized = String(value || '').trim().toLowerCase(); return normalized === PHONE_SMS_PROVIDER_FIVE_SIM ? PHONE_SMS_PROVIDER_FIVE_SIM : PHONE_SMS_PROVIDER_HERO_SMS; } function normalizePlusPaymentMethod(value = '') { const rootScope = typeof self !== 'undefined' ? self : globalThis; if (rootScope.GoPayUtils?.normalizePlusPaymentMethod) { return rootScope.GoPayUtils.normalizePlusPaymentMethod(value); } return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL; } function normalizeFiveSimCountryId(value, fallback = FIVE_SIM_COUNTRY_ID) { const rootScope = typeof self !== 'undefined' ? self : globalThis; const rawNormalized = rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryId ? rootScope.PhoneSmsFiveSimProvider.normalizeFiveSimCountryId(value, '') : String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); const normalized = String(rawNormalized || '').trim().toLowerCase(); if (FIVE_SIM_SUPPORTED_COUNTRY_ID_SET.has(normalized)) { return normalized; } const fallbackSource = fallback === undefined || fallback === null ? FIVE_SIM_COUNTRY_ID : fallback; const normalizedFallback = String(fallbackSource).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); if (!normalizedFallback) { return ''; } return FIVE_SIM_SUPPORTED_COUNTRY_ID_SET.has(normalizedFallback) ? normalizedFallback : FIVE_SIM_COUNTRY_ID; } function normalizeFiveSimCountryLabel(value = '', fallback = FIVE_SIM_COUNTRY_LABEL) { const rootScope = typeof self !== 'undefined' ? self : globalThis; if (rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryLabel) { return rootScope.PhoneSmsFiveSimProvider.normalizeFiveSimCountryLabel(value, fallback); } if (rootScope.PhoneSmsFiveSimProvider?.formatFiveSimCountryLabel) { return rootScope.PhoneSmsFiveSimProvider.formatFiveSimCountryLabel('', value, fallback); } return String(value || '').trim() || fallback; } function normalizeFiveSimOperator(value = '', fallback = FIVE_SIM_OPERATOR) { const rootScope = typeof self !== 'undefined' ? self : globalThis; if (rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimOperator) { return rootScope.PhoneSmsFiveSimProvider.normalizeFiveSimOperator(value || fallback); } return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '') || fallback; } function normalizeFiveSimMaxPrice(value = '') { const rootScope = typeof self !== 'undefined' ? self : globalThis; if (rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimMaxPrice) { return rootScope.PhoneSmsFiveSimProvider.normalizeFiveSimMaxPrice(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 normalizeFiveSimCountryFallback(value = []) { const rootScope = typeof self !== 'undefined' ? self : globalThis; if (rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryFallback) { return rootScope.PhoneSmsFiveSimProvider.normalizeFiveSimCountryFallback(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 = ''; let countryLabel = ''; if (entry && typeof entry === 'object' && !Array.isArray(entry)) { countryId = normalizeFiveSimCountryId(entry.countryId ?? entry.id ?? entry.slug, ''); countryLabel = String((entry.countryLabel ?? entry.label ?? entry.name ?? entry.text_en) || '').trim(); } else { const text = String(entry || '').trim(); const structuredMatch = text.match(/^([a-z0-9_-]+)\s*(?:[:|/-]\s*(.+))?$/i); countryId = normalizeFiveSimCountryId(structuredMatch?.[1] || text, ''); countryLabel = String(structuredMatch?.[2] || '').trim(); } if (!countryId || seenIds.has(countryId)) { continue; } seenIds.add(countryId); normalized.push({ id: countryId, label: countryLabel || normalizeFiveSimCountryLabel('', countryId), }); if (normalized.length >= 20) { break; } } return normalized; } function resolveLegacyAutoStepDelaySeconds(input = {}) { const hasLegacyMin = input.autoStepRandomDelayMinSeconds !== undefined; const hasLegacyMax = input.autoStepRandomDelayMaxSeconds !== undefined; if (!hasLegacyMin && !hasLegacyMax) { return undefined; } const minSeconds = normalizeAutoStepDelaySeconds(input.autoStepRandomDelayMinSeconds, null); const maxSeconds = normalizeAutoStepDelaySeconds(input.autoStepRandomDelayMaxSeconds, null); if (minSeconds === null && maxSeconds === null) { return null; } if (minSeconds === null) { return maxSeconds; } if (maxSeconds === null) { return minSeconds; } return Math.round((minSeconds + maxSeconds) / 2); } function normalizeRunCount(value) { const numeric = Number(value); if (!Number.isFinite(numeric)) { return 1; } return Math.max(1, Math.floor(numeric)); } function normalizeAutoRunTimerKind(value = '') { const normalized = String(value || '').trim().toLowerCase(); if (normalized === AUTO_RUN_TIMER_KIND_SCHEDULED_START) { return AUTO_RUN_TIMER_KIND_SCHEDULED_START; } if (normalized === AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS) { return AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS; } if (normalized === AUTO_RUN_TIMER_KIND_BEFORE_RETRY) { return AUTO_RUN_TIMER_KIND_BEFORE_RETRY; } return ''; } function normalizeAutoRunSessionId(value) { const numeric = Math.floor(Number(value) || 0); return numeric > 0 ? numeric : 0; } function createAutoRunSessionId() { autoRunSessionSeed = Math.max(autoRunSessionSeed + 1, Date.now()); autoRunSessionId = autoRunSessionSeed; return autoRunSessionId; } function setCurrentAutoRunSessionId(value) { autoRunSessionId = normalizeAutoRunSessionId(value); return autoRunSessionId; } function clearCurrentAutoRunSessionId(expectedSessionId = null) { if (expectedSessionId === null) { autoRunSessionId = 0; return autoRunSessionId; } const normalizedExpected = normalizeAutoRunSessionId(expectedSessionId); if (!normalizedExpected || normalizedExpected === autoRunSessionId) { autoRunSessionId = 0; } return autoRunSessionId; } function isCurrentAutoRunSessionId(value) { const normalized = normalizeAutoRunSessionId(value); return normalized > 0 && normalized === autoRunSessionId; } function throwIfAutoRunSessionStopped(sessionId) { const normalizedSessionId = normalizeAutoRunSessionId(sessionId); if (normalizedSessionId && !isCurrentAutoRunSessionId(normalizedSessionId)) { throw new Error(STOP_ERROR_MESSAGE); } throwIfStopped(); } function normalizeAutoRunTimerPlan(plan) { if (!plan || typeof plan !== 'object' || Array.isArray(plan)) { return null; } const kind = normalizeAutoRunTimerKind(plan.kind); if (!kind) { return null; } const fireAt = Number(plan.fireAt); if (!Number.isFinite(fireAt)) { return null; } const totalRuns = normalizeRunCount(plan.totalRuns); const autoRunSkipFailures = Boolean(plan.autoRunSkipFailures); const mode = plan.mode === 'continue' ? 'continue' : 'restart'; const currentRun = Math.max(0, Math.min(totalRuns, Math.floor(Number(plan.currentRun) || 0))); const attemptRun = Math.max( 0, Math.min(AUTO_RUN_MAX_RETRIES_PER_ROUND + 1, Math.floor(Number(plan.attemptRun) || 0)) ); const autoRunSessionId = normalizeAutoRunSessionId(plan.autoRunSessionId ?? plan.sessionId); const roundSummaries = serializeAutoRunRoundSummaries(totalRuns, plan.roundSummaries); const countdownTitle = String(plan.countdownTitle || '').trim(); const countdownNote = String(plan.countdownNote || '').trim(); if (kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START) { return { kind, fireAt, totalRuns, autoRunSkipFailures, mode, currentRun: 0, attemptRun: 0, autoRunSessionId, roundSummaries: [], countdownTitle: countdownTitle || '已计划自动运行', countdownNote: countdownNote || `计划于 ${formatAutoRunScheduleTime(fireAt)} 开始`, }; } if (kind === AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS) { const normalizedCurrentRun = Math.max(1, Math.min(totalRuns, currentRun)); const normalizedAttemptRun = Math.max(1, attemptRun); return { kind, fireAt, totalRuns, autoRunSkipFailures, mode: 'restart', currentRun: normalizedCurrentRun, attemptRun: normalizedAttemptRun, autoRunSessionId, roundSummaries, countdownTitle: countdownTitle || '线程间隔中', countdownNote: countdownNote || `第 ${Math.min(normalizedCurrentRun + 1, totalRuns)}/${totalRuns} 轮即将开始`, }; } const normalizedCurrentRun = Math.max(1, Math.min(totalRuns, currentRun)); const normalizedAttemptRun = Math.max(1, attemptRun); return { kind, fireAt, totalRuns, autoRunSkipFailures, mode: 'restart', currentRun: normalizedCurrentRun, attemptRun: normalizedAttemptRun, autoRunSessionId, roundSummaries, countdownTitle: countdownTitle || '线程间隔中', countdownNote: countdownNote || `第 ${normalizedCurrentRun}/${totalRuns} 轮第 ${normalizedAttemptRun} 次尝试即将开始`, }; } function normalizeAutoRunTimerPlanFromState(state = {}) { const directPlan = normalizeAutoRunTimerPlan(state.autoRunTimerPlan); if (directPlan) { return directPlan; } if (state.autoRunPhase !== 'scheduled') { return null; } const legacyScheduledAt = Number(state.scheduledAutoRunAt); if (!Number.isFinite(legacyScheduledAt)) { return null; } return normalizeAutoRunTimerPlan({ kind: AUTO_RUN_TIMER_KIND_SCHEDULED_START, fireAt: legacyScheduledAt, totalRuns: state.scheduledAutoRunPlan?.totalRuns ?? state.autoRunTotalRuns, autoRunSkipFailures: state.scheduledAutoRunPlan?.autoRunSkipFailures ?? state.autoRunSkipFailures, autoRunSessionId: state.autoRunSessionId, mode: state.scheduledAutoRunPlan?.mode, }); } function getAutoRunTimerPlanPhase(kind = '') { return kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START ? 'scheduled' : 'waiting_interval'; } function getAutoRunTimerStatusPayload(plan) { const normalizedPlan = normalizeAutoRunTimerPlan(plan); if (!normalizedPlan) { return null; } const phase = getAutoRunTimerPlanPhase(normalizedPlan.kind); return { phase, currentRun: normalizedPlan.currentRun, totalRuns: normalizedPlan.totalRuns, attemptRun: normalizedPlan.attemptRun, sessionId: normalizedPlan.autoRunSessionId, scheduledAt: phase === 'scheduled' ? normalizedPlan.fireAt : null, countdownAt: normalizedPlan.fireAt, countdownTitle: normalizedPlan.countdownTitle, countdownNote: normalizedPlan.countdownNote, }; } function normalizeEmailGenerator(value = '') { const normalized = String(value || '').trim().toLowerCase(); const customEmailPoolGenerator = typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string' ? CUSTOM_EMAIL_POOL_GENERATOR : 'custom-pool'; const gmailAliasGenerator = typeof GMAIL_ALIAS_GENERATOR === 'string' ? GMAIL_ALIAS_GENERATOR : 'gmail-alias'; if (normalized === 'custom' || normalized === 'manual') { return 'custom'; } if (normalized === gmailAliasGenerator) { return gmailAliasGenerator; } if (normalized === customEmailPoolGenerator) { return customEmailPoolGenerator; } if (normalized === 'icloud') { return 'icloud'; } if (normalized === 'cloudflare') return 'cloudflare'; if (normalized === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return CLOUDFLARE_TEMP_EMAIL_GENERATOR; return 'duck'; } function normalizeIcloudFetchMode(value = '') { const normalized = String(value || '').trim().toLowerCase(); return normalized === 'always_new' ? 'always_new' : 'reuse_existing'; } function normalizeCustomEmailPool(value = []) { const source = Array.isArray(value) ? value : String(value || '').split(/[\r\n,,;;]+/); return source .map((item) => String(item || '').trim().toLowerCase()) .filter((item) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(item)); } function normalizeCustomEmailPoolEntryObjects(value = []) { const source = Array.isArray(value) ? value : []; const seenEmails = new Set(); const entries = []; for (const rawEntry of source) { const asObject = rawEntry && typeof rawEntry === 'object' ? rawEntry : { email: rawEntry }; const email = String(asObject.email || '').trim().toLowerCase(); if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { continue; } if (seenEmails.has(email)) { continue; } seenEmails.add(email); entries.push({ id: String(asObject.id || `custom-pool-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`), email, enabled: asObject.enabled !== undefined ? Boolean(asObject.enabled) : true, used: Boolean(asObject.used), note: String(asObject.note || '').trim(), lastUsedAt: Number.isFinite(Number(asObject.lastUsedAt)) ? Number(asObject.lastUsedAt) : 0, }); } return entries; } function isCustomEmailPoolGenerator(stateOrValue = {}) { const generator = typeof stateOrValue === 'string' ? stateOrValue : stateOrValue?.emailGenerator; const customEmailPoolGenerator = typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string' ? CUSTOM_EMAIL_POOL_GENERATOR : 'custom-pool'; return normalizeEmailGenerator(generator) === customEmailPoolGenerator; } function getCustomEmailPool(state = {}) { if (typeof normalizeCustomEmailPoolEntryObjects === 'function') { const entries = normalizeCustomEmailPoolEntryObjects(state?.customEmailPoolEntries); if (entries.length > 0) { return entries .filter((entry) => entry.enabled && !entry.used) .map((entry) => entry.email); } } return normalizeCustomEmailPool(state?.customEmailPool); } function getCustomEmailPoolEntries(state = {}) { const entries = normalizeCustomEmailPoolEntryObjects(state?.customEmailPoolEntries); if (entries.length > 0) { return entries; } return normalizeCustomEmailPool(state?.customEmailPool).map((email) => ({ id: `custom-pool-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`, email, enabled: true, used: false, note: '', lastUsedAt: 0, })); } async function markCurrentCustomEmailPoolEntryUsed(state = {}, options = {}) { if (!isCustomEmailPoolGenerator(state)) { return { updated: false }; } const currentEmail = String(state?.email || '').trim().toLowerCase(); if (!currentEmail) { return { updated: false }; } const entries = getCustomEmailPoolEntries(state); if (!entries.length) { return { updated: false }; } let changed = false; const now = Date.now(); const nextEntries = entries.map((entry) => { if (entry.email !== currentEmail) { return entry; } if (entry.used && entry.lastUsedAt) { return entry; } changed = true; return { ...entry, used: true, lastUsedAt: now, }; }); if (!changed) { return { updated: false }; } const nextCustomEmailPool = nextEntries .filter((entry) => entry.enabled && !entry.used) .map((entry) => entry.email); await setPersistentSettings({ customEmailPoolEntries: nextEntries, customEmailPool: nextCustomEmailPool, }); await setState({ customEmailPoolEntries: nextEntries, customEmailPool: nextCustomEmailPool, }); broadcastDataUpdate({ customEmailPoolEntries: nextEntries, customEmailPool: nextCustomEmailPool, }); const logPrefix = String(options.logPrefix || '').trim() || '自定义邮箱池:流程成功后'; await addLog(`${logPrefix}已将 ${currentEmail} 标记为已用。`, options.level || 'ok'); return { updated: true, customEmailPoolEntries: nextEntries, customEmailPool: nextCustomEmailPool, }; } async function markCurrentRegistrationAccountUsed(state = {}, options = {}) { const providedState = state && typeof state === 'object' ? state : {}; const currentState = await getState(); const latestState = { ...providedState, ...(currentState && typeof currentState === 'object' ? currentState : {}), }; const reasonPrefix = String(options.logPrefix || '').trim() || '当前账号'; let updated = false; if (latestState.currentHotmailAccountId && isHotmailProvider(latestState)) { await patchHotmailAccount(latestState.currentHotmailAccountId, { used: true, lastUsedAt: Date.now(), }); await addLog(`${reasonPrefix}:Hotmail 账号已标记为已用。`, options.level || 'warn'); updated = true; } if (isLuckmailProvider(latestState)) { const currentPurchase = getCurrentLuckmailPurchase(latestState); if (currentPurchase?.id) { await setLuckmailPurchaseUsedState(currentPurchase.id, true); await clearLuckmailRuntimeState({ clearEmail: true }); await addLog(`${reasonPrefix}:LuckMail 邮箱 ${currentPurchase.email_address} 已标记为已用。`, options.level || 'warn'); updated = true; } } if (String(latestState.mailProvider || '').trim().toLowerCase() === '2925' && latestState.currentMail2925AccountId) { await patchMail2925Account(latestState.currentMail2925AccountId, { lastUsedAt: Date.now(), lastError: '', }); await addLog(`${reasonPrefix}:2925 账号已记录最近使用时间。`, options.level || 'warn'); updated = true; } const icloudResult = await finalizeIcloudAliasAfterSuccessfulFlow(latestState); updated = Boolean(icloudResult?.handled) || updated; if (typeof markCurrentCustomEmailPoolEntryUsed === 'function') { const result = await markCurrentCustomEmailPoolEntryUsed(latestState, { logPrefix: `${reasonPrefix}:自定义邮箱池`, level: options.level || 'warn', }); updated = Boolean(result?.updated) || updated; } return { updated }; } function getCustomEmailPoolEmailForRun(state = {}, targetRun = 1) { const entries = getCustomEmailPool(state); const numericRun = Math.max(1, Math.floor(Number(targetRun) || 1)); return entries[numericRun - 1] || ''; } function getCustomMailProviderPool(state = {}) { return normalizeCustomEmailPool(state?.customMailProviderPool); } function getCustomMailProviderPoolEmailForRun(state = {}, targetRun = 1) { const entries = getCustomMailProviderPool(state); const numericRun = Math.max(1, Math.floor(Number(targetRun) || 1)); return entries[numericRun - 1] || ''; } function normalizePanelMode(value = '') { const normalized = String(value || '').trim().toLowerCase(); if (normalized === 'sub2api') { return 'sub2api'; } if (normalized === 'codex2api') { return 'codex2api'; } return 'cpa'; } function normalizeMailProvider(value = '') { const normalized = String(value || '').trim().toLowerCase(); switch (normalized) { case 'custom': case ICLOUD_PROVIDER: case GMAIL_PROVIDER: case HOTMAIL_PROVIDER: case LUCKMAIL_PROVIDER: case CLOUDFLARE_TEMP_EMAIL_PROVIDER: case '163': case '163-vip': case '126': case 'qq': case 'inbucket': case '2925': return normalized; default: return PERSISTED_SETTING_DEFAULTS.mailProvider; } } function buildLuckmailSessionSettingsPayload(input = {}) { if (!input || typeof input !== 'object' || Array.isArray(input)) { return {}; } const payload = {}; if (input.luckmailApiKey !== undefined) { payload.luckmailApiKey = String(input.luckmailApiKey || ''); } if (input.luckmailBaseUrl !== undefined) { payload.luckmailBaseUrl = normalizeLuckmailBaseUrl(input.luckmailBaseUrl); } if (input.luckmailEmailType !== undefined) { payload.luckmailEmailType = normalizeLuckmailEmailType(input.luckmailEmailType); } if (input.luckmailDomain !== undefined) { payload.luckmailDomain = String(input.luckmailDomain || '').trim(); } if (input.luckmailUsedPurchases !== undefined) { payload.luckmailUsedPurchases = normalizeLuckmailUsedPurchases(input.luckmailUsedPurchases); } if (input.luckmailPreserveTagId !== undefined) { payload.luckmailPreserveTagId = Number(input.luckmailPreserveTagId) || 0; } if (input.luckmailPreserveTagName !== undefined) { payload.luckmailPreserveTagName = String(input.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME; } if (input.currentLuckmailPurchase !== undefined) { payload.currentLuckmailPurchase = input.currentLuckmailPurchase ? normalizeLuckmailPurchase(input.currentLuckmailPurchase) : null; } if (input.currentLuckmailMailCursor !== undefined) { payload.currentLuckmailMailCursor = input.currentLuckmailMailCursor ? normalizeLuckmailMailCursor(input.currentLuckmailMailCursor) : null; } return payload; } function normalizeMail2925Mode(value = '') { return String(value || '').trim().toLowerCase() === MAIL_2925_MODE_RECEIVE ? MAIL_2925_MODE_RECEIVE : DEFAULT_MAIL_2925_MODE; } function normalizeLocalCpaStep9Mode(value = '') { return String(value || '').trim().toLowerCase() === 'bypass' ? 'bypass' : DEFAULT_LOCAL_CPA_STEP9_MODE; } function normalizeCloudflareDomain(rawValue = '') { let value = String(rawValue || '').trim().toLowerCase(); if (!value) return ''; value = value.replace(/^@+/, ''); value = value.replace(/^https?:\/\//, ''); value = value.replace(/\/.*$/, ''); if (!/^[a-z0-9.-]+\.[a-z]{2,}$/.test(value)) return ''; return value; } function normalizeCloudflareDomains(values) { const normalizedDomains = []; const seen = new Set(); for (const value of Array.isArray(values) ? values : []) { const normalized = normalizeCloudflareDomain(value); if (!normalized || seen.has(normalized)) continue; seen.add(normalized); normalizedDomains.push(normalized); } return normalizedDomains; } function normalizeHotmailRemoteBaseUrl(rawValue = '') { const value = String(rawValue || '').trim(); if (!value) return DEFAULT_HOTMAIL_REMOTE_BASE_URL; try { const parsed = new URL(value); if (!['http:', 'https:'].includes(parsed.protocol)) { return DEFAULT_HOTMAIL_REMOTE_BASE_URL; } if (parsed.pathname.endsWith('/api/mail-new') || parsed.pathname.endsWith('/api/mail-all') || parsed.pathname === '/api.html') { parsed.pathname = ''; parsed.search = ''; parsed.hash = ''; } return parsed.toString().replace(/\/$/, ''); } catch { return DEFAULT_HOTMAIL_REMOTE_BASE_URL; } } function normalizeHotmailLocalBaseUrl(rawValue = '') { const value = String(rawValue || '').trim(); if (!value) return DEFAULT_HOTMAIL_LOCAL_BASE_URL; try { const parsed = new URL(value); if (!['http:', 'https:'].includes(parsed.protocol)) { return DEFAULT_HOTMAIL_LOCAL_BASE_URL; } if (['/messages', '/code', '/clear', '/token'].includes(parsed.pathname)) { parsed.pathname = ''; parsed.search = ''; parsed.hash = ''; } return parsed.toString().replace(/\/$/, ''); } catch { return DEFAULT_HOTMAIL_LOCAL_BASE_URL; } } function normalizeAccountRunHistoryHelperBaseUrl(rawValue = '') { const value = String(rawValue || '').trim(); if (!value) return DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL; try { const parsed = new URL(value); if (parsed.pathname === '/append-account-log' || parsed.pathname === '/sync-account-run-records') { parsed.pathname = ''; parsed.search = ''; parsed.hash = ''; } return normalizeHotmailLocalBaseUrl(parsed.toString()); } catch { return normalizeHotmailLocalBaseUrl(value); } } function getHotmailServiceSettings(state = {}) { return { mode: normalizeHotmailServiceMode(state.hotmailServiceMode), remoteBaseUrl: normalizeHotmailRemoteBaseUrl(state.hotmailRemoteBaseUrl), localBaseUrl: normalizeHotmailLocalBaseUrl(state.hotmailLocalBaseUrl), }; } function getCloudflareTempEmailConfig(state = {}) { return { baseUrl: normalizeCloudflareTempEmailBaseUrl(state.cloudflareTempEmailBaseUrl), adminAuth: String(state.cloudflareTempEmailAdminAuth || ''), customAuth: String(state.cloudflareTempEmailCustomAuth || ''), receiveMailbox: normalizeCloudflareTempEmailReceiveMailbox(state.cloudflareTempEmailReceiveMailbox), useRandomSubdomain: Boolean(state.cloudflareTempEmailUseRandomSubdomain), domain: normalizeCloudflareTempEmailDomain(state.cloudflareTempEmailDomain), domains: normalizeCloudflareTempEmailDomains(state.cloudflareTempEmailDomains), }; } function normalizeCloudflareTempEmailReceiveMailbox(value = '') { const normalized = normalizeCloudflareTempEmailAddress(value); if (!normalized) return ''; return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized) ? normalized : ''; } function resolveCloudflareTempEmailPollTargetEmail(state = {}, pollPayload = {}, config = getCloudflareTempEmailConfig(state)) { const configuredReceiveMailbox = normalizeCloudflareTempEmailReceiveMailbox(config.receiveMailbox); const mailProvider = String(state?.mailProvider || '').trim().toLowerCase(); const emailGenerator = String(state?.emailGenerator || '').trim().toLowerCase(); const shouldPreferConfiguredReceiveMailbox = mailProvider === 'cloudflare-temp-email' && emailGenerator !== 'cloudflare-temp-email'; if (shouldPreferConfiguredReceiveMailbox && configuredReceiveMailbox) { return configuredReceiveMailbox; } const requestedTarget = normalizeCloudflareTempEmailReceiveMailbox(pollPayload.targetEmail); if (requestedTarget) { return requestedTarget; } return normalizeCloudflareTempEmailReceiveMailbox(state.email); } function normalizePersistentSettingValue(key, value) { switch (key) { case 'panelMode': return normalizePanelMode(value); case 'vpsUrl': return String(value || '').trim(); case 'vpsPassword': return String(value || ''); case 'localCpaStep9Mode': return normalizeLocalCpaStep9Mode(value); case 'sub2apiUrl': return String(value || '').trim(); case 'sub2apiEmail': return String(value || '').trim(); case 'sub2apiPassword': return String(value || ''); case 'sub2apiGroupName': return String(value || '').trim(); case 'sub2apiDefaultProxyName': return String(value || '').trim(); case 'ipProxyEnabled': return Boolean(value); case 'ipProxyService': return normalizeIpProxyProviderValue(value); case 'ipProxyMode': return normalizeIpProxyMode(value); case 'ipProxyApiUrl': return String(value || '').trim(); case 'ipProxyServiceProfiles': return normalizeIpProxyServiceProfiles(value || {}, PERSISTED_SETTING_DEFAULTS); case 'ipProxyAccountList': return normalizeIpProxyAccountList(value || ''); case 'ipProxyAccountSessionPrefix': return normalizeIpProxyAccountSessionPrefix(value || ''); case 'ipProxyAccountLifeMinutes': return normalizeIpProxyAccountLifeMinutes(value || ''); case 'ipProxyPoolTargetCount': return normalizeIpProxyPoolTargetCount(value || '', 20); case 'ipProxyAutoSyncEnabled': return Boolean(value); case 'ipProxyAutoSyncIntervalMinutes': return normalizeIpProxyAutoSyncIntervalMinutes( value, PERSISTED_SETTING_DEFAULTS.ipProxyAutoSyncIntervalMinutes ); case 'ipProxyHost': return String(value || '').trim(); case 'ipProxyPort': return String(normalizeIpProxyPort(value || '') || ''); case 'ipProxyProtocol': return normalizeIpProxyProtocol(value); case 'ipProxyUsername': return String(value || '').trim(); case 'ipProxyPassword': return String(value || ''); case 'ipProxyRegion': return String(value || '').trim(); case 'ipProxyApiPool': return normalizeProxyPoolEntries( value, normalizeIpProxyProviderValue(value?.provider || DEFAULT_IP_PROXY_SERVICE) ); case 'ipProxyApiCurrentIndex': return normalizeIpProxyCurrentIndex(value, 0); case 'ipProxyApiCurrent': return normalizeProxyPoolEntries(value ? [value] : [], DEFAULT_IP_PROXY_SERVICE)[0] || null; case 'ipProxyAccountPool': return normalizeProxyPoolEntries( value, normalizeIpProxyProviderValue(value?.provider || DEFAULT_IP_PROXY_SERVICE) ); case 'ipProxyAccountCurrentIndex': return normalizeIpProxyCurrentIndex(value, 0); case 'ipProxyAccountCurrent': return normalizeProxyPoolEntries(value ? [value] : [], DEFAULT_IP_PROXY_SERVICE)[0] || null; case 'ipProxyPool': return normalizeProxyPoolEntries( value, normalizeIpProxyProviderValue(value?.provider || DEFAULT_IP_PROXY_SERVICE) ); case 'ipProxyCurrentIndex': return normalizeIpProxyCurrentIndex(value, 0); case 'ipProxyCurrent': return normalizeProxyPoolEntries(value ? [value] : [], DEFAULT_IP_PROXY_SERVICE)[0] || null; case 'codex2apiUrl': return normalizeCodex2ApiUrl(value); case 'codex2apiAdminKey': return String(value || '').trim(); case 'customPassword': return String(value || ''); case 'plusPaymentMethod': return normalizePlusPaymentMethod(value); case 'paypalEmail': return String(value || '').trim(); case 'paypalPassword': return String(value || ''); case 'currentPayPalAccountId': return String(value || '').trim(); case 'gopayCountryCode': return self.GoPayUtils?.normalizeGoPayCountryCode ? self.GoPayUtils.normalizeGoPayCountryCode(value) : String(value || '+86').trim(); case 'gopayPhone': return self.GoPayUtils?.normalizeGoPayPhone ? self.GoPayUtils.normalizeGoPayPhone(value) : String(value || '').trim(); case 'gopayOtp': return self.GoPayUtils?.normalizeGoPayOtp ? self.GoPayUtils.normalizeGoPayOtp(value) : String(value || '').trim().replace(/[^\d]/g, ''); case 'gopayPin': return self.GoPayUtils?.normalizeGoPayPin ? self.GoPayUtils.normalizeGoPayPin(value) : String(value || ''); case 'autoRunSkipFailures': case 'oauthFlowTimeoutEnabled': case 'autoRunDelayEnabled': case 'phoneVerificationEnabled': case 'plusModeEnabled': return Boolean(value); case 'phoneSmsProvider': return normalizePhoneSmsProvider(value); case 'phoneSmsProviderOrder': return normalizePhoneSmsProviderOrder(value); case 'autoRunFallbackThreadIntervalMinutes': return normalizeAutoRunFallbackThreadIntervalMinutes(value); case 'autoRunDelayMinutes': return normalizeAutoRunDelayMinutes(value); case 'autoStepDelaySeconds': return normalizeAutoStepDelaySeconds(value, PERSISTED_SETTING_DEFAULTS.autoStepDelaySeconds); case 'verificationResendCount': 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': return normalizeMailProvider(value); case 'mail2925Mode': return normalizeMail2925Mode(value); case 'mail2925UseAccountPool': return Boolean(value); case 'emailGenerator': return normalizeEmailGenerator(value); case 'customMailProviderPool': case 'customEmailPool': return normalizeCustomEmailPool(value); case 'customEmailPoolEntries': return normalizeCustomEmailPoolEntryObjects(value); case 'autoDeleteUsedIcloudAlias': case 'accountRunHistoryTextEnabled': case 'cloudflareTempEmailUseRandomSubdomain': return Boolean(value); case 'icloudHostPreference': return normalizeIcloudHost(value) || 'auto'; case 'icloudTargetMailboxType': return normalizeIcloudTargetMailboxType(value); case 'icloudForwardMailProvider': return normalizeIcloudForwardMailProvider(value); case 'icloudFetchMode': return normalizeIcloudFetchMode(value); case 'accountRunHistoryHelperBaseUrl': return normalizeAccountRunHistoryHelperBaseUrl(value); case 'gmailBaseEmail': case 'mail2925BaseEmail': case 'currentMail2925AccountId': case 'emailPrefix': return String(value || '').trim(); case 'inbucketHost': return String(value || '').trim(); case 'inbucketMailbox': return String(value || '').trim(); case 'hotmailServiceMode': return normalizeHotmailServiceMode(value); case 'hotmailRemoteBaseUrl': return normalizeHotmailRemoteBaseUrl(value); case 'hotmailLocalBaseUrl': return normalizeHotmailLocalBaseUrl(value); case 'luckmailApiKey': return String(value || ''); case 'luckmailBaseUrl': return normalizeLuckmailBaseUrl(value); case 'luckmailEmailType': return normalizeLuckmailEmailType(value); case 'luckmailDomain': return String(value || '').trim(); case 'luckmailUsedPurchases': return normalizeLuckmailUsedPurchases(value); case 'luckmailPreserveTagId': return Number(value) || 0; case 'luckmailPreserveTagName': return String(value || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME; case 'cloudflareDomain': return normalizeCloudflareDomain(value); case 'cloudflareDomains': return normalizeCloudflareDomains(value); case 'cloudflareTempEmailBaseUrl': return normalizeCloudflareTempEmailBaseUrl(value); case 'cloudflareTempEmailAdminAuth': case 'cloudflareTempEmailCustomAuth': return String(value || ''); case 'cloudflareTempEmailReceiveMailbox': return normalizeCloudflareTempEmailReceiveMailbox(value); case 'cloudflareTempEmailDomain': return normalizeCloudflareTempEmailDomain(value); case 'cloudflareTempEmailDomains': return normalizeCloudflareTempEmailDomains(value); case 'hotmailAccounts': return normalizeHotmailAccounts(value); case 'mail2925Accounts': return normalizeMail2925Accounts(value); case 'paypalAccounts': return normalizePayPalAccounts(value); case 'phoneSmsProvider': return normalizePhoneSmsProvider(value); case 'heroSmsApiKey': return String(value || ''); case 'heroSmsReuseEnabled': return Boolean(value); case 'heroSmsAcquirePriority': return normalizeHeroSmsAcquirePriority(value); case 'heroSmsMaxPrice': return normalizeHeroSmsMaxPrice(value); case 'heroSmsCountryId': { const parsed = Math.floor(Number(value)); if (Number.isFinite(parsed) && HERO_SMS_SUPPORTED_COUNTRY_ID_SET.has(String(parsed))) { return parsed; } return HERO_SMS_COUNTRY_ID; } case 'heroSmsCountryLabel': return String(value || HERO_SMS_COUNTRY_LABEL).trim() || HERO_SMS_COUNTRY_LABEL; case 'heroSmsCountryFallback': return normalizeHeroSmsCountryFallback(value); case 'fiveSimApiKey': return String(value || ''); case 'fiveSimCountryId': return normalizeFiveSimCountryId(value); case 'fiveSimCountryLabel': return normalizeFiveSimCountryLabel(value); case 'fiveSimCountryFallback': return normalizeFiveSimCountryFallback(value); case 'fiveSimCountryOrder': return normalizeFiveSimCountryFallback(value) .map((entry) => entry.id) .filter(Boolean); case 'fiveSimMaxPrice': return normalizeFiveSimMaxPrice(value); case 'fiveSimOperator': return normalizeFiveSimOperator(value); default: return value; } } function buildPersistentSettingsPayload(input = {}, options = {}) { const { fillDefaults = false, requireKnownKeys = false } = options; if (!input || typeof input !== 'object' || Array.isArray(input)) { throw new Error('\u914d\u7f6e\u5185\u5bb9\u683c\u5f0f\u65e0\u6548\u3002'); } const normalizedInput = { ...input }; if (normalizedInput.autoStepDelaySeconds === undefined) { const legacyAutoStepDelaySeconds = resolveLegacyAutoStepDelaySeconds(normalizedInput); if (legacyAutoStepDelaySeconds !== undefined) { normalizedInput.autoStepDelaySeconds = legacyAutoStepDelaySeconds; } } if (normalizedInput.verificationResendCount === undefined) { const legacyVerificationResendCount = normalizedInput.signupVerificationResendCount !== undefined ? normalizedInput.signupVerificationResendCount : normalizedInput.loginVerificationResendCount; if (legacyVerificationResendCount !== undefined) { normalizedInput.verificationResendCount = legacyVerificationResendCount; } } const payload = {}; let matchedKeyCount = 0; for (const key of PERSISTED_SETTING_KEYS) { if (normalizedInput[key] !== undefined) { payload[key] = normalizePersistentSettingValue(key, normalizedInput[key]); matchedKeyCount += 1; } else if (fillDefaults) { payload[key] = normalizePersistentSettingValue(key, PERSISTED_SETTING_DEFAULTS[key]); } } if (requireKnownKeys && matchedKeyCount === 0) { throw new Error('\u914d\u7f6e\u6587\u4ef6\u4e2d\u6ca1\u6709\u53ef\u8bc6\u522b\u7684\u914d\u7f6e\u5185\u5bb9\u3002'); } if (payload.cloudflareDomains) { const domains = normalizeCloudflareDomains(payload.cloudflareDomains); if (payload.cloudflareDomain && !domains.includes(payload.cloudflareDomain)) { domains.unshift(payload.cloudflareDomain); } payload.cloudflareDomains = domains; } if (payload.cloudflareTempEmailDomains) { const domains = normalizeCloudflareTempEmailDomains(payload.cloudflareTempEmailDomains); if (payload.cloudflareTempEmailDomain && !domains.includes(payload.cloudflareTempEmailDomain)) { domains.unshift(payload.cloudflareTempEmailDomain); } payload.cloudflareTempEmailDomains = domains; } if (payload.ipProxyServiceProfiles) { const selectedService = normalizeIpProxyProviderValue( payload.ipProxyService || PERSISTED_SETTING_DEFAULTS.ipProxyService ); const normalizedProfiles = normalizeIpProxyServiceProfiles(payload.ipProxyServiceProfiles, { ...PERSISTED_SETTING_DEFAULTS, ...payload, }); payload.ipProxyServiceProfiles = normalizedProfiles; const activeProfile = normalizedProfiles[selectedService] || buildIpProxyServiceProfileFromState({ ...PERSISTED_SETTING_DEFAULTS, ...payload, }); payload.ipProxyService = selectedService; payload.ipProxyMode = normalizeIpProxyMode(activeProfile?.mode || payload.ipProxyMode); payload.ipProxyApiUrl = String(activeProfile?.apiUrl || payload.ipProxyApiUrl || '').trim(); payload.ipProxyAccountList = normalizeIpProxyAccountList(activeProfile?.accountList || payload.ipProxyAccountList || ''); payload.ipProxyAccountSessionPrefix = normalizeIpProxyAccountSessionPrefix(activeProfile?.accountSessionPrefix || payload.ipProxyAccountSessionPrefix || ''); payload.ipProxyAccountLifeMinutes = normalizeIpProxyAccountLifeMinutes(activeProfile?.accountLifeMinutes || payload.ipProxyAccountLifeMinutes || ''); payload.ipProxyPoolTargetCount = normalizeIpProxyPoolTargetCount(activeProfile?.poolTargetCount || payload.ipProxyPoolTargetCount || '', 20); payload.ipProxyHost = String(activeProfile?.host || payload.ipProxyHost || '').trim(); payload.ipProxyPort = String(normalizeIpProxyPort(activeProfile?.port || payload.ipProxyPort || '') || ''); payload.ipProxyProtocol = normalizeIpProxyProtocol(activeProfile?.protocol || payload.ipProxyProtocol); payload.ipProxyUsername = String(activeProfile?.username || payload.ipProxyUsername || '').trim(); payload.ipProxyPassword = String(activeProfile?.password || payload.ipProxyPassword || ''); payload.ipProxyRegion = String(activeProfile?.region || payload.ipProxyRegion || '').trim(); } return payload; } async function getPersistedSettings() { const stored = await chrome.storage.local.get([ ...PERSISTED_SETTING_KEYS, ...LEGACY_AUTO_STEP_DELAY_KEYS, ...LEGACY_VERIFICATION_RESEND_COUNT_KEYS, ]); return buildPersistentSettingsPayload(stored, { fillDefaults: true }); } async function getPersistedAliasState() { try { const stored = await chrome.storage.local.get(PERSISTENT_ALIAS_STATE_KEYS); const manualAliasUsage = normalizeBooleanMap(stored.manualAliasUsage); const preservedAliases = normalizeBooleanMap(stored.preservedAliases); return { manualAliasUsage, preservedAliases, icloudAliasCache: normalizeIcloudAliasCacheList(stored.icloudAliasCache, { usedEmails: toNormalizedEmailSet(manualAliasUsage), preservedEmails: toNormalizedEmailSet(preservedAliases), }), icloudAliasCacheAt: Math.max(0, Number(stored.icloudAliasCacheAt) || 0), }; } catch (err) { console.warn(LOG_PREFIX, 'Failed to read persisted iCloud alias state:', err?.message || err); return { manualAliasUsage: {}, preservedAliases: {}, icloudAliasCache: [], icloudAliasCacheAt: 0, }; } } async function getState() { const [state, persistedSettings, persistedAliasState, accountRunHistory] = await Promise.all([ chrome.storage.session.get(null), getPersistedSettings(), getPersistedAliasState(), accountRunHistoryHelpers?.getPersistedAccountRunHistory?.() || [], ]); return { ...DEFAULT_STATE, ...persistedSettings, ...persistedAliasState, ...state, accountRunHistory }; } async function initializeSessionStorageAccess() { try { if (chrome.storage?.session?.setAccessLevel) { await chrome.storage.session.setAccessLevel({ accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS', }); console.log(LOG_PREFIX, 'Enabled storage.session for content scripts'); } } catch (err) { console.warn(LOG_PREFIX, 'Failed to enable storage.session for content scripts:', err?.message || err); } } async function setState(updates) { console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200)); if (Object.keys(updates || {}).length > 0) { await chrome.storage.session.set(updates); const persistentAliasUpdates = {}; if (Object.prototype.hasOwnProperty.call(updates, 'manualAliasUsage')) { persistentAliasUpdates.manualAliasUsage = normalizeBooleanMap(updates.manualAliasUsage); } if (Object.prototype.hasOwnProperty.call(updates, 'preservedAliases')) { persistentAliasUpdates.preservedAliases = normalizeBooleanMap(updates.preservedAliases); } if (Object.prototype.hasOwnProperty.call(updates, 'icloudAliasCache')) { persistentAliasUpdates.icloudAliasCache = normalizeIcloudAliasCacheList(updates.icloudAliasCache); } if (Object.prototype.hasOwnProperty.call(updates, 'icloudAliasCacheAt')) { persistentAliasUpdates.icloudAliasCacheAt = Math.max(0, Number(updates.icloudAliasCacheAt) || 0); } if (Object.keys(persistentAliasUpdates).length > 0) { await chrome.storage.local.set(persistentAliasUpdates); } } } async function setPersistentSettings(updates) { const persistedUpdates = buildPersistentSettingsPayload(updates); if (Object.keys(persistedUpdates).length > 0) { await chrome.storage.local.set(persistedUpdates); } } function buildSettingsExportFilename(date = new Date()) { const pad = (value) => String(value).padStart(2, '0'); return `${SETTINGS_EXPORT_FILENAME_PREFIX}-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}.json`; } async function exportSettingsBundle() { const settings = await getPersistedSettings(); const bundle = { schemaVersion: SETTINGS_EXPORT_SCHEMA_VERSION, exportedAt: new Date().toISOString(), extensionVersion: chrome.runtime.getManifest().version, settings, }; return { fileName: buildSettingsExportFilename(), fileContent: JSON.stringify(bundle, null, 2), }; } async function importSettingsBundle(configBundle) { const state = await ensureManualInteractionAllowed('\u5bfc\u5165\u914d\u7f6e'); if (Object.values(state.stepStatuses || {}).some((status) => status === 'running')) { throw new Error('\u5f53\u524d\u6709\u6b65\u9aa4\u6b63\u5728\u6267\u884c\uff0c\u65e0\u6cd5\u5bfc\u5165\u914d\u7f6e\u3002'); } if (!configBundle || typeof configBundle !== 'object' || Array.isArray(configBundle)) { throw new Error('\u914d\u7f6e\u6587\u4ef6\u5185\u5bb9\u65e0\u6548\u3002'); } const schemaVersion = Number(configBundle.schemaVersion); if (schemaVersion !== SETTINGS_EXPORT_SCHEMA_VERSION) { throw new Error(`\u4ec5\u652f\u6301\u5bfc\u5165 schemaVersion=${SETTINGS_EXPORT_SCHEMA_VERSION} \u7684\u914d\u7f6e\u6587\u4ef6\u3002`); } if (!configBundle.settings || typeof configBundle.settings !== 'object' || Array.isArray(configBundle.settings)) { throw new Error('\u914d\u7f6e\u6587\u4ef6\u7f3a\u5c11 settings \u914d\u7f6e\u6bb5\u3002'); } const importedSettings = buildPersistentSettingsPayload(configBundle.settings, { fillDefaults: true, requireKnownKeys: true, }); await setPersistentSettings(importedSettings); const sessionUpdates = { ...importedSettings, currentHotmailAccountId: null, email: null, }; await setState(sessionUpdates); broadcastDataUpdate({ ...importedSettings, currentHotmailAccountId: null, ...(sessionUpdates.email !== undefined ? { email: sessionUpdates.email } : {}), }); return getState(); } function broadcastDataUpdate(payload) { chrome.runtime.sendMessage({ type: 'DATA_UPDATED', payload, }).catch(() => { }); } function broadcastIcloudAliasesChanged(payload = {}) { chrome.runtime.sendMessage({ type: 'ICLOUD_ALIASES_CHANGED', payload, }).catch(() => { }); } async function setEmailStateSilently(email) { await setState({ email }); broadcastDataUpdate({ email }); } async function setEmailState(email) { await setEmailStateSilently(email); if (email) { await appendManualAccountRunRecordIfNeeded('step2_stopped', null, '步骤 2 已使用邮箱,流程尚未完成。'); await resumeAutoRunIfWaitingForEmail(); } } async function setPasswordState(password) { await setState({ password }); broadcastDataUpdate({ password }); } function buildContributionModeState(enabled, persistedSettings = {}, currentState = {}) { const currentContributionState = {}; for (const key of CONTRIBUTION_RUNTIME_KEYS) { currentContributionState[key] = currentState[key] !== undefined ? currentState[key] : CONTRIBUTION_RUNTIME_DEFAULTS[key]; } if (enabled) { const routing = resolveContributionModeRoutingState({ ...persistedSettings, ...currentState, ...currentContributionState, }); return { ...currentContributionState, contributionMode: true, contributionModeExpected: true, contributionSource: routing.source, contributionTargetGroupName: routing.targetGroupName, panelMode: routing.source, customPassword: '', accountRunHistoryTextEnabled: false, }; } return { ...CONTRIBUTION_RUNTIME_DEFAULTS, contributionMode: false, contributionModeExpected: false, panelMode: persistedSettings.panelMode || DEFAULT_STATE.panelMode, customPassword: persistedSettings.customPassword || '', accountRunHistoryTextEnabled: Boolean(persistedSettings.accountRunHistoryTextEnabled), }; } async function setContributionMode(enabled) { const normalizedEnabled = Boolean(enabled); const [persistedSettings, currentState] = await Promise.all([ getPersistedSettings(), getState(), ]); const updates = buildContributionModeState(normalizedEnabled, persistedSettings, currentState); await setState(updates); const nextState = await getState(); const contributionBroadcast = {}; for (const key of CONTRIBUTION_RUNTIME_KEYS) { contributionBroadcast[key] = nextState[key]; } broadcastDataUpdate({ ...contributionBroadcast, panelMode: nextState.panelMode, customPassword: nextState.customPassword, accountRunHistoryTextEnabled: nextState.accountRunHistoryTextEnabled, accountRunHistoryHelperBaseUrl: nextState.accountRunHistoryHelperBaseUrl, }); return nextState; } function getLuckmailUsedPurchases(state = {}) { return normalizeLuckmailUsedPurchases(state?.luckmailUsedPurchases); } function getLuckmailPreserveTagInfo(state = {}) { return { id: Number(state?.luckmailPreserveTagId) || 0, name: String(state?.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, }; } async function setLuckmailUsedPurchasesState(usedPurchases) { const normalizedUsedPurchases = normalizeLuckmailUsedPurchases(usedPurchases); await setPersistentSettings({ luckmailUsedPurchases: normalizedUsedPurchases }); await setState({ luckmailUsedPurchases: normalizedUsedPurchases }); broadcastDataUpdate({ luckmailUsedPurchases: normalizedUsedPurchases }); return normalizedUsedPurchases; } async function setLuckmailPurchaseUsedState(purchaseId, used) { const normalizedPurchaseId = normalizeLuckmailPurchaseId(purchaseId); if (!normalizedPurchaseId) { throw new Error('LuckMail 邮箱 ID 无效。'); } const state = await getState(); const usedPurchases = getLuckmailUsedPurchases(state); if (used) { usedPurchases[normalizedPurchaseId] = true; } else { delete usedPurchases[normalizedPurchaseId]; } await setLuckmailUsedPurchasesState(usedPurchases); return { purchaseId: Number(normalizedPurchaseId), used: Boolean(used), }; } async function setLuckmailPreserveTagInfo(tag) { const normalizedTags = normalizeLuckmailTags([tag]); const normalizedTag = normalizedTags[0] || { id: 0, name: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, }; const updates = { luckmailPreserveTagId: Number(normalizedTag.id) || 0, luckmailPreserveTagName: String(normalizedTag.name || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, }; await setPersistentSettings(updates); await setState(updates); broadcastDataUpdate(updates); return updates; } async function setLuckmailPurchaseState(purchase) { const normalizedPurchase = purchase ? normalizeLuckmailPurchase(purchase) : null; await setState({ currentLuckmailPurchase: normalizedPurchase }); broadcastDataUpdate({ currentLuckmailPurchase: normalizedPurchase }); return normalizedPurchase; } async function setLuckmailMailCursorState(cursor) { const normalizedCursor = cursor ? normalizeLuckmailMailCursor(cursor) : null; await setState({ currentLuckmailMailCursor: normalizedCursor }); return normalizedCursor; } async function clearLuckmailRuntimeState(options = {}) { const { clearEmail = false } = options; const updates = { currentLuckmailPurchase: null, currentLuckmailMailCursor: null, }; if (clearEmail) { updates.email = null; } await setState(updates); broadcastDataUpdate(updates); } function getManualAliasUsageMap(state) { return normalizeBooleanMap(state?.manualAliasUsage); } function getPreservedAliasMap(state) { return normalizeBooleanMap(state?.preservedAliases); } function isAliasPreserved(state, email) { const normalizedEmail = String(email || '').trim().toLowerCase(); if (!normalizedEmail) return false; return Boolean(getPreservedAliasMap(state)[normalizedEmail]); } function getEffectiveUsedEmails(state) { return toNormalizedEmailSet(getManualAliasUsageMap(state)); } function normalizeIcloudAliasCacheList(value = [], options = {}) { const aliases = Array.isArray(value) ? value : []; const usedEmails = toNormalizedEmailSet(options.usedEmails); const preservedEmails = toNormalizedEmailSet(options.preservedEmails); return aliases .map((alias) => normalizeIcloudAliasRecord(alias, { usedEmails, preservedEmails })) .filter(Boolean) .sort((left, right) => { if (left.active !== right.active) return left.active ? -1 : 1; if (left.used !== right.used) return left.used ? 1 : -1; return String(left.email).localeCompare(String(right.email)); }); } function getIcloudAliasCacheFromState(state, options = {}) { const maxAgeMs = Math.max(0, Number(options.maxAgeMs) || ICLOUD_ALIAS_CACHE_MAX_AGE_MS); const cachedAt = Number(state?.icloudAliasCacheAt || 0); if (!Array.isArray(state?.icloudAliasCache) || state.icloudAliasCache.length <= 0) { return []; } if (maxAgeMs > 0 && cachedAt > 0 && Date.now() - cachedAt > maxAgeMs) { return []; } return normalizeIcloudAliasCacheList(state.icloudAliasCache, { usedEmails: getEffectiveUsedEmails(state), preservedEmails: getPreservedAliasMap(state), }); } function isLikelyIcloudAliasEmail(value = '') { const email = String(value || '').trim().toLowerCase(); if (!email || !email.includes('@')) { return false; } return /@(icloud\.com|me\.com|mac\.com|privaterelay\.appleid\.com)$/.test(email); } function buildIcloudAliasFallbackFromLocalState(state = {}) { const manualAliasUsage = getManualAliasUsageMap(state); const preservedAliases = getPreservedAliasMap(state); const candidates = new Set(); for (const email of Object.keys(manualAliasUsage)) { if (isLikelyIcloudAliasEmail(email)) { candidates.add(String(email).trim().toLowerCase()); } } for (const email of Object.keys(preservedAliases)) { if (isLikelyIcloudAliasEmail(email)) { candidates.add(String(email).trim().toLowerCase()); } } const currentEmail = String(state?.email || '').trim().toLowerCase(); if (isLikelyIcloudAliasEmail(currentEmail)) { candidates.add(currentEmail); } if (!candidates.size) { return []; } const aliases = Array.from(candidates, (email) => ({ hme: email, email, state: 'active', active: true, })); return normalizeIcloudAliasCacheList(aliases, { usedEmails: getEffectiveUsedEmails(state), preservedEmails: preservedAliases, }); } async function setIcloudAliasUsedState(payload = {}, options = {}) { const email = String(payload.email || '').trim().toLowerCase(); if (!email) { throw new Error('未提供 iCloud 隐私邮箱地址。'); } const used = Boolean(payload.used); const state = await getState(); const manualAliasUsage = getManualAliasUsageMap(state); manualAliasUsage[email] = used; await setState({ manualAliasUsage }); if (!options.silentLog) { await addLog(`iCloud:已将 ${email} 标记为${used ? '已用' : '未用'}`, 'ok'); } broadcastIcloudAliasesChanged({ reason: 'used-updated', email, used }); return { email, used }; } async function setIcloudAliasPreservedState(payload = {}) { const email = String(payload.email || '').trim().toLowerCase(); if (!email) { throw new Error('未提供 iCloud 隐私邮箱地址。'); } const preserved = Boolean(payload.preserved); const state = await getState(); const preservedAliases = getPreservedAliasMap(state); preservedAliases[email] = preserved; await setState({ preservedAliases }); await addLog(`iCloud:已将 ${email} ${preserved ? '设为保留' : '取消保留'}`, 'ok'); broadcastIcloudAliasesChanged({ reason: 'preserved-updated', email, preserved }); return { email, preserved }; } async function resetState() { console.log(LOG_PREFIX, 'Resetting all state'); // Preserve settings and persistent data across resets const [prev, persistedSettings, persistedAliasState] = await Promise.all([ chrome.storage.session.get([ 'seenCodes', 'seenInbucketMailIds', 'accounts', 'tabRegistry', 'sourceLastUrls', 'reusablePhoneActivation', 'phoneReusableActivationPool', 'luckmailApiKey', 'luckmailBaseUrl', 'luckmailEmailType', 'luckmailDomain', 'luckmailUsedPurchases', 'luckmailPreserveTagId', 'luckmailPreserveTagName', 'preferredIcloudHost', ...CONTRIBUTION_RUNTIME_KEYS, ]), getPersistedSettings(), getPersistedAliasState(), ]); 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; const phoneReusableActivationPool = Array.isArray(prev.phoneReusableActivationPool) ? prev.phoneReusableActivationPool .map((entry) => normalizePhonePreferredActivation(entry)) .filter(Boolean) : []; await chrome.storage.session.clear(); await chrome.storage.session.set({ ...DEFAULT_STATE, ...persistedSettings, ...persistedAliasState, ...contributionModeState, seenCodes: prev.seenCodes || [], seenInbucketMailIds: prev.seenInbucketMailIds || [], accounts: prev.accounts || [], tabRegistry: prev.tabRegistry || {}, sourceLastUrls: prev.sourceLastUrls || {}, luckmailApiKey: String(prev.luckmailApiKey || ''), luckmailBaseUrl: normalizeLuckmailBaseUrl(prev.luckmailBaseUrl), luckmailEmailType: normalizeLuckmailEmailType(prev.luckmailEmailType), luckmailDomain: String(prev.luckmailDomain || '').trim(), luckmailUsedPurchases: normalizeLuckmailUsedPurchases(prev.luckmailUsedPurchases), luckmailPreserveTagId: Number(prev.luckmailPreserveTagId) || 0, luckmailPreserveTagName: String(prev.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, currentLuckmailPurchase: null, currentLuckmailMailCursor: null, // Keep reusable phone activation across round resets so the same number can be reactivated up to maxUses. reusablePhoneActivation, phoneReusableActivationPool, preferredIcloudHost: prev.preferredIcloudHost || '', }); } /** * Generate a random password: 14 chars, mix of uppercase, lowercase, digits, symbols. */ function generatePassword() { const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; const lower = 'abcdefghjkmnpqrstuvwxyz'; const digits = '23456789'; const symbols = '!@#$%&*?'; const all = upper + lower + digits + symbols; // Ensure at least one of each type let pw = ''; pw += upper[Math.floor(Math.random() * upper.length)]; pw += lower[Math.floor(Math.random() * lower.length)]; pw += digits[Math.floor(Math.random() * digits.length)]; pw += symbols[Math.floor(Math.random() * symbols.length)]; // Fill remaining 10 chars for (let i = 0; i < 10; i++) { pw += all[Math.floor(Math.random() * all.length)]; } // Shuffle return pw.split('').sort(() => Math.random() - 0.5).join(''); } function normalizePayPalAccount(account = {}) { if (self.PayPalUtils?.normalizePayPalAccount) { return self.PayPalUtils.normalizePayPalAccount(account); } return { id: String(account.id || crypto.randomUUID()), email: String(account.email || '').trim().toLowerCase(), password: String(account.password || ''), createdAt: Number.isFinite(Number(account.createdAt)) ? Number(account.createdAt) : Date.now(), updatedAt: Number.isFinite(Number(account.updatedAt)) ? Number(account.updatedAt) : Date.now(), lastUsedAt: Number.isFinite(Number(account.lastUsedAt)) ? Number(account.lastUsedAt) : 0, }; } function normalizePayPalAccounts(accounts) { if (self.PayPalUtils?.normalizePayPalAccounts) { return self.PayPalUtils.normalizePayPalAccounts(accounts); } return Array.isArray(accounts) ? accounts.map((account) => normalizePayPalAccount(account)) : []; } function findPayPalAccount(accounts, accountId) { if (self.PayPalUtils?.findPayPalAccount) { return self.PayPalUtils.findPayPalAccount(accounts, accountId); } const normalizedId = String(accountId || '').trim(); if (!normalizedId) return null; return normalizePayPalAccounts(accounts).find((account) => account.id === normalizedId) || null; } function upsertPayPalAccountInList(accounts, nextAccount) { if (self.PayPalUtils?.upsertPayPalAccountInList) { return self.PayPalUtils.upsertPayPalAccountInList(accounts, nextAccount); } const normalizedNext = normalizePayPalAccount(nextAccount); const list = normalizePayPalAccounts(accounts); const existingIndex = list.findIndex((account) => account.id === normalizedNext.id); if (existingIndex >= 0) { list[existingIndex] = normalizedNext; return list; } return [...list, normalizedNext]; } function normalizeHotmailAccount(account = {}) { const normalizedLastAuthAt = Number.isFinite(Number(account.lastAuthAt)) ? Number(account.lastAuthAt) : 0; const normalizedStatus = String( account.status || (normalizedLastAuthAt > 0 ? 'authorized' : 'pending') ); return { id: String(account.id || crypto.randomUUID()), email: String(account.email || '').trim(), password: String(account.password || ''), clientId: String(account.clientId || '').trim(), refreshToken: String(account.refreshToken || ''), status: normalizedStatus, enabled: account.enabled !== undefined ? Boolean(account.enabled) : true, used: Boolean(account.used), lastUsedAt: Number.isFinite(Number(account.lastUsedAt)) ? Number(account.lastUsedAt) : 0, lastAuthAt: normalizedLastAuthAt, lastError: String(account.lastError || ''), }; } function normalizeHotmailAccounts(accounts) { if (!Array.isArray(accounts)) return []; const deduped = new Map(); for (const account of accounts) { const normalized = normalizeHotmailAccount(account); if (!normalized.email && !normalized.id) continue; deduped.set(normalized.id, normalized); } return [...deduped.values()]; } function findHotmailAccount(accounts, accountId) { return normalizeHotmailAccounts(accounts).find((account) => account.id === accountId) || null; } function isHotmailProvider(stateOrProvider) { const provider = typeof stateOrProvider === 'string' ? stateOrProvider : stateOrProvider?.mailProvider; return provider === HOTMAIL_PROVIDER; } function isLuckmailProvider(stateOrProvider) { const provider = typeof stateOrProvider === 'string' ? stateOrProvider : stateOrProvider?.mailProvider; return provider === LUCKMAIL_PROVIDER; } function isCustomMailProvider(stateOrProvider) { const provider = typeof stateOrProvider === 'string' ? stateOrProvider : stateOrProvider?.mailProvider; return provider === 'custom'; } function getMail2925Mode(stateOrMode) { if (typeof stateOrMode === 'string') { return normalizeMail2925Mode(stateOrMode); } return normalizeMail2925Mode(stateOrMode?.mail2925Mode); } async function syncHotmailAccounts(accounts) { const normalized = normalizeHotmailAccounts(accounts); await setPersistentSettings({ hotmailAccounts: normalized }); await setState({ hotmailAccounts: normalized }); broadcastDataUpdate({ hotmailAccounts: normalized }); return normalized; } async function upsertHotmailAccount(input) { const state = await getState(); const accounts = normalizeHotmailAccounts(state.hotmailAccounts); const normalizedEmail = String(input?.email || '').trim().toLowerCase(); const existing = input?.id ? findHotmailAccount(accounts, input.id) : accounts.find((account) => account.email.toLowerCase() === normalizedEmail) || null; const credentialsChanged = !existing || (input?.clientId !== undefined && String(input.clientId).trim() !== existing.clientId) || (input?.refreshToken !== undefined && String(input.refreshToken).trim() !== existing.refreshToken) || (input?.email !== undefined && String(input.email).trim().toLowerCase() !== existing.email.toLowerCase()); const normalized = normalizeHotmailAccount({ ...(existing || {}), ...(credentialsChanged ? { status: 'pending', lastAuthAt: 0, lastError: '', } : {}), ...input, id: input?.id || existing?.id || crypto.randomUUID(), }); const nextAccounts = existing ? accounts.map((account) => (account.id === normalized.id ? normalized : account)) : [...accounts, normalized]; await syncHotmailAccounts(nextAccounts); return normalized; } async function deleteHotmailAccount(accountId) { const state = await getState(); const accounts = normalizeHotmailAccounts(state.hotmailAccounts); const nextAccounts = accounts.filter((account) => account.id !== accountId); await syncHotmailAccounts(nextAccounts); if (state.currentHotmailAccountId === accountId) { await setState({ currentHotmailAccountId: null }); if (isHotmailProvider(state)) { await setEmailState(null); } broadcastDataUpdate({ currentHotmailAccountId: null }); } } async function deleteHotmailAccounts(mode = 'all') { const state = await getState(); const accounts = normalizeHotmailAccounts(state.hotmailAccounts); const targets = filterHotmailAccountsByUsage(accounts, mode); const targetIds = new Set(targets.map((account) => account.id)); const nextAccounts = mode === 'used' ? accounts.filter((account) => !targetIds.has(account.id)) : []; await syncHotmailAccounts(nextAccounts); if (state.currentHotmailAccountId && targetIds.has(state.currentHotmailAccountId)) { await setState({ currentHotmailAccountId: null }); if (isHotmailProvider(state)) { await setEmailState(null); } broadcastDataUpdate({ currentHotmailAccountId: null }); } return { deletedCount: targets.length, remainingCount: nextAccounts.length, }; } async function patchHotmailAccount(accountId, updates = {}) { const state = await getState(); const accounts = normalizeHotmailAccounts(state.hotmailAccounts); const account = findHotmailAccount(accounts, accountId); if (!account) { throw new Error('未找到对应的 Hotmail 账号。'); } const nextAccount = normalizeHotmailAccount({ ...account, ...updates, id: account.id, }); await syncHotmailAccounts(accounts.map((item) => (item.id === account.id ? nextAccount : item))); if (state.currentHotmailAccountId === account.id && shouldClearHotmailCurrentSelection(nextAccount)) { await setState({ currentHotmailAccountId: null }); broadcastDataUpdate({ currentHotmailAccountId: null }); if (isHotmailProvider(state)) { await setEmailState(null); } } return nextAccount; } async function setCurrentHotmailAccount(accountId, options = {}) { const { markUsed = false, syncEmail = true } = options; const state = await getState(); const accounts = normalizeHotmailAccounts(state.hotmailAccounts); const account = findHotmailAccount(accounts, accountId); if (!account) { throw new Error('未找到对应的 Hotmail 账号。'); } if (markUsed) { account.lastUsedAt = Date.now(); await syncHotmailAccounts(accounts.map((item) => (item.id === account.id ? account : item))); } await setState({ currentHotmailAccountId: account.id }); broadcastDataUpdate({ currentHotmailAccountId: account.id }); if (syncEmail) { await setEmailState(account.email || null); } return account; } function isAuthorizedHotmailRunAccount(candidate) { return Boolean(candidate) && candidate.status === 'authorized' && !candidate.used && Boolean(candidate.refreshToken); } function isPendingHotmailVerificationCandidate(candidate) { return Boolean(candidate) && candidate.status === 'pending' && !candidate.used && Boolean(candidate.refreshToken); } function compareHotmailAccountAllocationPriority(left, right) { const leftUsedAt = Number(left?.lastUsedAt) || 0; const rightUsedAt = Number(right?.lastUsedAt) || 0; if (leftUsedAt !== rightUsedAt) { return leftUsedAt - rightUsedAt; } return String(left?.email || '').localeCompare(String(right?.email || '')); } function pickPendingHotmailAccountForVerification(accounts, options = {}) { const excludeIds = new Set((options.excludeIds || []).filter(Boolean)); const candidates = normalizeHotmailAccounts(accounts) .filter((candidate) => isPendingHotmailVerificationCandidate(candidate) && !excludeIds.has(candidate.id)); if (!candidates.length) { return null; } const preferredAccountId = String(options.preferredAccountId || '').trim(); if (preferredAccountId) { const preferredCandidate = candidates.find((candidate) => candidate.id === preferredAccountId); if (preferredCandidate) { return preferredCandidate; } } return candidates .slice() .sort(compareHotmailAccountAllocationPriority)[0] || null; } async function ensureHotmailAccountForFlow(options = {}) { const { allowAllocate = true, markUsed = false, preferredAccountId = null, excludeIds = [], } = options; const state = await getState(); const accounts = normalizeHotmailAccounts(state.hotmailAccounts); const excludedAccountIds = new Set((excludeIds || []).filter(Boolean)); const availableAccounts = accounts.filter((candidate) => isAuthorizedHotmailRunAccount(candidate) && !excludedAccountIds.has(candidate.id)); let account = null; if (preferredAccountId && !excludedAccountIds.has(preferredAccountId)) { account = findHotmailAccount(accounts, preferredAccountId); } if ((!account || !isAuthorizedHotmailRunAccount(account)) && state.currentHotmailAccountId && !excludedAccountIds.has(state.currentHotmailAccountId)) { account = findHotmailAccount(accounts, state.currentHotmailAccountId); } if ((!account || !isAuthorizedHotmailRunAccount(account)) && allowAllocate) { account = availableAccounts.length ? pickHotmailAccountForRun(availableAccounts, {}) : null; } if (!account) { throw new Error('没有可用的 Hotmail 账号。请先在侧边栏添加至少一个带刷新令牌(refresh token)的账号。'); } if (!isAuthorizedHotmailRunAccount(account)) { throw new Error(`Hotmail 账号 ${account.email || account.id} 尚未就绪,无法读取邮件。`); } return setCurrentHotmailAccount(account.id, { markUsed, syncEmail: true }); } function buildHotmailLocalEndpoint(baseUrl, path) { const normalizedBaseUrl = normalizeHotmailLocalBaseUrl(baseUrl); return new URL(path, `${normalizedBaseUrl}/`).toString(); } async function requestHotmailRemoteMailbox(account, mailbox = 'INBOX') { if (!account?.email) { throw new Error('Hotmail 账号缺少邮箱地址。'); } if (!account?.clientId) { throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少客户端 ID。`); } if (!account?.refreshToken) { throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少刷新令牌(refresh token)。`); } const { timeoutMs } = getHotmailMailApiRequestConfig(); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs); try { const result = await fetchMicrosoftMailboxMessages({ clientId: account.clientId, refreshToken: account.refreshToken, mailbox, top: 10, signal: controller.signal, }); return { mailbox, payload: { source: 'microsoft-api', transport: result.transport, tokenStrategy: result.tokenStrategy, }, messages: normalizeHotmailMailApiMessages(result.messages).map((message) => ({ ...message, mailbox: message?.mailbox || mailbox, })), nextRefreshToken: result.nextRefreshToken, }; } catch (err) { if (err?.name === 'AbortError') { throw new Error(`Hotmail API 对接请求超时(>${Math.round(timeoutMs / 1000)} 秒):${mailbox}`); } throw new Error(`Hotmail API 对接请求失败:${err.message}`); } finally { clearTimeout(timeoutId); } } function applyHotmailApiResultToAccount(account, apiResult) { const nextRefreshToken = String(apiResult?.nextRefreshToken || '').trim(); return { ...account, refreshToken: nextRefreshToken || account.refreshToken, status: 'authorized', lastAuthAt: Date.now(), lastError: '', }; } function buildHotmailMailApiFailureAccount(account, errorMessage) { return normalizeHotmailAccount({ ...account, status: 'error', lastError: String(errorMessage || ''), }); } async function fetchHotmailMailboxMessagesFromRemoteService(account, mailboxes = HOTMAIL_MAILBOXES) { let workingAccount = normalizeHotmailAccount(account); const mailboxResults = []; try { for (const mailbox of mailboxes) { const result = await requestHotmailRemoteMailbox(workingAccount, mailbox); workingAccount = applyHotmailApiResultToAccount(workingAccount, result); mailboxResults.push({ mailbox, count: result.messages.length, messages: result.messages.map((message) => ({ ...message, mailbox: message?.mailbox || mailbox, })), }); } } catch (err) { const failedAccount = buildHotmailMailApiFailureAccount(workingAccount, err.message); await upsertHotmailAccount(failedAccount); throw err; } const savedAccount = await upsertHotmailAccount(workingAccount); return { account: savedAccount, mailboxResults, messages: mailboxResults.flatMap((item) => item.messages), }; } async function requestHotmailLocalMessages(account, mailboxes = HOTMAIL_MAILBOXES) { if (!account?.email) { throw new Error('Hotmail 账号缺少邮箱地址。'); } if (!account?.clientId) { throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少客户端 ID。`); } if (!account?.refreshToken) { throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少刷新令牌(refresh token)。`); } const serviceSettings = getHotmailServiceSettings(await getState()); const { timeoutMs } = getHotmailMailApiRequestConfig(); const requestTimeoutMs = Math.max(timeoutMs, HOTMAIL_LOCAL_HELPER_TIMEOUT_MS); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), requestTimeoutMs); let response; try { response = await fetch(buildHotmailLocalEndpoint(serviceSettings.localBaseUrl, '/messages'), { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ email: account.email, clientId: account.clientId, refreshToken: account.refreshToken, mailboxes, top: 5, }), signal: controller.signal, }); } catch (err) { if (err?.name === 'AbortError') { throw new Error(`Hotmail 本地助手请求超时(>${Math.round(requestTimeoutMs / 1000)} 秒)`); } throw new Error(`Hotmail 本地助手请求失败:${err.message}`); } finally { clearTimeout(timeoutId); } const text = await response.text(); let payload = {}; try { payload = text ? JSON.parse(text) : {}; } catch { payload = { raw: text }; } if (!response.ok || payload?.ok === false) { const errorText = payload?.error || payload?.message || text || `HTTP ${response.status}`; throw new Error(`Hotmail 本地助手返回失败:${errorText}`); } const rawMessages = Array.isArray(payload?.messages) ? payload.messages : []; const normalizedMessages = normalizeHotmailMailApiMessages(rawMessages).map((message, index) => ({ ...message, mailbox: rawMessages[index]?.mailbox || 'INBOX', receivedTimestamp: Number(rawMessages[index]?.receivedTimestamp || 0) || 0, })); const mailboxResults = Array.isArray(payload?.mailboxResults) ? payload.mailboxResults.map((item) => ({ mailbox: String(item?.mailbox || 'INBOX'), count: Number(item?.count || 0), messages: normalizedMessages.filter((message) => String(message.mailbox || 'INBOX') === String(item?.mailbox || 'INBOX')), })) : mailboxes.map((mailbox) => ({ mailbox, count: normalizedMessages.filter((message) => String(message.mailbox || 'INBOX') === mailbox).length, messages: normalizedMessages.filter((message) => String(message.mailbox || 'INBOX') === mailbox), })); const nextAccount = applyHotmailApiResultToAccount(account, { nextRefreshToken: String(payload?.nextRefreshToken || '').trim(), }); const savedAccount = await upsertHotmailAccount(nextAccount); return { account: savedAccount, mailboxResults, messages: normalizedMessages, }; } async function requestHotmailLocalCode(account, pollPayload = {}) { if (!account?.email) { throw new Error('Hotmail 账号缺少邮箱地址。'); } if (!account?.clientId) { throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少客户端 ID。`); } if (!account?.refreshToken) { throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少刷新令牌(refresh token)。`); } const serviceSettings = getHotmailServiceSettings(await getState()); const { timeoutMs } = getHotmailMailApiRequestConfig(); const requestTimeoutMs = Math.max(timeoutMs, HOTMAIL_LOCAL_HELPER_TIMEOUT_MS); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), requestTimeoutMs); let response; try { response = await fetch(buildHotmailLocalEndpoint(serviceSettings.localBaseUrl, '/code'), { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ email: account.email, clientId: account.clientId, refreshToken: account.refreshToken, mailboxes: HOTMAIL_MAILBOXES, top: 5, senderFilters: pollPayload.senderFilters || [], subjectFilters: pollPayload.subjectFilters || [], excludeCodes: pollPayload.excludeCodes || [], filterAfterTimestamp: Number(pollPayload.filterAfterTimestamp || 0) || 0, }), signal: controller.signal, }); } catch (err) { if (err?.name === 'AbortError') { throw new Error(`Hotmail 本地助手请求超时(>${Math.round(requestTimeoutMs / 1000)} 秒)`); } throw new Error(`Hotmail 本地助手请求失败:${err.message}`); } finally { clearTimeout(timeoutId); } const text = await response.text(); let payload = {}; try { payload = text ? JSON.parse(text) : {}; } catch { payload = { raw: text }; } if (!response.ok || payload?.ok === false) { const errorText = payload?.error || payload?.message || text || `HTTP ${response.status}`; throw new Error(`Hotmail 本地助手返回失败:${errorText}`); } const normalizedMessage = payload?.message ? { ...normalizeHotmailMailApiMessages([payload.message])[0], mailbox: payload?.message?.mailbox || 'INBOX', receivedTimestamp: Number(payload?.message?.receivedTimestamp || 0) || 0, } : null; const nextAccount = applyHotmailApiResultToAccount(account, { nextRefreshToken: String(payload?.nextRefreshToken || '').trim(), }); const savedAccount = await upsertHotmailAccount(nextAccount); return { account: savedAccount, code: String(payload?.code || ''), message: normalizedMessage, usedTimeFallback: Boolean(payload?.usedTimeFallback), selectionSource: String(payload?.selectionSource || ''), }; } async function pollHotmailVerificationCodeViaLocalHelper(step, account, pollPayload = {}) { const maxAttempts = Number(pollPayload.maxAttempts) || 5; const intervalMs = Number(pollPayload.intervalMs) || 3000; let workingAccount = account; let lastError = null; for (let attempt = 1; attempt <= maxAttempts; attempt++) { throwIfStopped(); try { await addLog(`步骤 ${step}:正在通过本地助手轮询 Hotmail 验证码(${attempt}/${maxAttempts})...`, 'info'); const fetchResult = await requestHotmailLocalCode(workingAccount, pollPayload); workingAccount = fetchResult.account; if (fetchResult.code) { const mailboxLabel = fetchResult.message?.mailbox || 'INBOX'; if (fetchResult.usedTimeFallback) { await addLog(`步骤 ${step}:本地助手使用时间回退后命中 Hotmail ${mailboxLabel} 验证码。`, 'warn'); } await addLog(`步骤 ${step}:已通过本地助手在 Hotmail ${mailboxLabel} 中找到验证码:${fetchResult.code}`, 'ok'); return { ok: true, code: fetchResult.code, emailTimestamp: fetchResult.message?.receivedTimestamp || Date.now(), mailId: fetchResult.message?.id || '', }; } lastError = new Error(`步骤 ${step}:本地助手暂未返回匹配验证码(${attempt}/${maxAttempts})。`); await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info'); } catch (err) { lastError = err; await addLog(`步骤 ${step}:本地助手轮询 Hotmail 失败:${err.message}`, 'warn'); } if (attempt < maxAttempts) { await sleepWithStop(intervalMs); } } throw lastError || new Error(`步骤 ${step}:本地助手未返回新的匹配验证码。`); } async function fetchHotmailMailboxMessages(account, mailboxes = HOTMAIL_MAILBOXES) { const serviceSettings = getHotmailServiceSettings(await getState()); if (serviceSettings.mode === HOTMAIL_SERVICE_MODE_LOCAL) { return requestHotmailLocalMessages(account, mailboxes); } return fetchHotmailMailboxMessagesFromRemoteService(account, mailboxes); } async function verifyHotmailAccount(accountId) { const state = await getState(); const account = findHotmailAccount(state.hotmailAccounts, accountId); if (!account) { throw new Error('未找到需要校验的 Hotmail 账号。'); } const result = await fetchHotmailMailboxMessages(account, ['INBOX']); return { account: result.account, messageCount: result.mailboxResults[0]?.count || 0, }; } async function ensureHotmailMailboxReadyForAutoRunRound(options = {}) { const { targetRun = 0, totalRuns = 0, attemptRun = 1, } = options; const state = await getState(); if (!isHotmailProvider(state)) { return null; } const buildRoundLabel = () => { if (targetRun > 0 && totalRuns > 0) { return `第 ${targetRun}/${totalRuns} 轮`; } return '当前轮'; }; const exhaustedAccountIds = new Set(); let preferredAccountId = state.currentHotmailAccountId || null; let lastError = null; while (true) { throwIfStopped(); const latestState = await getState(); const latestAccounts = normalizeHotmailAccounts(latestState.hotmailAccounts); const remainingAuthorizedAccounts = latestAccounts .filter((candidate) => isAuthorizedHotmailRunAccount(candidate) && !exhaustedAccountIds.has(candidate.id)); const remainingPendingAccounts = latestAccounts .filter((candidate) => isPendingHotmailVerificationCandidate(candidate) && !exhaustedAccountIds.has(candidate.id)); if (!remainingAuthorizedAccounts.length && !remainingPendingAccounts.length) { if (lastError) { throw new Error(`自动运行${buildRoundLabel()}开始前未找到可通过校验的 Hotmail 账号:${lastError.message}`); } throw new Error('没有可用的 Hotmail 账号。请先在侧边栏添加至少一个带刷新令牌(refresh token)的账号。'); } let account = null; if (remainingAuthorizedAccounts.length) { account = await ensureHotmailAccountForFlow({ allowAllocate: true, markUsed: false, preferredAccountId, excludeIds: [...exhaustedAccountIds], }); } else { const pendingAccount = pickPendingHotmailAccountForVerification(latestAccounts, { preferredAccountId, excludeIds: [...exhaustedAccountIds], }); if (!pendingAccount) { throw new Error('没有可用的 Hotmail 账号。请先在侧边栏添加至少一个带刷新令牌(refresh token)的账号。'); } account = await setCurrentHotmailAccount(pendingAccount.id, { markUsed: false, syncEmail: true, }); await addLog( `自动运行${buildRoundLabel()}开始前未找到已校验 Hotmail 账号,正在尝试校验待校验账号 ${account.email}。`, 'warn' ); } try { await addLog( `自动运行${buildRoundLabel()}第 ${attemptRun} 次尝试开始前,正在校验 Hotmail 账号 ${account.email} 的邮箱可用性。`, 'info' ); const result = await verifyHotmailAccount(account.id); await addLog( `自动运行${buildRoundLabel()}开始前已校验 Hotmail 账号 ${result.account?.email || account.email},INBOX 当前 ${result.messageCount} 封邮件。`, 'ok' ); return result.account; } catch (error) { lastError = error; exhaustedAccountIds.add(account.id); preferredAccountId = null; const latestErrorMessage = error?.message || '未知错误'; await addLog( `自动运行${buildRoundLabel()}开始前校验 Hotmail 账号 ${account.email} 失败:${latestErrorMessage}`, 'warn' ); const nextState = await getState(); const hasRemainingAccounts = normalizeHotmailAccounts(nextState.hotmailAccounts) .some((candidate) => ( isAuthorizedHotmailRunAccount(candidate) || isPendingHotmailVerificationCandidate(candidate) ) && !exhaustedAccountIds.has(candidate.id)); if (hasRemainingAccounts) { await addLog(`自动运行${buildRoundLabel()}开始前将切换下一个 Hotmail 账号并重试。`, 'warn'); } } } } async function testHotmailAccountMailAccess(accountId) { const state = await getState(); const account = findHotmailAccount(state.hotmailAccounts, accountId); if (!account) { throw new Error('未找到需要测试的 Hotmail 账号。'); } const result = await fetchHotmailMailboxMessages(account, HOTMAIL_MAILBOXES); const latestMessage = getLatestHotmailMessage(result.messages); const latestCode = latestMessage ? extractVerificationCodeFromMessage(latestMessage) : null; return { account: result.account, accountId: result.account.id, email: result.account.email, messageCount: result.messages.length, latestSubject: latestMessage?.subject || '', latestMailbox: latestMessage?.mailbox || '', latestCode: latestCode || '', inboxCount: result.mailboxResults.find((item) => item.mailbox === 'INBOX')?.count || 0, junkCount: result.mailboxResults.find((item) => item.mailbox === 'Junk')?.count || 0, }; } async function pollHotmailVerificationCode(step, state, pollPayload = {}) { await addLog(`步骤 ${step}:正在确定 Hotmail 收信账号...`, 'info'); let account = await ensureHotmailAccountForFlow({ allowAllocate: true, markUsed: false, preferredAccountId: state.currentHotmailAccountId || null, }); await addLog(`步骤 ${step}:当前使用 Hotmail 账号 ${account.email} 轮询收件箱。`, 'info'); const serviceSettings = getHotmailServiceSettings(state); if (serviceSettings.mode === HOTMAIL_SERVICE_MODE_LOCAL) { return pollHotmailVerificationCodeViaLocalHelper(step, account, pollPayload); } const maxAttempts = Number(pollPayload.maxAttempts) || 5; const intervalMs = Number(pollPayload.intervalMs) || 3000; let lastError = null; function summarizeMessagesForLog(messages) { return (messages || []) .slice() .sort((left, right) => { const leftTime = Date.parse(left.receivedDateTime || '') || 0; const rightTime = Date.parse(right.receivedDateTime || '') || 0; return rightTime - leftTime; }) .slice(0, 3) .map((message) => { const receivedAt = message?.receivedDateTime || '未知时间'; const sender = message?.from?.emailAddress?.address || '未知发件人'; const subject = message?.subject || '(无主题)'; const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80); return `[${message.mailbox || 'INBOX'}] ${receivedAt} | ${sender} | ${subject} | ${preview}`; }) .join(' || '); } for (let attempt = 1; attempt <= maxAttempts; attempt++) { throwIfStopped(); try { await addLog(`步骤 ${step}:正在通过 API对接 轮询 Hotmail 邮件(${attempt}/${maxAttempts})...`, 'info'); const fetchResult = await fetchHotmailMailboxMessages(account, HOTMAIL_MAILBOXES); account = fetchResult.account; const matchResult = pickVerificationMessageWithTimeFallback(fetchResult.messages, { afterTimestamp: pollPayload.filterAfterTimestamp || 0, senderFilters: pollPayload.senderFilters || [], subjectFilters: pollPayload.subjectFilters || [], excludeCodes: pollPayload.excludeCodes || [], }); const match = matchResult.match; if (match?.code) { const mailboxLabel = match.message?.mailbox || 'INBOX'; if (matchResult.usedRelaxedFilters) { const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配'; await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Hotmail ${mailboxLabel} 验证码。`, 'warn'); } await addLog(`步骤 ${step}:已通过 API对接 在 Hotmail ${mailboxLabel} 中找到验证码:${match.code}`, 'ok'); return { ok: true, code: match.code, emailTimestamp: match.receivedAt || Date.now(), mailId: match.message?.id || '', }; } lastError = new Error(`步骤 ${step}:暂未在 Hotmail 收件箱中找到匹配验证码(${attempt}/${maxAttempts})。`); await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info'); const mailSummary = summarizeMessagesForLog(fetchResult.messages); if (mailSummary) { await addLog(`步骤 ${step}:最近邮件样本:${mailSummary}`, 'info'); } } catch (err) { lastError = err; await addLog(`步骤 ${step}:Hotmail API 对接轮询失败:${err.message}`, 'warn'); } if (attempt < maxAttempts) { await sleepWithStop(intervalMs); } } throw lastError || new Error(`步骤 ${step}:未在 Hotmail 收件箱中找到新的匹配验证码。`); } function generateRandomSuffix(length = 6) { const chars = 'abcdefghjkmnpqrstuvwxyz23456789'; let suffix = ''; for (let i = 0; i < length; i++) { suffix += chars[Math.floor(Math.random() * chars.length)]; } return suffix; } const GMAIL_ALIAS_WORDS = [ 'amber', 'apple', 'ash', 'berry', 'birch', 'blue', 'brook', 'cedar', 'cloud', 'clover', 'coast', 'cocoa', 'coral', 'dawn', 'delta', 'echo', 'ember', 'field', 'flint', 'flora', 'forest', 'frost', 'glade', 'harbor', 'hazel', 'honey', 'ivory', 'jade', 'lake', 'leaf', 'light', 'lilac', 'lotus', 'lunar', 'maple', 'meadow', 'mist', 'moon', 'nova', 'oasis', 'olive', 'opal', 'pearl', 'pine', 'pixel', 'plum', 'quartz', 'rain', 'raven', 'river', 'rose', 'sage', 'shore', 'sky', 'solar', 'spark', 'stone', 'storm', 'sun', 'terra', 'vale', 'wave', 'willow', 'zephyr', ]; function generateRandomWordAliasTag(parts = 3) { const selected = []; for (let i = 0; i < parts; i++) { selected.push(GMAIL_ALIAS_WORDS[Math.floor(Math.random() * GMAIL_ALIAS_WORDS.length)]); } return selected.join(''); } function parseGmailBaseEmail(rawValue) { const value = String(rawValue || '').trim().toLowerCase(); const match = value.match(/^([^@\s+]+)@((?:gmail|googlemail)\.com)$/i); if (!match) return null; return { localPart: match[1], domain: match[2].toLowerCase(), }; } function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) { if ( stateOrProvider && typeof stateOrProvider === 'object' && !Array.isArray(stateOrProvider) && normalizeEmailGenerator(stateOrProvider.emailGenerator) === ( typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string' ? CUSTOM_EMAIL_POOL_GENERATOR : 'custom-pool' ) ) { return false; } const provider = typeof stateOrProvider === 'string' ? stateOrProvider : stateOrProvider?.mailProvider; const resolvedMail2925Mode = mail2925Mode !== undefined ? normalizeMail2925Mode(mail2925Mode) : getMail2925Mode(stateOrProvider); const utils = (typeof self !== 'undefined' ? self : globalThis).MultiPageManagedAliasUtils || null; if (utils?.usesManagedAliasGeneration) { return utils.usesManagedAliasGeneration(provider, { mail2925Mode: resolvedMail2925Mode }); } if (utils?.isManagedAliasProvider) { if (String(provider || '').trim().toLowerCase() === '2925') { return utils.isManagedAliasProvider(provider) && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE; } return utils.isManagedAliasProvider(provider); } return provider === GMAIL_PROVIDER || (provider === '2925' && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE); } function shouldUseCustomRegistrationEmail(state = {}) { return isCustomMailProvider(state) || (!isHotmailProvider(state) && !isGeneratedAliasProvider(state) && normalizeEmailGenerator(state.emailGenerator) === 'custom'); } function buildGeneratedAliasEmail(state) { const provider = state.mailProvider || '163'; const emailPrefix = (state.emailPrefix || '').trim(); if (provider === GMAIL_PROVIDER) { if (!emailPrefix) { throw new Error('Gmail 原邮箱未设置,请先在侧边栏填写。'); } const parsed = parseGmailBaseEmail(emailPrefix); if (!parsed) { throw new Error('Gmail 原邮箱格式不正确,请填写类似 name@gmail.com 的地址。'); } return `${parsed.localPart}+${generateRandomWordAliasTag()}@${parsed.domain}`; } if (!emailPrefix) { throw new Error('2925 邮箱前缀未设置,请先在侧边栏填写。'); } if (provider === '2925' && isGeneratedAliasProvider(state)) { return `${emailPrefix}${generateRandomSuffix(6)}@2925.com`; } throw new Error(`未支持的别名邮箱类型:${provider}`); } function getManagedAliasUtils() { return (typeof self !== 'undefined' ? self : globalThis).MultiPageManagedAliasUtils || null; } function parseGmailBaseEmail(rawValue) { const utils = getManagedAliasUtils(); if (utils?.parseManagedAliasBaseEmail) { return utils.parseManagedAliasBaseEmail(rawValue, GMAIL_PROVIDER); } const value = String(rawValue || '').trim().toLowerCase(); const match = value.match(/^([^@\s+]+)@((?:gmail|googlemail)\.com)$/i); if (!match) return null; return { localPart: match[1], domain: match[2].toLowerCase(), }; } function parseManagedAliasBaseEmail(rawValue, provider) { const utils = getManagedAliasUtils(); if (utils?.parseManagedAliasBaseEmail) { return utils.parseManagedAliasBaseEmail(rawValue, provider); } if (provider === GMAIL_PROVIDER) { return parseGmailBaseEmail(rawValue); } const value = String(rawValue || '').trim().toLowerCase(); const match = value.match(/^([^@\s+]+)@(2925\.com)$/i); if (!match) return null; return { localPart: match[1], domain: match[2].toLowerCase(), }; } function isManagedAliasEmail(value, provider, baseEmail = '') { const utils = getManagedAliasUtils(); if (utils?.isManagedAliasEmail) { return utils.isManagedAliasEmail(value, provider, baseEmail); } const normalizedValue = String(value || '').trim().toLowerCase(); if (!normalizedValue) return false; const parsedEmail = normalizedValue.match(/^([^@\s]+)@([^@\s]+\.[^@\s]+)$/); if (!parsedEmail) return false; const candidateLocalPart = parsedEmail[1]; const candidateDomain = parsedEmail[2]; if (provider === GMAIL_PROVIDER) { if (!/^(?:gmail|googlemail)\.com$/i.test(candidateDomain)) { return false; } const parsedBaseEmail = parseManagedAliasBaseEmail(baseEmail, provider); if (!parsedBaseEmail) { return true; } return candidateDomain === parsedBaseEmail.domain && candidateLocalPart.split('+')[0] === parsedBaseEmail.localPart; } if (provider !== '2925' || candidateDomain !== '2925.com') { return false; } const parsedBaseEmail = parseManagedAliasBaseEmail(baseEmail, provider); if (!parsedBaseEmail) { return true; } return candidateLocalPart === parsedBaseEmail.localPart || candidateLocalPart.startsWith(parsedBaseEmail.localPart); } function getManagedAliasBaseEmail(state = {}, provider = state?.mailProvider) { const normalizedProvider = String(provider || '').trim().toLowerCase(); const legacyEmailPrefix = String(state?.emailPrefix || '').trim(); if (normalizedProvider === GMAIL_PROVIDER) { const gmailBaseEmail = String(state?.gmailBaseEmail || '').trim(); if (gmailBaseEmail) { return gmailBaseEmail; } return parseManagedAliasBaseEmail(legacyEmailPrefix, normalizedProvider) ? legacyEmailPrefix : ''; } if (normalizedProvider === '2925') { const currentAccount = Boolean(state?.mail2925UseAccountPool) ? getCurrentMail2925Account(state) : null; if (currentAccount?.email) { return currentAccount.email; } const mail2925BaseEmail = String(state?.mail2925BaseEmail || '').trim(); if (mail2925BaseEmail) { return mail2925BaseEmail; } return parseManagedAliasBaseEmail(legacyEmailPrefix, normalizedProvider) ? legacyEmailPrefix : ''; } return ''; } function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) { if ( stateOrProvider && typeof stateOrProvider === 'object' && !Array.isArray(stateOrProvider) && normalizeEmailGenerator(stateOrProvider.emailGenerator) === ( typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string' ? CUSTOM_EMAIL_POOL_GENERATOR : 'custom-pool' ) ) { return false; } const provider = typeof stateOrProvider === 'string' ? stateOrProvider : stateOrProvider?.mailProvider; const resolvedMail2925Mode = mail2925Mode !== undefined ? normalizeMail2925Mode(mail2925Mode) : getMail2925Mode(stateOrProvider); const utils = getManagedAliasUtils(); if (utils?.usesManagedAliasGeneration) { return utils.usesManagedAliasGeneration(provider, { mail2925Mode: resolvedMail2925Mode }); } if (utils?.isManagedAliasProvider) { if (String(provider || '').trim().toLowerCase() === '2925') { return utils.isManagedAliasProvider(provider) && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE; } return utils.isManagedAliasProvider(provider); } return provider === GMAIL_PROVIDER || (provider === '2925' && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE); } function shouldUseCustomRegistrationEmail(state = {}) { return isCustomMailProvider(state) || (!isHotmailProvider(state) && !isGeneratedAliasProvider(state) && normalizeEmailGenerator(state.emailGenerator) === 'custom'); } function isReusableGeneratedAliasEmail(state = {}, email = state?.email) { if (!isGeneratedAliasProvider(state)) { return false; } return isManagedAliasEmail(email, state?.mailProvider, getManagedAliasBaseEmail(state)); } function buildGeneratedAliasEmail(state) { const provider = state.mailProvider || '163'; const baseEmail = getManagedAliasBaseEmail(state, provider); const baseLabel = provider === GMAIL_PROVIDER ? 'Gmail 原邮箱' : '2925 基邮箱'; const exampleEmail = provider === GMAIL_PROVIDER ? 'name@gmail.com' : 'name@2925.com'; if (!baseEmail) { throw new Error(`${baseLabel}未设置,请先在侧边栏填写,或直接在“注册邮箱”中手动填写完整邮箱。`); } if (!parseManagedAliasBaseEmail(baseEmail, provider)) { throw new Error(`${baseLabel}格式不正确,请填写类似 ${exampleEmail} 的地址。`); } const utils = getManagedAliasUtils(); if (utils?.buildManagedAliasEmail) { return utils.buildManagedAliasEmail( provider, baseEmail, provider === GMAIL_PROVIDER ? generateRandomWordAliasTag() : generateRandomSuffix(6) ); } const parsedBaseEmail = parseManagedAliasBaseEmail(baseEmail, provider); if (provider === GMAIL_PROVIDER) { return `${parsedBaseEmail.localPart}+${generateRandomWordAliasTag()}@${parsedBaseEmail.domain}`; } if (provider === '2925') { return `${parsedBaseEmail.localPart}${generateRandomSuffix(6)}@${parsedBaseEmail.domain}`; } throw new Error(`未支持的别名邮箱类型:${provider}`); } function getLuckmailSessionConfig(state = {}) { return { apiKey: String(state.luckmailApiKey || ''), baseUrl: normalizeLuckmailBaseUrl(state.luckmailBaseUrl), emailType: normalizeLuckmailEmailType(state.luckmailEmailType), domain: String(state.luckmailDomain || '').trim(), }; } function ensureLuckmailApiKey(state = {}) { const apiKey = String(state.luckmailApiKey || '').trim(); if (!apiKey) { throw new Error('LuckMail API Key 为空,请先在侧边栏填写。'); } return apiKey; } async function requestLuckmail(method, path, { baseUrl, apiKey, params, jsonData, timeout = 30000 } = {}) { const requestUrl = new URL(`${normalizeLuckmailBaseUrl(baseUrl)}${path}`); if (params && typeof params === 'object') { for (const [key, value] of Object.entries(params)) { if (value === undefined || value === null || value === '') continue; requestUrl.searchParams.set(key, String(value)); } } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); const headers = { Accept: 'application/json', }; if (apiKey) { headers['X-API-Key'] = apiKey; } const upperMethod = String(method || 'GET').toUpperCase(); const fetchOptions = { method: upperMethod, headers, signal: controller.signal, }; if (jsonData !== undefined) { headers['Content-Type'] = 'application/json'; fetchOptions.body = JSON.stringify(jsonData || {}); } let response = null; try { response = await fetch(requestUrl.toString(), fetchOptions); } catch (err) { if (err?.name === 'AbortError') { throw new Error(`LuckMail 请求超时:${path}`); } throw new Error(`LuckMail 请求失败:${err.message}`); } finally { clearTimeout(timeoutId); } let payload = null; try { payload = await response.json(); } catch { throw new Error(`LuckMail 返回了无法解析的响应:${path}`); } if (!response.ok) { const errorText = String(payload?.message || response.statusText || 'HTTP error'); throw new Error(`LuckMail 请求失败:${errorText}`); } if (!payload || typeof payload !== 'object') { throw new Error(`LuckMail 返回数据无效:${path}`); } if (payload.code !== 0) { const errorText = String(payload.message || 'Unknown error'); throw new Error(`LuckMail 接口返回失败:${errorText}`); } return payload.data; } function createLuckmailClient(state = {}) { const config = getLuckmailSessionConfig(state); const apiKey = ensureLuckmailApiKey(state); const request = (method, path, options = {}) => requestLuckmail(method, path, { baseUrl: config.baseUrl, apiKey, ...options, }); return { user: { async purchaseEmails(projectCode, quantity, { emailType, domain } = {}) { const body = { project_code: projectCode, quantity, email_type: normalizeLuckmailEmailType(emailType), }; if (domain) { body.domain = String(domain).trim(); } return request('POST', '/api/v1/openapi/email/purchase', { jsonData: body, }); }, async getPurchases({ page = 1, pageSize = 100, projectId, tagId, keyword, userDisabled } = {}) { return normalizeLuckmailPurchaseListPage(await request('GET', '/api/v1/openapi/email/purchases', { params: { page, page_size: pageSize, project_id: projectId, tag_id: tagId, keyword, user_disabled: userDisabled, }, })); }, async getTokenCode(token) { return normalizeLuckmailTokenCode(await request( 'GET', `/api/v1/openapi/email/token/${encodeURIComponent(token)}/code` )); }, async checkTokenAlive(token) { const data = await request( 'GET', `/api/v1/openapi/email/token/${encodeURIComponent(token)}/alive` ); return { email_address: String(data?.email_address || ''), project: String(data?.project || ''), alive: Boolean(data?.alive), status: String(data?.status || ''), message: String(data?.message || ''), mail_count: Number(data?.mail_count) || 0, }; }, async getTokenMails(token) { const data = await request('GET', `/api/v1/openapi/email/token/${encodeURIComponent(token)}/mails`); return { email_address: String(data?.email_address || ''), project: String(data?.project || ''), warranty_until: String(data?.warranty_until || ''), mails: normalizeLuckmailTokenMails(data?.mails || []), }; }, async getTokenMailDetail(token, messageId) { return normalizeLuckmailTokenMail(await request( 'GET', `/api/v1/openapi/email/token/${encodeURIComponent(token)}/mails/${encodeURIComponent(messageId)}` )); }, async setPurchaseDisabled(purchaseId, disabled) { await request('PUT', `/api/v1/openapi/email/purchases/${encodeURIComponent(purchaseId)}/disabled`, { jsonData: { disabled: disabled ? 1 : 0, }, }); }, async batchSetPurchaseDisabled(ids, disabled) { await request('POST', '/api/v1/openapi/email/purchases/batch-disabled', { jsonData: { ids: (Array.isArray(ids) ? ids : []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0), disabled: disabled ? 1 : 0, }, }); }, async setPurchaseTag(purchaseId, { tagId, tagName } = {}) { const body = {}; if (tagId !== undefined) { body.tag_id = Number(tagId) || 0; } if (tagName !== undefined) { body.tag_name = String(tagName || '').trim(); } await request('PUT', `/api/v1/openapi/email/purchases/${encodeURIComponent(purchaseId)}/tag`, { jsonData: body, }); }, async batchSetPurchaseTag(ids, { tagId, tagName } = {}) { const body = { ids: (Array.isArray(ids) ? ids : []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0), }; if (tagId !== undefined) { body.tag_id = Number(tagId) || 0; } if (tagName !== undefined) { body.tag_name = String(tagName || '').trim(); } await request('POST', '/api/v1/openapi/email/purchases/batch-tag', { jsonData: body, }); }, async getTags() { return normalizeLuckmailTags(await request('GET', '/api/v1/openapi/email/tags')); }, async createTag(name, limitType, remark) { const body = { name: String(name || '').trim(), limit_type: Number(limitType) || 0, }; if (remark !== undefined) { body.remark = String(remark || '').trim(); } return normalizeLuckmailTags([await request('POST', '/api/v1/openapi/email/tags', { jsonData: body, })])[0] || null; }, }, }; } function getCurrentLuckmailPurchase(state = {}) { return state.currentLuckmailPurchase ? normalizeLuckmailPurchase(state.currentLuckmailPurchase) : null; } function buildLuckmailPurchaseView(purchase, state = {}) { const normalizedPurchase = normalizeLuckmailPurchase(purchase); const usedPurchases = getLuckmailUsedPurchases(state); const preserveTagInfo = getLuckmailPreserveTagInfo(state); return { id: normalizedPurchase.id, email_address: normalizedPurchase.email_address, project_name: normalizeLuckmailProjectName(normalizedPurchase.project_name) || DEFAULT_LUCKMAIL_PROJECT_CODE, price: normalizedPurchase.price, status: normalizedPurchase.status, tag_id: normalizedPurchase.tag_id, tag_name: normalizedPurchase.tag_name, user_disabled: normalizedPurchase.user_disabled, warranty_hours: normalizedPurchase.warranty_hours, warranty_until: normalizedPurchase.warranty_until, created_at: normalizedPurchase.created_at, used: Boolean(usedPurchases[normalizeLuckmailPurchaseId(normalizedPurchase.id)]), preserved: isLuckmailPurchasePreserved(normalizedPurchase, { preserveTagId: preserveTagInfo.id, preserveTagName: preserveTagInfo.name, }), disabled: normalizedPurchase.user_disabled === 1, current: Number(getCurrentLuckmailPurchase(state)?.id) === normalizedPurchase.id, reusable: isLuckmailPurchaseReusable(normalizedPurchase, { projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, usedPurchases, preserveTagId: preserveTagInfo.id, preserveTagName: preserveTagInfo.name, now: Date.now(), }), }; } async function getAllLuckmailPurchases(state, options = {}) { const client = options.client || createLuckmailClient(state); const pageSize = Math.max(1, Math.min(100, Number(options.pageSize) || 100)); const maxPages = Math.max(1, Number(options.maxPages) || 50); const purchases = []; for (let page = 1; page <= maxPages; page += 1) { const pageResult = await client.user.getPurchases({ page, pageSize, keyword: options.keyword, projectId: options.projectId, tagId: options.tagId, userDisabled: options.userDisabled, }); const normalizedPage = normalizeLuckmailPurchaseListPage(pageResult); purchases.push(...normalizedPage.list); if (normalizedPage.list.length === 0) { break; } if (normalizedPage.total > 0 && purchases.length >= normalizedPage.total) { break; } if (normalizedPage.list.length < normalizedPage.page_size) { break; } } return purchases; } async function listLuckmailPurchasesByProject(state, options = {}) { const projectCode = normalizeLuckmailProjectName(options.projectCode || DEFAULT_LUCKMAIL_PROJECT_CODE) || DEFAULT_LUCKMAIL_PROJECT_CODE; const purchases = await getAllLuckmailPurchases(state, options); return purchases.filter((purchase) => isLuckmailPurchaseForProject(purchase, projectCode)); } async function getLuckmailPurchaseById(state, purchaseId, options = {}) { const normalizedPurchaseId = Number(normalizeLuckmailPurchaseId(purchaseId)) || 0; if (!normalizedPurchaseId) { throw new Error('LuckMail 邮箱 ID 无效。'); } const purchases = await listLuckmailPurchasesByProject(state, options); const purchase = purchases.find((item) => item.id === normalizedPurchaseId) || null; if (!purchase) { throw new Error(`未找到 ID=${normalizedPurchaseId} 的 openai LuckMail 邮箱。`); } return purchase; } async function listLuckmailPurchasesForManagement() { const state = await getState(); const purchases = await listLuckmailPurchasesByProject(state, { projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, }); return purchases.map((purchase) => buildLuckmailPurchaseView(purchase, state)); } async function ensureLuckmailPreserveTag(client, state = null) { const resolvedState = state || await getState(); const preserveTagInfo = getLuckmailPreserveTagInfo(resolvedState); if (preserveTagInfo.id > 0) { return preserveTagInfo; } const tags = normalizeLuckmailTags(await client.user.getTags()); let preserveTag = tags.find( (tag) => normalizeLuckmailProjectName(tag.name) === normalizeLuckmailProjectName(preserveTagInfo.name) ) || null; if (!preserveTag) { preserveTag = await client.user.createTag( DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, 0, '保留邮箱(不参与自动复用)' ); } await setLuckmailPreserveTagInfo(preserveTag); return { id: Number(preserveTag?.id) || 0, name: String(preserveTag?.name || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, }; } async function activateLuckmailPurchaseForFlow(state, client, purchase, options = {}) { const normalizedPurchase = normalizeLuckmailPurchase(purchase); if (!normalizedPurchase?.email_address || !normalizedPurchase?.token) { throw new Error('LuckMail 邮箱缺少 email/token,无法用于当前流程。'); } let baselineCursor = null; if (options.initializeCursor !== false) { const mailList = await client.user.getTokenMails(normalizedPurchase.token); baselineCursor = buildLuckmailBaselineCursor(mailList?.mails || []); } await setLuckmailPurchaseState(normalizedPurchase); await setLuckmailMailCursorState(baselineCursor); await setEmailState(normalizedPurchase.email_address); if (options.logMessage) { await addLog(options.logMessage, options.logLevel || 'ok'); } return normalizedPurchase; } async function findReusableLuckmailPurchaseForFlow(state, client) { const preserveTagInfo = getLuckmailPreserveTagInfo(state); const reusablePurchases = filterReusableLuckmailPurchases( await listLuckmailPurchasesByProject(state, { client, projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, }), { projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, usedPurchases: getLuckmailUsedPurchases(state), preserveTagId: preserveTagInfo.id, preserveTagName: preserveTagInfo.name, now: Date.now(), } ); for (const candidate of reusablePurchases) { try { const aliveResult = await client.user.checkTokenAlive(candidate.token); if (!aliveResult?.alive) { await addLog( `LuckMail:跳过不可复用邮箱 ${candidate.email_address}:${aliveResult?.message || aliveResult?.status || 'token 不可用'}`, 'warn' ); continue; } return candidate; } catch (err) { await addLog(`LuckMail:检测复用邮箱 ${candidate.email_address} 失败:${err.message}`, 'warn'); } } return null; } async function selectLuckmailPurchase(purchaseId) { const state = await ensureManualInteractionAllowed('切换 LuckMail 邮箱'); const client = createLuckmailClient(state); const purchase = await getLuckmailPurchaseById(state, purchaseId, { client, projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, }); if (purchase.user_disabled === 1) { throw new Error(`LuckMail 邮箱 ${purchase.email_address} 已禁用,无法使用。`); } const aliveResult = await client.user.checkTokenAlive(purchase.token); if (!aliveResult?.alive) { throw new Error(`LuckMail 邮箱 ${purchase.email_address} 当前不可用:${aliveResult?.message || aliveResult?.status || 'token 已失效'}`); } const activatedPurchase = await activateLuckmailPurchaseForFlow(state, client, purchase, { initializeCursor: true, logMessage: `LuckMail:已切换当前邮箱为 ${purchase.email_address}`, }); const nextState = await getState(); return buildLuckmailPurchaseView(activatedPurchase, nextState); } async function setLuckmailPurchasePreservedState(purchaseId, preserved) { const state = await ensureManualInteractionAllowed('设置 LuckMail 邮箱保留状态'); const client = createLuckmailClient(state); const purchase = await getLuckmailPurchaseById(state, purchaseId, { client, projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, }); if (preserved) { const preserveTag = await ensureLuckmailPreserveTag(client, state); await client.user.setPurchaseTag(purchase.id, { tagId: preserveTag.id }); } else { await client.user.setPurchaseTag(purchase.id, { tagId: 0 }); } await addLog(`LuckMail:已将 ${purchase.email_address} ${preserved ? '设为保留' : '取消保留'}`, 'ok'); const refreshedState = await getState(); const refreshedPurchase = await getLuckmailPurchaseById(refreshedState, purchase.id, { client, projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, }); return buildLuckmailPurchaseView(refreshedPurchase, await getState()); } async function setLuckmailPurchaseDisabledState(purchaseId, disabled) { const state = await ensureManualInteractionAllowed(disabled ? '禁用 LuckMail 邮箱' : '启用 LuckMail 邮箱'); const client = createLuckmailClient(state); const purchase = await getLuckmailPurchaseById(state, purchaseId, { client, projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, }); await client.user.setPurchaseDisabled(purchase.id, disabled ? 1 : 0); const currentPurchase = getCurrentLuckmailPurchase(await getState()); if (disabled && currentPurchase?.id === purchase.id) { await clearLuckmailRuntimeState({ clearEmail: isLuckmailProvider(await getState()) }); } await addLog(`LuckMail:已将 ${purchase.email_address} ${disabled ? '禁用' : '启用'}`, 'ok'); const refreshedState = await getState(); const refreshedPurchase = await getLuckmailPurchaseById(refreshedState, purchase.id, { client, projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, }); return buildLuckmailPurchaseView(refreshedPurchase, await getState()); } async function batchUpdateLuckmailPurchases(input = {}) { const action = String(input.action || '').trim(); const selectedIds = Array.isArray(input.ids) ? [...new Set(input.ids.map((id) => Number(normalizeLuckmailPurchaseId(id)) || 0).filter((id) => id > 0))] : []; if (!selectedIds.length) { throw new Error('请先选择至少一个 LuckMail 邮箱。'); } const state = await ensureManualInteractionAllowed('批量更新 LuckMail 邮箱'); const client = createLuckmailClient(state); const purchases = await listLuckmailPurchasesByProject(state, { client, projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, }); const purchaseMap = new Map(purchases.map((purchase) => [purchase.id, purchase])); const targetPurchases = selectedIds.map((id) => purchaseMap.get(id)).filter(Boolean); if (!targetPurchases.length) { throw new Error('未找到可批量处理的 openai LuckMail 邮箱。'); } const targetIds = targetPurchases.map((purchase) => purchase.id); if (action === 'used' || action === 'unused') { const nextUsedState = getLuckmailUsedPurchases(state); targetIds.forEach((id) => { const key = normalizeLuckmailPurchaseId(id); if (!key) return; if (action === 'used') { nextUsedState[key] = true; } else { delete nextUsedState[key]; } }); await setLuckmailUsedPurchasesState(nextUsedState); await addLog(`LuckMail:已批量${action === 'used' ? '标记已用' : '标记未用'} ${targetIds.length} 个邮箱`, 'ok'); } else if (action === 'preserve' || action === 'unpreserve') { if (action === 'preserve') { const preserveTag = await ensureLuckmailPreserveTag(client, state); await client.user.batchSetPurchaseTag(targetIds, { tagId: preserveTag.id }); } else { await client.user.batchSetPurchaseTag(targetIds, { tagId: 0 }); } await addLog(`LuckMail:已批量${action === 'preserve' ? '保留' : '取消保留'} ${targetIds.length} 个邮箱`, 'ok'); } else if (action === 'disable' || action === 'enable') { await client.user.batchSetPurchaseDisabled(targetIds, action === 'disable' ? 1 : 0); const currentPurchase = getCurrentLuckmailPurchase(await getState()); if (action === 'disable' && currentPurchase?.id && targetIds.includes(currentPurchase.id)) { await clearLuckmailRuntimeState({ clearEmail: isLuckmailProvider(await getState()) }); } await addLog(`LuckMail:已批量${action === 'disable' ? '禁用' : '启用'} ${targetIds.length} 个邮箱`, 'ok'); } else { throw new Error(`不支持的 LuckMail 批量操作:${action}`); } return { updatedIds: targetIds, }; } async function disableUsedLuckmailPurchases() { const state = await ensureManualInteractionAllowed('禁用已用 LuckMail 邮箱'); const usedPurchases = getLuckmailUsedPurchases(state); const preserveTagInfo = getLuckmailPreserveTagInfo(state); const client = createLuckmailClient(state); const purchases = await listLuckmailPurchasesByProject(state, { client, projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, }); const targets = purchases.filter((purchase) => { const purchaseId = normalizeLuckmailPurchaseId(purchase.id); return Boolean(purchaseId && usedPurchases[purchaseId]) && !isLuckmailPurchasePreserved(purchase, { preserveTagId: preserveTagInfo.id, preserveTagName: preserveTagInfo.name, }) && purchase.user_disabled !== 1; }); if (!targets.length) { return { disabledIds: [] }; } const targetIds = targets.map((purchase) => purchase.id); await client.user.batchSetPurchaseDisabled(targetIds, 1); const currentPurchase = getCurrentLuckmailPurchase(await getState()); if (currentPurchase?.id && targetIds.includes(currentPurchase.id)) { await clearLuckmailRuntimeState({ clearEmail: isLuckmailProvider(await getState()) }); } await addLog(`LuckMail:已禁用 ${targetIds.length} 个本地已用邮箱`, 'ok'); return { disabledIds: targetIds }; } async function ensureLuckmailPurchaseForFlow(options = {}) { const { allowReuse = true } = options; const state = await getState(); const existingPurchase = getCurrentLuckmailPurchase(state); if (allowReuse && existingPurchase?.email_address && existingPurchase?.token) { if (state.email !== existingPurchase.email_address) { await setEmailState(existingPurchase.email_address); } return existingPurchase; } const config = getLuckmailSessionConfig(state); const client = createLuckmailClient(state); if (allowReuse) { const reusablePurchase = await findReusableLuckmailPurchaseForFlow(state, client); if (reusablePurchase) { return activateLuckmailPurchaseForFlow(state, client, reusablePurchase, { initializeCursor: true, logMessage: `LuckMail:已复用 openai 邮箱 ${reusablePurchase.email_address}`, }); } } const result = await client.user.purchaseEmails(DEFAULT_LUCKMAIL_PROJECT_CODE, 1, { emailType: config.emailType, domain: config.domain || undefined, }); const purchases = normalizeLuckmailPurchases(result); const purchase = purchases[0] || null; if (!purchase?.email_address || !purchase?.token) { throw new Error('LuckMail 购邮成功,但未返回可用邮箱或 token。'); } return activateLuckmailPurchaseForFlow(state, client, purchase, { initializeCursor: false, logMessage: `LuckMail:已购买邮箱 ${purchase.email_address}(类型:${config.emailType},项目:${DEFAULT_LUCKMAIL_PROJECT_CODE})`, }); } async function resolveLuckmailVerificationMail(client, token, filters = {}, tokenCodeResult = null) { const tokenCode = tokenCodeResult ? normalizeLuckmailTokenCode(tokenCodeResult) : null; if (tokenCode?.mail) { const tokenMail = tokenCode.verification_code && !tokenCode.mail.verification_code ? { ...tokenCode.mail, verification_code: tokenCode.verification_code, } : tokenCode.mail; const inlineMatch = pickLuckmailVerificationMail([tokenMail], filters); if (inlineMatch) { return inlineMatch; } } const mailList = await client.user.getTokenMails(token); let match = pickLuckmailVerificationMail(mailList.mails, filters); if (match?.mail?.message_id && !match.mail.verification_code) { const detail = await client.user.getTokenMailDetail(token, match.mail.message_id); match = pickLuckmailVerificationMail([detail], filters); } return match || null; } async function legacyPollLuckmailVerificationCode(step, state, pollPayload = {}) { const purchase = getCurrentLuckmailPurchase(state); if (!purchase?.token) { throw new Error('LuckMail 当前没有可用 token,请先执行步骤 3 购买邮箱。'); } const client = createLuckmailClient(state); const maxAttempts = Math.max(1, Number(pollPayload.maxAttempts) || 3); const intervalMs = Math.max(15000, Number(pollPayload.intervalMs) || 15000); const excludedCodes = new Set((pollPayload.excludeCodes || []).filter(Boolean)); const initialCursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor); if (!initialCursor.messageId && !initialCursor.receivedAt) { const mailList = await client.user.getTokenMails(purchase.token); const baselineCursor = buildLuckmailBaselineCursor(mailList?.mails || []); await setLuckmailMailCursorState(baselineCursor); if (baselineCursor?.messageId || baselineCursor?.receivedAt) { await addLog(`步骤 ${step}:LuckMail 已保存当前邮箱旧邮件快照,后续仅使用新收到的验证码。`, 'info'); } } let lastError = null; for (let attempt = 1; attempt <= maxAttempts; attempt++) { throwIfStopped(); await addLog(`步骤 ${step}:正在通过 LuckMail 轮询验证码(${attempt}/${maxAttempts})...`, 'info'); try { const tokenCode = await client.user.getTokenCode(purchase.token); const cursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor); if (tokenCode.verification_code && tokenCode.mail && !isLuckmailMailNewerThanCursor(tokenCode.mail, cursor)) { throw new Error(`步骤 ${step}:LuckMail 返回的最新邮件仍是旧验证码。`); } let match = null; if (tokenCode.has_new_mail || tokenCode.verification_code) { match = await resolveLuckmailVerificationMail(client, purchase.token, filters, tokenCode); } if (!match) { match = await resolveLuckmailVerificationMail(client, purchase.token, filters, null); } if (match?.mail) { const cursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor); if (!isLuckmailMailNewerThanCursor(match.mail, cursor)) { throw new Error(`步骤 ${step}:LuckMail 命中的邮件不是新邮件。`); } await setLuckmailMailCursorState(buildLuckmailMailCursor(match.mail)); return { ok: true, code: match.code, emailTimestamp: normalizeLuckmailTimestamp(match.mail.received_at) || Date.now(), mailId: match.mail.message_id, }; } lastError = new Error(`步骤 ${step}:暂未在 LuckMail 邮箱中找到新的匹配验证码。`); } catch (err) { if (isStopError(err)) { throw err; } lastError = err; await addLog(`步骤 ${step}:LuckMail 轮询失败:${err.message}`, 'warn'); } if (attempt < maxAttempts) { await sleepWithStop(intervalMs); } } throw lastError || new Error(`步骤 ${step}:未在 LuckMail 邮箱中找到新的匹配验证码。`); } async function pollLuckmailVerificationCode(step, state, pollPayload = {}) { const purchase = getCurrentLuckmailPurchase(state); if (!purchase?.token) { throw new Error('LuckMail 当前没有可用 token,请先执行步骤 3 购买邮箱。'); } const client = createLuckmailClient(state); const maxAttempts = Math.max(1, Number(pollPayload.maxAttempts) || 3); const intervalMs = Math.max(15000, Number(pollPayload.intervalMs) || 15000); const excludedCodes = new Set((pollPayload.excludeCodes || []).filter(Boolean)); const initialCursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor); if (!initialCursor.messageId && !initialCursor.receivedAt) { const mailList = await client.user.getTokenMails(purchase.token); const baselineCursor = buildLuckmailBaselineCursor(mailList?.mails || []); await setLuckmailMailCursorState(baselineCursor); if (baselineCursor?.messageId || baselineCursor?.receivedAt) { await addLog(`步骤 ${step}:LuckMail 已保存当前邮箱旧邮件快照,后续仅使用新收到的验证码。`, 'info'); } } let lastError = null; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { throwIfStopped(); await addLog(`步骤 ${step}:正在通过 LuckMail /code 接口轮询验证码(${attempt}/${maxAttempts})...`, 'info'); try { const tokenCode = await client.user.getTokenCode(purchase.token); const remoteEmail = String(tokenCode?.email_address || '').trim().toLowerCase(); const expectedEmail = String(purchase.email_address || state?.email || '').trim().toLowerCase(); if (remoteEmail && expectedEmail && remoteEmail !== expectedEmail) { throw new Error(`步骤 ${step}:LuckMail token 对应邮箱与当前邮箱不一致。当前邮箱:${expectedEmail};token 邮箱:${remoteEmail}`); } const tokenMail = tokenCode.verification_code && tokenCode.mail && !tokenCode.mail.verification_code ? { ...tokenCode.mail, verification_code: tokenCode.verification_code, } : tokenCode.mail; const code = String(tokenCode?.verification_code || tokenMail?.verification_code || '').trim(); const cursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor); if (!code || !tokenMail) { lastError = new Error(`步骤 ${step}:LuckMail /code 接口暂未返回新的验证码。`); } else if (excludedCodes.has(code)) { lastError = new Error(`步骤 ${step}:LuckMail 返回的验证码 ${code} 已试过,等待 15 秒后再次轮询。`); } else if (!isLuckmailMailNewerThanCursor(tokenMail, cursor)) { lastError = new Error(`步骤 ${step}:LuckMail /code 返回的最新邮件仍是旧验证码。`); } else { await setLuckmailMailCursorState(buildLuckmailMailCursor(tokenMail)); return { ok: true, code, emailTimestamp: normalizeLuckmailTimestamp(tokenMail.received_at) || Date.now(), mailId: tokenMail.message_id, }; } } catch (err) { if (isStopError(err)) { throw err; } lastError = err; await addLog(`步骤 ${step}:LuckMail /code 轮询失败:${err.message}`, 'warn'); } if (attempt < maxAttempts) { await sleepWithStop(intervalMs); } } throw lastError || new Error(`步骤 ${step}:未在 LuckMail /code 接口中获取到新的验证码。`); } function summarizeCloudflareTempEmailMessagesForLog(messages) { return (messages || []) .slice() .sort((left, right) => { const leftTime = Date.parse(left.receivedDateTime || '') || 0; const rightTime = Date.parse(right.receivedDateTime || '') || 0; return rightTime - leftTime; }) .slice(0, 3) .map((message) => { const receivedAt = message?.receivedDateTime || '未知时间'; const sender = message?.from?.emailAddress?.address || '未知发件人'; const subject = message?.subject || '(无主题)'; const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80); const address = message?.address || '未知地址'; return `[${address}] ${receivedAt} | ${sender} | ${subject} | ${preview}`; }) .join(' || '); } async function deleteCloudflareTempEmailMail(config, mailId) { const normalizedMailId = String(mailId || '').trim(); if (!normalizedMailId) return false; await requestCloudflareTempEmailJson(config, `/admin/mails/${encodeURIComponent(normalizedMailId)}`, { method: 'DELETE', }); return true; } async function listCloudflareTempEmailMessages(state, options = {}) { const config = ensureCloudflareTempEmailConfig(state, { requireAdminAuth: true }); const address = normalizeCloudflareTempEmailAddress(options.address); const payload = await requestCloudflareTempEmailJson(config, '/admin/mails', { method: 'GET', searchParams: { limit: Number(options.limit) || CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE, offset: Number(options.offset) || 0, address, }, }); const messages = normalizeCloudflareTempEmailMailApiMessages(payload).filter((message) => { if (!address) return true; return !message.address || normalizeCloudflareTempEmailAddress(message.address) === address; }); return { config, messages }; } async function pollCloudflareTempEmailVerificationCode(step, state, pollPayload = {}) { const config = ensureCloudflareTempEmailConfig(state, { requireAdminAuth: true }); const targetEmail = resolveCloudflareTempEmailPollTargetEmail(state, pollPayload, config); const registrationEmail = normalizeCloudflareTempEmailReceiveMailbox(state.email); if (!targetEmail) { throw new Error('Cloudflare Temp Email 轮询前缺少目标邮箱地址,请先填写注册邮箱或“邮件接收”邮箱。'); } if (registrationEmail && registrationEmail !== targetEmail) { await addLog(`步骤 ${step}:正在轮询 Cloudflare Temp Email 收件邮箱(${targetEmail}),注册邮箱为 ${registrationEmail}...`, 'info'); } else { await addLog(`步骤 ${step}:正在轮询 Cloudflare Temp Email 邮件(${targetEmail})...`, 'info'); } const maxAttempts = Number(pollPayload.maxAttempts) || 5; const intervalMs = Number(pollPayload.intervalMs) || 3000; let lastError = null; for (let attempt = 1; attempt <= maxAttempts; attempt++) { throwIfStopped(); try { const { messages } = await listCloudflareTempEmailMessages(state, { address: targetEmail, limit: pollPayload.limit || CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE, offset: pollPayload.offset || 0, }); const matchResult = pickVerificationMessageWithTimeFallback(messages, { afterTimestamp: pollPayload.filterAfterTimestamp || 0, senderFilters: pollPayload.senderFilters || [], subjectFilters: pollPayload.subjectFilters || [], excludeCodes: pollPayload.excludeCodes || [], }); const match = matchResult.match; if (match?.code) { if (matchResult.usedRelaxedFilters) { const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配'; await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Cloudflare Temp Email 验证码。`, 'warn'); } try { await deleteCloudflareTempEmailMail(config, match.message?.id); } catch (err) { await addLog(`步骤 ${step}:删除 Cloudflare Temp Email 邮件失败:${err.message}`, 'warn'); } return { ok: true, code: match.code, emailTimestamp: match.receivedAt || Date.now(), mailId: match.message?.id || '', }; } lastError = new Error(`步骤 ${step}:暂未在 Cloudflare Temp Email 中找到匹配验证码(${attempt}/${maxAttempts})。`); await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info'); const sample = summarizeCloudflareTempEmailMessagesForLog(messages); if (sample) { await addLog(`步骤 ${step}:最近邮件样本:${sample}`, 'info'); } } catch (err) { lastError = err; await addLog(`步骤 ${step}:Cloudflare Temp Email 轮询失败:${err.message}`, 'warn'); } if (attempt < maxAttempts) { await sleepWithStop(intervalMs); } } throw lastError || new Error(`步骤 ${step}:未在 Cloudflare Temp Email 中找到新的匹配验证码。`); } async function getOpenIcloudHostPreference() { try { const tabs = await chrome.tabs.query({ url: ICLOUD_TAB_URL_PATTERNS, }); const activeTab = tabs.find((tab) => tab.active); const candidates = activeTab ? [activeTab, ...tabs.filter((tab) => tab.id !== activeTab.id)] : tabs; for (const tab of candidates) { try { const host = normalizeIcloudHost(new URL(tab.url).host); if (host) return host; } catch {} } } catch {} return ''; } async function getPreferredIcloudLoginUrl(error = null, state = null) { const currentState = state || await getState(); const configuredHost = getConfiguredIcloudHostPreference(currentState); if (configuredHost) { return getIcloudLoginUrlForHost(configuredHost); } const openHost = await getOpenIcloudHostPreference(); if (openHost) { return getIcloudLoginUrlForHost(openHost); } const savedHost = normalizeIcloudHost(currentState?.preferredIcloudHost); if (savedHost) { return getIcloudLoginUrlForHost(savedHost); } const messageHint = getIcloudHostHintFromMessage(getErrorMessage(error)); if (messageHint) { return getIcloudLoginUrlForHost(messageHint); } return getIcloudLoginUrlForHost('icloud.com') || ICLOUD_LOGIN_URLS[0]; } async function getPreferredIcloudSetupUrls(state = null, error = null) { const currentState = state || await getState(); const configuredHost = getConfiguredIcloudHostPreference(currentState); if (configuredHost) { const forcedSetupUrl = getIcloudSetupUrlForHost(configuredHost); if (forcedSetupUrl) { return [forcedSetupUrl]; } } const preferredLoginUrl = await getPreferredIcloudLoginUrl(error, state); const preferredHost = normalizeIcloudHost(new URL(preferredLoginUrl).host); const preferredSetupUrl = getIcloudSetupUrlForHost(preferredHost); if (!preferredSetupUrl) { return [...ICLOUD_SETUP_URLS]; } return [ preferredSetupUrl, ...ICLOUD_SETUP_URLS.filter((url) => url !== preferredSetupUrl), ]; } function isIcloudLoginRequiredError(error) { const message = getErrorMessage(error).toLowerCase(); const hasAuthStatus401 = /\bstatus 401\b/.test(message); const hasAuthStatus403 = /\bstatus 403\b/.test(message); const hasTransientStatus = /\bstatus (409|421|429|5\d\d)\b/.test(message); const hasTransientNetworkHint = message.includes('failed to fetch') || message.includes('networkerror') || message.includes('network request failed') || message.includes('timeout') || message.includes('timed out') || message.includes('cors') || message.includes('address space'); const hasExplicitLoginHint = message.includes('please sign in') || message.includes('sign in required') || message.includes('not logged in') || message.includes('login required') || message.includes('re-authentication required') || message.includes('unauthenticated') || message.includes('authentication required') || message.includes('需要先登录') || message.includes('请先登录'); const hasSelfPromptHint = message.includes('请先在新打开的 icloud 页面中完成登录') || message.includes('请先在当前浏览器登录'); const hasAuthStatusWithExplicitLoginHint = (hasAuthStatus401 || hasAuthStatus403) && hasExplicitLoginHint; // Keep transient validate/network/cors errors out of login-required path. if (message.includes('could not validate icloud session')) { return false; } if (message.includes('page_context:')) { return false; } if (hasSelfPromptHint) { return false; } if (hasTransientStatus || hasTransientNetworkHint) { return false; } if (hasAuthStatusWithExplicitLoginHint) { return true; } if (hasExplicitLoginHint) { return true; } return false; } function isIcloudTransientContextError(error) { const message = getErrorMessage(error).toLowerCase(); return /\bstatus (401|403|409|421|429|5\d\d)\b/.test(message) || message.includes('could not validate icloud session') || message.includes('page_context:') || message.includes('failed to fetch') || message.includes('networkerror') || message.includes('network request failed') || message.includes('cors') || message.includes('address space') || message.includes('timeout') || message.includes('timed out'); } let lastIcloudLoginPromptAt = 0; const activeIcloudRequestControllers = new Set(); let lastResolvedIcloudServiceUrl = ''; const icloudTransientLogThrottle = new Map(); function shouldEmitIcloudTransientLog(key, windowMs = 1500) { const normalizedKey = String(key || '').trim(); if (!normalizedKey) { return true; } const now = Date.now(); const lastAt = Number(icloudTransientLogThrottle.get(normalizedKey) || 0); if (now - lastAt < Math.max(200, Number(windowMs) || 1500)) { return false; } icloudTransientLogThrottle.set(normalizedKey, now); return true; } async function openIcloudLoginPage(preferredUrl) { const tabs = await chrome.tabs.query({ url: ICLOUD_TAB_URL_PATTERNS, }); const preferredHost = new URL(preferredUrl).host; const preferredIcloudHost = normalizeIcloudHost(preferredHost); const existingSameHost = tabs.find((tab) => { try { return normalizeIcloudHost(new URL(tab.url).host) === preferredIcloudHost; } catch { return false; } }); const existingAnyIcloudTab = tabs.find((tab) => Number.isInteger(tab?.id)); if (existingSameHost?.id) { await chrome.tabs.update(existingSameHost.id, { active: true }); return existingSameHost.id; } if (existingAnyIcloudTab?.id) { await chrome.tabs.update(existingAnyIcloudTab.id, { active: true }); return existingAnyIcloudTab.id; } const created = await chrome.tabs.create({ url: preferredUrl, active: true }); return created.id; } async function promptIcloudLogin(error, actionLabel = 'iCloud 操作') { const now = Date.now(); const preferredUrl = await getPreferredIcloudLoginUrl(error); const originalError = getErrorMessage(error); chrome.runtime.sendMessage({ type: 'ICLOUD_LOGIN_REQUIRED', payload: { actionLabel, loginUrl: preferredUrl, message: '需要先登录 iCloud,我已经为你打开登录页。', detail: originalError, }, }).catch(() => { }); if (now - lastIcloudLoginPromptAt < 15000) { return; } lastIcloudLoginPromptAt = now; await addLog(`iCloud:${actionLabel}时需要登录,正在打开 ${new URL(preferredUrl).host} ...`, 'warn'); try { await openIcloudLoginPage(preferredUrl); } catch (tabErr) { await addLog(`iCloud:自动打开登录页失败:${getErrorMessage(tabErr)}`, 'warn'); } } async function withIcloudLoginHelp(actionLabel, action) { const safeActionLabel = String(actionLabel || 'iCloud 操作').trim() || 'iCloud 操作'; const maxTransientAttempts = Math.max(1, Number(ICLOUD_TRANSIENT_RETRY_MAX_ATTEMPTS) || 1); const retryDelayMs = Math.max(300, Number(ICLOUD_TRANSIENT_RETRY_DELAY_MS) || 1200); for (let attempt = 1; attempt <= maxTransientAttempts; attempt += 1) { try { return await action(); } catch (err) { if (isIcloudLoginRequiredError(err)) { await promptIcloudLogin(err, actionLabel); throw new Error('请先在新打开的 iCloud 页面中完成登录,再回来点击“我已登录”。'); } if (isIcloudTransientContextError(err)) { if (attempt < maxTransientAttempts) { if (shouldEmitIcloudTransientLog(`${safeActionLabel}:retry:${attempt}/${maxTransientAttempts}`)) { await addLog(`iCloud:${safeActionLabel}受网络/上下文波动影响,正在重试(${attempt}/${maxTransientAttempts})...`, 'warn'); } await new Promise((resolve) => setTimeout(resolve, retryDelayMs * attempt)); continue; } if (shouldEmitIcloudTransientLog(`${safeActionLabel}:final`)) { await addLog(`iCloud:${safeActionLabel}受网络/上下文波动影响:${getErrorMessage(err)}`, 'warn'); } const safeActionLabel = String(actionLabel || '操作').trim() || '操作'; const transientError = new Error(`iCloud:${safeActionLabel}受网络/上下文波动影响,请稍后重试。`); transientError.code = 'ICLOUD_TRANSIENT_CONTEXT'; transientError.actionLabel = safeActionLabel; transientError.cause = err; throw transientError; } throw err; } } throw new Error('iCloud 操作失败:未知错误。'); } function isIcloudApiUrl(url = '') { const rawUrl = String(url || '').trim(); if (!rawUrl) { return false; } try { const parsedUrl = new URL(rawUrl); if (parsedUrl.protocol !== 'https:') { return false; } const hostname = String(parsedUrl.hostname || '').trim().toLowerCase().replace(/\.$/, ''); if (!hostname) { return false; } return hostname === 'icloud.com' || hostname.endsWith('.icloud.com') || hostname === 'icloud.com.cn' || hostname.endsWith('.icloud.com.cn'); } catch { return false; } } function normalizeIcloudServiceUrl(rawUrl = '') { const value = String(rawUrl || '').trim(); if (!value) { return ''; } try { const parsedUrl = new URL(value); if ((parsedUrl.protocol === 'https:' && parsedUrl.port === '443') || (parsedUrl.protocol === 'http:' && parsedUrl.port === '80')) { parsedUrl.port = ''; } return parsedUrl.toString().replace(/\/$/, ''); } catch { return value.replace(/\/$/, ''); } } function rememberIcloudServiceUrl(rawUrl = '') { const normalized = normalizeIcloudServiceUrl(rawUrl); if (normalized) { lastResolvedIcloudServiceUrl = normalized; } return normalized; } function isIcloudMaildomainwsHost(rawHost = '') { const host = String(rawHost || '').trim().toLowerCase().replace(/\.$/, ''); if (!host) { return false; } return host.endsWith('maildomainws.icloud.com') || host.endsWith('maildomainws.icloud.com.cn'); } function appendIcloudClientQueryParams(rawUrl = '') { const input = String(rawUrl || '').trim(); if (!input) { return ''; } try { const parsed = new URL(input); if (!isIcloudMaildomainwsHost(parsed.hostname)) { return input; } if (!parsed.searchParams.has('clientBuildNumber')) { parsed.searchParams.set('clientBuildNumber', ICLOUD_MAILDOMAINWS_CLIENT_BUILD_NUMBER); } if (!parsed.searchParams.has('clientMasteringNumber')) { parsed.searchParams.set('clientMasteringNumber', ICLOUD_MAILDOMAINWS_CLIENT_BUILD_NUMBER); } if (!parsed.searchParams.has('clientId')) { parsed.searchParams.set('clientId', ''); } if (!parsed.searchParams.has('dsid')) { parsed.searchParams.set('dsid', ''); } return parsed.toString(); } catch { return input; } } function isIcloudMailPageUrl(rawUrl = '') { try { const parsedUrl = new URL(String(rawUrl || '').trim()); if (!normalizeIcloudHost(parsedUrl.hostname)) { return false; } const pathname = String(parsedUrl.pathname || '').toLowerCase(); return pathname === '/mail' || pathname.startsWith('/mail/'); } catch { return false; } } async function waitForIcloudMailTabReady(tabId, timeoutMs = 8000) { if (!Number.isInteger(tabId)) { return false; } const deadline = Date.now() + Math.max(500, Number(timeoutMs) || 8000); while (Date.now() < deadline) { try { const tab = await chrome.tabs.get(tabId); const status = String(tab?.status || ''); if (isIcloudMailPageUrl(tab?.url) && status === 'complete') { return true; } } catch { return false; } await new Promise((resolve) => setTimeout(resolve, 250)); } return false; } async function ensureIcloudMailContextTab(tabs = [], targetHost = '', preferredHost = '') { const tabList = Array.isArray(tabs) ? tabs : []; const normalizedTargetHost = normalizeIcloudHost(targetHost); const normalizedPreferredHost = normalizeIcloudHost(preferredHost); const fallbackHost = normalizedTargetHost || normalizedPreferredHost || await getOpenIcloudHostPreference() || 'icloud.com'; const fallbackMailUrl = getIcloudMailUrlForHost(fallbackHost) || getIcloudMailUrlForHost('icloud.com'); if (!fallbackMailUrl) { return tabList; } const readHostFromTab = (tab) => { try { return normalizeIcloudHost(new URL(String(tab?.url || '')).hostname); } catch { return ''; } }; const mailTabs = tabList.filter((tab) => isIcloudMailPageUrl(tab?.url)); if (mailTabs.length > 0) { if (fallbackHost) { const hasTargetHostMailTab = mailTabs.some((tab) => readHostFromTab(tab) === fallbackHost); if (!hasTargetHostMailTab && Number.isInteger(mailTabs[0]?.id)) { try { await chrome.tabs.update(mailTabs[0].id, { url: fallbackMailUrl, active: false }); await waitForIcloudMailTabReady(mailTabs[0].id, 9000); try { return await chrome.tabs.query({ url: ICLOUD_TAB_URL_PATTERNS, }); } catch { return tabList; } } catch {} } } return tabList; } const sameHostIcloudTab = tabList.find((tab) => ( Number.isInteger(tab?.id) && readHostFromTab(tab) === fallbackHost )); const anyIcloudTab = tabList.find((tab) => Number.isInteger(tab?.id)); try { if (sameHostIcloudTab?.id) { await chrome.tabs.update(sameHostIcloudTab.id, { url: fallbackMailUrl, active: false }); await waitForIcloudMailTabReady(sameHostIcloudTab.id, 9000); } else if (anyIcloudTab?.id) { await chrome.tabs.update(anyIcloudTab.id, { url: fallbackMailUrl, active: false }); await waitForIcloudMailTabReady(anyIcloudTab.id, 9000); } else { const created = await chrome.tabs.create({ url: fallbackMailUrl, active: false }); await waitForIcloudMailTabReady(created?.id, 9000); } } catch {} try { return await chrome.tabs.query({ url: ICLOUD_TAB_URL_PATTERNS, }); } catch { return tabList; } } function shouldTryIcloudRequestPageContextFallback(url, status, errorMessage = '') { if (!isIcloudApiUrl(url)) { return false; } const normalizedStatus = Number(status) || 0; if (normalizedStatus === 401 || normalizedStatus === 403 || normalizedStatus === 409 || normalizedStatus === 421 || normalizedStatus === 429 || normalizedStatus >= 500) { return true; } const message = String(errorMessage || '').toLowerCase(); return message.includes('failed to fetch') || message.includes('network request failed') || message.includes('networkerror') || message.includes('timed out') || message.includes('timeout') || message.includes('cors') || message.includes('address space'); } async function icloudRequestViaPageContext(method, url, options = {}) { const { data, contentType = '', } = options; const state = await getState(); const configuredHost = getConfiguredIcloudHostPreference(state); const targetHost = configuredHost || normalizeIcloudHost(new URL(url).hostname); const preferredHost = configuredHost || normalizeIcloudHost(state?.preferredIcloudHost); let tabs = await chrome.tabs.query({ url: ICLOUD_TAB_URL_PATTERNS, }); tabs = await ensureIcloudMailContextTab(tabs, targetHost, preferredHost); if (!tabs.length) { throw new Error('page_context:no_icloud_tab'); } const sortedTabs = [...tabs].sort((left, right) => { const score = (tab) => { let tabHost = ''; try { tabHost = normalizeIcloudHost(new URL(String(tab?.url || '')).hostname); } catch {} return (isIcloudMailPageUrl(tab?.url) ? 8 : 0) + (tab?.active ? 4 : 0) + (tabHost && tabHost === targetHost ? 2 : 0) + (tabHost && tabHost === preferredHost ? 1 : 0); }; return score(right) - score(left); }); const mailTabs = sortedTabs.filter((tab) => isIcloudMailPageUrl(tab?.url)); const candidateTabs = mailTabs.length ? mailTabs : sortedTabs; const errors = []; for (const tab of candidateTabs) { if (!Number.isInteger(tab?.id)) { continue; } try { const injections = await chrome.scripting.executeScript({ target: { tabId: tab.id, allFrames: false }, world: 'MAIN', func: async (requestConfig) => { const timeoutMs = 15000; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const headers = requestConfig.hasData ? { 'Content-Type': requestConfig.contentType || 'application/json' } : undefined; const response = await fetch(requestConfig.url, { method: requestConfig.method, credentials: 'include', cache: 'no-store', mode: 'cors', headers, body: requestConfig.hasData ? JSON.stringify(requestConfig.data) : undefined, signal: controller.signal, }); const text = await response.text(); return { ok: Boolean(response.ok), status: Number(response.status) || 0, text, error: '', }; } catch (err) { return { ok: false, status: 0, text: '', error: String(err?.message || err || 'unknown error'), }; } finally { clearTimeout(timeoutId); } }, args: [{ method, url, hasData: data !== undefined, data: data === undefined ? null : data, contentType: contentType || '', }], }); const result = injections?.[0]?.result || null; if (!result) { throw new Error('empty result'); } if (!result.ok) { if (result.status) { throw new Error(`status ${result.status}`); } throw new Error(result.error || 'page context request failed'); } if (!String(result.text || '').trim()) { return {}; } try { return JSON.parse(result.text); } catch (parseErr) { throw new Error(`invalid json: ${getErrorMessage(parseErr)}`); } } catch (err) { errors.push(`tab_${tab.id}:${getErrorMessage(err)}`); } } throw new Error(errors.length ? errors.join(' | ') : 'page_context:unknown'); } function getIcloudRequestTargetLabel(rawUrl) { try { const parsed = new URL(rawUrl); return `${parsed.host}${parsed.pathname}`; } catch { return String(rawUrl || '').trim(); } } function getIcloudRetryDelay(attemptIndex) { if (attemptIndex <= 0) return ICLOUD_RETRY_DELAYS_MS[0]; return ICLOUD_RETRY_DELAYS_MS[Math.min(attemptIndex - 1, ICLOUD_RETRY_DELAYS_MS.length - 1)]; } function isIcloudRetryableStatus(status) { return [408, 429, 500, 502, 503, 504].includes(Number(status)); } function isIcloudRetryableError(error) { const status = Number(error?.status || error?.responseStatus || 0); if (status && isIcloudRetryableStatus(status)) { return true; } if (error?.timedOut || error?.networkFailure) { return true; } const message = getErrorMessage(error).toLowerCase(); return message.includes('failed to fetch') || message.includes('networkerror') || message.includes('network error') || message.includes('fetch failed') || message.includes('timed out') || message.includes('timeout') || (error?.name === 'AbortError' && !stopRequested); } function abortActiveIcloudRequests() { for (const controller of [...activeIcloudRequestControllers]) { try { controller.abort(); } catch {} } activeIcloudRequestControllers.clear(); } async function icloudRequest(method, url, options = {}) { const { data, timeoutMs = ICLOUD_REQUEST_TIMEOUT_MS, maxAttempts = 1, retryLabel = '', logRetries = false, } = options; const requestUrl = appendIcloudClientQueryParams(url); const requestContentType = (() => { if (data === undefined) { return ''; } try { return isIcloudMaildomainwsHost(new URL(requestUrl).hostname) ? 'text/plain;charset=UTF-8' : 'application/json'; } catch { return 'application/json'; } })(); let lastError = null; const totalAttempts = Math.max(1, Number(maxAttempts) || 1); for (let attempt = 1; attempt <= totalAttempts; attempt += 1) { throwIfStopped(); const controller = new AbortController(); let response = null; let timeoutTriggered = false; let timeoutId = null; activeIcloudRequestControllers.add(controller); try { timeoutId = setTimeout(() => { timeoutTriggered = true; try { controller.abort(); } catch {} }, Math.max(1000, Number(timeoutMs) || ICLOUD_REQUEST_TIMEOUT_MS)); response = await fetch(requestUrl, { method, credentials: 'include', headers: requestContentType ? { 'Content-Type': requestContentType } : undefined, body: data !== undefined ? JSON.stringify(data) : undefined, signal: controller.signal, }); if (!response.ok) { let responseText = ''; try { responseText = normalizeText(await response.text()).slice(0, 240); } catch {} const error = new Error( responseText ? `iCloud 请求失败:${method} ${requestUrl},status ${response.status},body: ${responseText}` : `iCloud 请求失败:${method} ${requestUrl},status ${response.status}` ); error.status = response.status; throw error; } const rawText = await response.text(); if (!rawText) { return {}; } try { return JSON.parse(rawText); } catch (err) { throw new Error(`iCloud 返回的 JSON 无法解析:${method} ${requestUrl},${err.message}`); } } catch (err) { if (stopRequested) { throw new Error(STOP_ERROR_MESSAGE); } let requestError = err; if (timeoutTriggered || err?.name === 'AbortError') { requestError = new Error(`iCloud 请求超时:${method} ${url},${timeoutMs}ms`); requestError.name = 'IcloudTimeoutError'; requestError.timedOut = true; } else if (!requestError?.status) { const message = getErrorMessage(requestError); if (/failed to fetch|networkerror|network error|fetch failed/i.test(message)) { requestError.networkFailure = true; } } const directErrorMessage = getErrorMessage(requestError) || `iCloud 请求失败:${method} ${requestUrl}`; const shouldTryPageContext = shouldTryIcloudRequestPageContextFallback( requestUrl, Number(requestError?.status) || 0, directErrorMessage ); if (shouldTryPageContext) { try { return await icloudRequestViaPageContext(method, requestUrl, { data, contentType: requestContentType || undefined, }); } catch (pageContextError) { const pageContextMessage = getErrorMessage(pageContextError); if (!pageContextMessage.includes('page_context:no_icloud_tab')) { const mergedError = new Error(`${directErrorMessage} | page_context:${pageContextMessage}`); if (requestError?.status) { mergedError.status = requestError.status; } requestError = mergedError; } } } lastError = requestError; const shouldRetry = attempt < totalAttempts && isIcloudRetryableError(requestError); if (!shouldRetry) { throw requestError; } if (logRetries) { const delayMs = getIcloudRetryDelay(attempt); await addLog( `iCloud:${retryLabel || getIcloudRequestTargetLabel(requestUrl)} 第 ${attempt}/${totalAttempts} 次失败:${getErrorMessage(requestError)},${Math.round(delayMs / 1000)} 秒后重试...`, 'warn' ); } await sleepWithStop(getIcloudRetryDelay(attempt)); } finally { if (timeoutId) { clearTimeout(timeoutId); } activeIcloudRequestControllers.delete(controller); } } throw lastError || new Error(`iCloud 请求失败:${method} ${requestUrl}`); } async function validateIcloudSession(setupUrl) { const data = await icloudRequest('POST', `${setupUrl}/validate`); if (!data?.webservices?.premiummailsettings?.url) { throw new Error('Could not validate iCloud session. Hide My Email service was unavailable.'); } return data; } function shouldTryIcloudPageContextFallback(errors = []) { const combinedMessage = String((errors || []).join(' | ')).toLowerCase(); if (!combinedMessage) { return false; } return combinedMessage.includes('status 401') || combinedMessage.includes('status 403') || combinedMessage.includes('status 421') || combinedMessage.includes('networkerror') || combinedMessage.includes('network request failed') || combinedMessage.includes('failed to fetch') || combinedMessage.includes('timed out') || combinedMessage.includes('timeout') || combinedMessage.includes('cors'); } async function validateIcloudSessionViaPageContext(tabId, setupUrl) { const host = new URL(setupUrl).host; try { const injections = await chrome.scripting.executeScript({ target: { tabId, allFrames: false }, world: 'MAIN', func: async (targetSetupUrl) => { const timeoutMs = 12000; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(`${targetSetupUrl}/validate`, { method: 'POST', credentials: 'include', cache: 'no-store', mode: 'cors', signal: controller.signal, }); const text = await response.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch {} return { ok: Boolean(response.ok), status: Number(response.status) || 0, data, error: '', }; } catch (err) { return { ok: false, status: 0, data: null, error: String(err?.message || err || 'unknown error'), }; } finally { clearTimeout(timeoutId); } }, args: [setupUrl], }); const result = injections?.[0]?.result || null; if (result?.ok && result?.data?.webservices?.premiummailsettings?.url) { return { setupUrl, serviceUrl: normalizeIcloudServiceUrl(result.data.webservices.premiummailsettings.url), resolvedBy: 'page_context', }; } if (result?.status) { throw new Error(`status ${result.status}`); } throw new Error(result?.error || 'page context validate failed'); } catch (err) { throw new Error(`${host}: ${getErrorMessage(err)}`); } } async function resolveIcloudPremiumMailServiceViaPageContext(setupUrls, state, options = {}) { const errors = []; let tabs = []; try { tabs = await chrome.tabs.query({ url: ICLOUD_TAB_URL_PATTERNS, }); } catch (err) { errors.push(`page_context:query_tabs:${getErrorMessage(err)}`); return { service: null, errors, noTab: false }; } const explicitHost = normalizeIcloudHost(options?.hostPreference || options?.preferredHost || ''); const configuredHost = getConfiguredIcloudHostPreference(state); const preferredHost = explicitHost || configuredHost || normalizeIcloudHost(state?.preferredIcloudHost); tabs = await ensureIcloudMailContextTab(tabs, preferredHost, preferredHost); if (!tabs.length) { return { service: null, errors: [], noTab: true }; } const sortedTabs = [...tabs].sort((left, right) => { const leftActive = left?.active ? 1 : 0; const rightActive = right?.active ? 1 : 0; if (leftActive !== rightActive) return rightActive - leftActive; const leftMail = isIcloudMailPageUrl(left?.url) ? 1 : 0; const rightMail = isIcloudMailPageUrl(right?.url) ? 1 : 0; if (leftMail !== rightMail) return rightMail - leftMail; let leftHost = ''; let rightHost = ''; try { leftHost = normalizeIcloudHost(new URL(String(left?.url || '')).host); } catch {} try { rightHost = normalizeIcloudHost(new URL(String(right?.url || '')).host); } catch {} const leftPreferred = leftHost && leftHost === preferredHost ? 1 : 0; const rightPreferred = rightHost && rightHost === preferredHost ? 1 : 0; return rightPreferred - leftPreferred; }); for (const tab of sortedTabs) { if (!Number.isInteger(tab?.id)) { continue; } for (const setupUrl of setupUrls) { try { const service = await validateIcloudSessionViaPageContext(tab.id, setupUrl); return { service, errors }; } catch (err) { errors.push(`page_context:tab_${tab.id}:${getErrorMessage(err)}`); } } } return { service: null, errors, noTab: false }; } async function resolveIcloudPremiumMailService(options = {}) { const errors = []; const state = await getState(); const explicitHost = normalizeIcloudHost(options?.hostPreference || options?.preferredHost || ''); const configuredHost = getConfiguredIcloudHostPreference(state); const effectiveHost = explicitHost || configuredHost; const setupUrls = effectiveHost ? (() => { const forcedSetupUrl = getIcloudSetupUrlForHost(effectiveHost); return forcedSetupUrl ? [forcedSetupUrl] : []; })() : await getPreferredIcloudSetupUrls(state); for (const setupUrl of setupUrls) { try { const data = await validateIcloudSession(setupUrl); const preferredIcloudHost = normalizeIcloudHost(new URL(setupUrl).host); if (preferredIcloudHost && preferredIcloudHost !== normalizeIcloudHost(state.preferredIcloudHost)) { await setState({ preferredIcloudHost }); } return { setupUrl, serviceUrl: rememberIcloudServiceUrl(data.webservices.premiummailsettings.url), }; } catch (err) { errors.push(`${new URL(setupUrl).host}: ${getErrorMessage(err)}`); } } if (shouldTryIcloudPageContextFallback(errors)) { const { service, errors: pageContextErrors, noTab: pageContextNoTab = false, } = await resolveIcloudPremiumMailServiceViaPageContext(setupUrls, state, { hostPreference: effectiveHost, }); if (service) { const preferredIcloudHost = normalizeIcloudHost(new URL(service.setupUrl).host); if (preferredIcloudHost && preferredIcloudHost !== normalizeIcloudHost(state.preferredIcloudHost)) { await setState({ preferredIcloudHost }); } await addLog(`iCloud:后台会话校验失败,已切换页面上下文校验(${new URL(service.setupUrl).host})。`, 'warn'); return { ...service, serviceUrl: rememberIcloudServiceUrl(service.serviceUrl), }; } if (!pageContextNoTab && Array.isArray(pageContextErrors) && pageContextErrors.length) { errors.push(...pageContextErrors); } } throw new Error(errors.length ? `Could not validate iCloud session. ${errors.join(' | ')}` : `Could not validate iCloud session. 请先在当前浏览器登录 ${effectiveHost || 'icloud.com 或 icloud.com.cn'}。`); } function getIcloudAliasLabel() { const now = new Date(); const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; return `MultiPage ${dateStr}`; } async function checkIcloudSession(options = {}) { const actionLabel = String(options?.actionLabel || '检查 iCloud 会话').trim() || '检查 iCloud 会话'; const { actionLabel: _ignoredActionLabel, ...resolveOptions } = options || {}; return withIcloudLoginHelp(actionLabel, async () => { const { setupUrl } = await resolveIcloudPremiumMailService(resolveOptions); await addLog(`iCloud:会话校验通过(${new URL(setupUrl).host})`, 'ok'); return { ok: true, setupUrl }; }); } async function loadNormalizedIcloudAliases(options = {}) { const { resolveOptions = {}, serviceUrl: initialServiceUrl = '', silent = false, } = options; let serviceUrl = String(initialServiceUrl || '').trim().replace(/\/$/, ''); let lastError = null; for (let endpointAttempt = 1; endpointAttempt <= 2; endpointAttempt += 1) { throwIfStopped(); if (!serviceUrl) { const resolved = await resolveIcloudPremiumMailService(resolveOptions); serviceUrl = resolved.serviceUrl; } try { if (!silent) { await addLog(`iCloud:正在从 ${new URL(serviceUrl).host} 加载 Hide My Email 别名列表...`, 'info'); } const response = await icloudRequest('GET', `${serviceUrl}/v2/hme/list`, { timeoutMs: ICLOUD_REQUEST_TIMEOUT_MS, maxAttempts: ICLOUD_LIST_MAX_ATTEMPTS, retryLabel: '加载 iCloud 别名列表', logRetries: true, }); const state = await getState(); return { serviceUrl, aliases: normalizeIcloudAliasList(response, { usedEmails: getEffectiveUsedEmails(state), preservedEmails: getPreservedAliasMap(state), }), }; } catch (err) { lastError = err; if (endpointAttempt >= 2 || !isIcloudRetryableError(err)) { throw err; } await addLog(`iCloud:${new URL(serviceUrl).host} 别名列表请求失败,正在刷新服务节点后重试...`, 'warn'); serviceUrl = ''; } } throw lastError || new Error('加载 iCloud 别名列表失败。'); } async function listIcloudAliases(options = {}) { try { return await withIcloudLoginHelp('加载 iCloud 隐私邮箱列表', async () => { const { serviceUrl } = await resolveIcloudPremiumMailService(options); const response = await icloudRequest('GET', `${serviceUrl}/v2/hme/list`); const state = await getState(); const aliases = normalizeIcloudAliasList(response, { usedEmails: getEffectiveUsedEmails(state), preservedEmails: getPreservedAliasMap(state), }); await setState({ icloudAliasCache: normalizeIcloudAliasCacheList(aliases), icloudAliasCacheAt: Date.now(), }); return aliases; }); } catch (err) { const message = getErrorMessage(err); const transientContextError = err?.code === 'ICLOUD_TRANSIENT_CONTEXT' || message.includes('网络/上下文波动'); if (!transientContextError) { throw err; } const state = await getState(); const freshCachedAliases = getIcloudAliasCacheFromState(state); if (freshCachedAliases.length) { await addLog(`iCloud:加载别名失败,已回退最近缓存(${freshCachedAliases.length} 条)。`, 'warn'); return freshCachedAliases; } const staleCachedAliases = getIcloudAliasCacheFromState(state, { maxAgeMs: 0 }); if (staleCachedAliases.length) { await addLog(`iCloud:加载别名失败,已回退历史缓存(${staleCachedAliases.length} 条)。`, 'warn'); return staleCachedAliases; } const localFallbackAliases = buildIcloudAliasFallbackFromLocalState(state); if (localFallbackAliases.length) { await addLog(`iCloud:加载别名失败,已回退本地别名记录(${localFallbackAliases.length} 条)。`, 'warn'); return localFallbackAliases; } throw err; } } async function deleteIcloudAlias(payload) { return withIcloudLoginHelp('删除 iCloud 隐私邮箱', async () => { const alias = typeof payload === 'string' ? { email: String(payload).trim().toLowerCase(), anonymousId: '' } : { email: String(payload?.email || '').trim().toLowerCase(), anonymousId: String(payload?.anonymousId || '').trim(), }; if (!alias.email) { throw new Error('未提供需要删除的 iCloud 隐私邮箱。'); } if (!alias.anonymousId) { throw new Error(`缺少 ${alias.email} 的 anonymousId,请先刷新 iCloud 别名列表。`); } let serviceUrl = ''; try { ({ serviceUrl } = await resolveIcloudPremiumMailService()); } catch (resolveErr) { const canFallbackToCachedService = isIcloudTransientContextError(resolveErr) && Boolean(lastResolvedIcloudServiceUrl); if (!canFallbackToCachedService) { throw resolveErr; } serviceUrl = lastResolvedIcloudServiceUrl; await addLog(`iCloud:会话校验暂时不可用,已回退最近可用服务节点 ${new URL(serviceUrl).host} 继续删除。`, 'warn'); } try { const directDelete = await icloudRequest('POST', `${serviceUrl}/v1/hme/delete`, { data: { anonymousId: alias.anonymousId }, }); if (directDelete?.success === false) { throw new Error(directDelete?.error?.errorMessage || 'delete failed'); } } catch (err) { await addLog(`iCloud:直接删除 ${alias.email} 失败,尝试先停用再删除...`, 'warn'); const deactivated = await icloudRequest('POST', `${serviceUrl}/v1/hme/deactivate`, { data: { anonymousId: alias.anonymousId }, }); if (deactivated?.success === false) { throw new Error(deactivated?.error?.errorMessage || `停用 ${alias.email} 失败`); } const deleted = await icloudRequest('POST', `${serviceUrl}/v1/hme/delete`, { data: { anonymousId: alias.anonymousId }, }); if (deleted?.success === false) { throw new Error(deleted?.error?.errorMessage || `删除 ${alias.email} 失败`); } } const state = await getState(); const manualAliasUsage = getManualAliasUsageMap(state); const preservedAliases = getPreservedAliasMap(state); delete manualAliasUsage[alias.email]; delete preservedAliases[alias.email]; await setState({ manualAliasUsage, preservedAliases }); await addLog(`iCloud:已删除 ${alias.email}`, 'ok'); broadcastIcloudAliasesChanged({ reason: 'deleted', email: alias.email }); return { email: alias.email }; }); } async function deleteUsedIcloudAliases() { const aliases = await listIcloudAliases(); const usedAliases = aliases.filter((alias) => alias.used); if (!usedAliases.length) { return { deleted: [], skipped: [] }; } const deleted = []; const skipped = []; for (const alias of usedAliases) { if (alias.preserved) { skipped.push({ email: alias.email, error: 'preserved' }); continue; } try { await deleteIcloudAlias(alias); deleted.push(alias.email); } catch (err) { skipped.push({ email: alias.email, error: getErrorMessage(err) }); } } return { deleted, skipped }; } async function fetchIcloudHideMyEmail(options = {}) { return withIcloudLoginHelp('获取 iCloud 隐私邮箱', async () => { throwIfStopped(); const generateNew = Boolean(options?.generateNew); const preferredHost = String(options?.hostPreference || options?.preferredHost || '').trim(); await addLog('iCloud:正在加载别名列表并校验当前浏览器登录状态...', 'info'); const { serviceUrl, setupUrl } = await resolveIcloudPremiumMailService( preferredHost ? { hostPreference: preferredHost } : {} ); await addLog(`iCloud:已通过 ${new URL(setupUrl).host} 验证会话`, 'ok'); await addLog(`iCloud:当前 Hide My Email 服务节点 ${new URL(serviceUrl).host}`, 'info'); let activeServiceUrl = serviceUrl; const existingAliases = await listIcloudAliases(); const existingAliasEmailSet = new Set( existingAliases .map((aliasItem) => String(aliasItem?.email || '').trim().toLowerCase()) .filter(Boolean) ); if (!generateNew) { const reusableAlias = pickReusableIcloudAlias(existingAliases); if (reusableAlias) { await setEmailState(reusableAlias.email); await addLog(`iCloud:复用未使用别名 ${reusableAlias.email}`, 'ok'); broadcastIcloudAliasesChanged({ reason: 'selected', email: reusableAlias.email }); return reusableAlias.email; } } else { await addLog('iCloud:已启用“始终创建新别名”,本次将跳过复用。', 'info'); } await addLog('iCloud:没有可复用别名,开始生成新的 Hide My Email 地址...', 'warn'); await addLog(`iCloud:正在向 ${new URL(activeServiceUrl).host} 请求新的 Hide My Email 候选地址...`, 'info'); try { let generated = null; try { generated = await icloudRequest('POST', `${activeServiceUrl}/v1/hme/generate`, { timeoutMs: ICLOUD_REQUEST_TIMEOUT_MS, maxAttempts: ICLOUD_WRITE_MAX_ATTEMPTS, retryLabel: '生成 Hide My Email 地址', logRetries: true, }); } catch (err) { if (!isIcloudRetryableError(err)) { throw err; } await addLog('iCloud:生成候选别名失败,正在刷新服务节点后再试一次...', 'warn'); const refreshedService = await resolveIcloudPremiumMailService( preferredHost ? { hostPreference: preferredHost } : {} ); activeServiceUrl = refreshedService.serviceUrl; generated = await icloudRequest('POST', `${activeServiceUrl}/v1/hme/generate`, { timeoutMs: ICLOUD_REQUEST_TIMEOUT_MS, maxAttempts: ICLOUD_WRITE_MAX_ATTEMPTS, retryLabel: '生成 Hide My Email 地址', logRetries: true, }); } if (!generated?.success || !generated?.result?.hme) { throw new Error(generated?.error?.errorMessage || 'iCloud 隐私邮箱生成失败。'); } const generatedHmeRaw = generated.result.hme; const generatedAlias = String( (typeof generatedHmeRaw === 'string' ? generatedHmeRaw : generatedHmeRaw?.hme || generatedHmeRaw?.email || generatedHmeRaw?.alias || generatedHmeRaw?.address || '') ).trim().toLowerCase(); if (!generatedAlias) { throw new Error('iCloud 隐私邮箱生成失败:未返回可用别名。'); } await addLog(`iCloud:已生成候选别名 ${generatedAlias},正在保留...`, 'info'); const reserveData = { ...(generatedHmeRaw && typeof generatedHmeRaw === 'object' && !Array.isArray(generatedHmeRaw) ? generatedHmeRaw : {}), hme: generatedAlias, label: getIcloudAliasLabel(), note: 'Generated through Multi-Page Automation', }; let alias = ''; try { const reserved = await icloudRequest('POST', `${activeServiceUrl}/v1/hme/reserve`, { data: reserveData, timeoutMs: ICLOUD_REQUEST_TIMEOUT_MS, maxAttempts: 1, }); if (!reserved?.success || !reserved?.result?.hme?.hme) { throw new Error(reserved?.error?.errorMessage || 'iCloud 隐私邮箱保留失败。'); } alias = String(reserved.result.hme.hme || '').trim().toLowerCase(); } catch (reserveErr) { const reserveErrMessage = getErrorMessage(reserveErr); const shouldTryListFallback = isIcloudRetryableError(reserveErr) || /\bstatus (?:401|403|409)\b/i.test(reserveErrMessage) || /failed to fetch/i.test(reserveErrMessage); if (!shouldTryListFallback) { throw reserveErr; } await addLog('iCloud:保留别名返回鉴权/网络异常,正在回查别名列表确认是否已创建...', 'warn'); const { aliases: aliasesAfterReserveFailure, serviceUrl: refreshedListServiceUrl } = await loadNormalizedIcloudAliases({ serviceUrl: activeServiceUrl, silent: true, }); activeServiceUrl = refreshedListServiceUrl || activeServiceUrl; let recoveredAlias = findIcloudAliasByEmail(aliasesAfterReserveFailure, generatedAlias); if (!recoveredAlias) { recoveredAlias = aliasesAfterReserveFailure.find( (aliasItem) => !existingAliasEmailSet.has(String(aliasItem?.email || '').trim().toLowerCase()) ) || null; } if (recoveredAlias?.email) { alias = String(recoveredAlias.email || '').trim().toLowerCase(); await addLog(`iCloud:保留请求异常,但已在列表确认别名 ${alias},继续使用。`, 'warn'); } else if (isIcloudRetryableError(reserveErr)) { await addLog(`iCloud:列表中尚未出现 ${generatedAlias},正在刷新服务节点后重试保留一次...`, 'warn'); const refreshedService = await resolveIcloudPremiumMailService( preferredHost ? { hostPreference: preferredHost } : {} ); activeServiceUrl = refreshedService.serviceUrl; const reservedRetry = await icloudRequest('POST', `${activeServiceUrl}/v1/hme/reserve`, { data: reserveData, timeoutMs: ICLOUD_REQUEST_TIMEOUT_MS, maxAttempts: 1, }); if (!reservedRetry?.success || !reservedRetry?.result?.hme?.hme) { throw new Error(reservedRetry?.error?.errorMessage || 'iCloud 隐私邮箱保留失败。'); } alias = String(reservedRetry.result.hme.hme || '').trim().toLowerCase(); } else { alias = generatedAlias; await addLog(`iCloud:保留请求异常,已回退使用生成别名 ${alias}。`, 'warn'); } } await setEmailState(alias); await addLog(`iCloud:已创建并保留新别名 ${alias}`, 'ok'); broadcastIcloudAliasesChanged({ reason: 'created', email: alias }); return alias; } catch (err) { if (!shouldStopIcloudAutoFetchRetries(err)) { throw err; } const reusableAlias = pickReusableIcloudAlias(existingAliases); if (reusableAlias) { await setEmailState(reusableAlias.email); await addLog( `iCloud:当前网络/上下文波动,暂无法创建新别名,已临时回退复用 ${reusableAlias.email}。`, 'warn' ); broadcastIcloudAliasesChanged({ reason: 'selected', email: reusableAlias.email }); return reusableAlias.email; } throw new Error( `iCloud 当前无法创建新别名:${getErrorMessage(err)}。请先确认 iCloud 页面已登录且网络可访问,再重试。` ); } }); } async function finalizeIcloudAliasAfterSuccessfulFlow(state) { const email = String(state?.email || '').trim().toLowerCase(); if (!email) { return { handled: false, deleted: false }; } const knownIcloudAlias = normalizeEmailGenerator(state?.emailGenerator) === 'icloud' || Object.prototype.hasOwnProperty.call(getManualAliasUsageMap(state), email) || Object.prototype.hasOwnProperty.call(getPreservedAliasMap(state), email); if (!knownIcloudAlias) { return { handled: false, deleted: false }; } await setIcloudAliasUsedState({ email, used: true }, { silentLog: true }); await addLog(`iCloud:流程成功后已标记 ${email} 为已用。`, 'ok'); if (!state.autoDeleteUsedIcloudAlias) { return { handled: true, deleted: false }; } if (isAliasPreserved(state, email)) { await addLog(`iCloud:${email} 已被标记为保留,跳过自动删除。`, 'info'); return { handled: true, deleted: false }; } try { const aliases = await listIcloudAliases(); const alias = findIcloudAliasByEmail(aliases, email); if (!alias) { await addLog(`iCloud:自动删除跳过,列表中未找到 ${email}。`, 'warn'); return { handled: true, deleted: false }; } if (alias.preserved) { await addLog(`iCloud:${email} 在最新别名列表中已是保留状态,跳过自动删除。`, 'info'); return { handled: true, deleted: false }; } if (!alias.anonymousId) { await addLog(`iCloud:自动删除跳过,${email} 缺少 anonymousId,请先刷新列表后重试。`, 'warn'); return { handled: true, deleted: false }; } await deleteIcloudAlias(alias); await addLog(`iCloud:流程成功后已自动删除 ${email}。`, 'ok'); return { handled: true, deleted: true }; } catch (err) { if (isIcloudTransientContextError(err)) { await addLog(`iCloud:自动删除 ${email} 暂时跳过(网络/上下文波动),可稍后手动删除。`, 'info'); } else { await addLog(`iCloud:自动删除 ${email} 失败:${getErrorMessage(err)}`, 'warn'); } return { handled: true, deleted: false }; } } async function finalizePhoneActivationAfterSuccessfulFlow(state) { if (typeof phoneVerificationHelpers?.finalizePendingPhoneActivationConfirmation !== 'function') { return null; } return phoneVerificationHelpers.finalizePendingPhoneActivationConfirmation(state); } // ============================================================ // Tab Registry // ============================================================ async function getTabRegistry() { return tabRuntime.getTabRegistry(); } async function registerTab(source, tabId) { return tabRuntime.registerTab(source, tabId); } async function isTabAlive(source) { return tabRuntime.isTabAlive(source); } async function getTabId(source) { return tabRuntime.getTabId(source); } function parseUrlSafely(rawUrl) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.parseUrlSafely) { return navigationUtils.parseUrlSafely(rawUrl); } if (!rawUrl) return null; try { return new URL(rawUrl); } catch { return null; } } function normalizeSub2ApiUrl(rawUrl) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.normalizeSub2ApiUrl) { return navigationUtils.normalizeSub2ApiUrl(rawUrl); } const input = (rawUrl || '').trim() || DEFAULT_SUB2API_URL; const withProtocol = /^https?:\/\//i.test(input) ? input : `https://${input}`; const parsed = new URL(withProtocol); if (!parsed.pathname || parsed.pathname === '/') { parsed.pathname = '/admin/accounts'; } parsed.hash = ''; return parsed.toString(); } function normalizeCodex2ApiUrl(rawUrl) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.normalizeCodex2ApiUrl) { return navigationUtils.normalizeCodex2ApiUrl(rawUrl); } const input = (rawUrl || '').trim() || DEFAULT_CODEX2API_URL; const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`; const parsed = new URL(withProtocol); if (!parsed.pathname || parsed.pathname === '/' || parsed.pathname === '/admin') { parsed.pathname = '/admin/accounts'; } parsed.hash = ''; return parsed.toString(); } function getPanelMode(state = {}) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.getPanelMode) { return navigationUtils.getPanelMode(state); } if (state.panelMode === 'sub2api') { return 'sub2api'; } if (state.panelMode === 'codex2api') { return 'codex2api'; } return 'cpa'; } function getPanelModeLabel(modeOrState) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.getPanelModeLabel) { return navigationUtils.getPanelModeLabel(modeOrState); } const mode = typeof modeOrState === 'string' ? modeOrState : getPanelMode(modeOrState); if (mode === 'sub2api') { return 'SUB2API'; } if (mode === 'codex2api') { return 'Codex2API'; } return 'CPA'; } function isSignupPageHost(hostname = '') { if (typeof navigationUtils !== 'undefined' && navigationUtils?.isSignupPageHost) { return navigationUtils.isSignupPageHost(hostname); } return ['auth0.openai.com', 'auth.openai.com', 'accounts.openai.com'].includes(hostname); } function isSignupEntryHost(hostname = '') { if (typeof navigationUtils !== 'undefined' && navigationUtils?.isSignupEntryHost) { return navigationUtils.isSignupEntryHost(hostname); } return ['chatgpt.com', 'chat.openai.com'].includes(hostname); } function isSignupPasswordPageUrl(rawUrl) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.isSignupPasswordPageUrl) { return navigationUtils.isSignupPasswordPageUrl(rawUrl); } const parsed = parseUrlSafely(rawUrl); if (!parsed) return false; return isSignupPageHost(parsed.hostname) && /\/create-account\/password(?:[/?#]|$)/i.test(parsed.pathname || ''); } function isSignupEmailVerificationPageUrl(rawUrl) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.isSignupEmailVerificationPageUrl) { return navigationUtils.isSignupEmailVerificationPageUrl(rawUrl); } const parsed = parseUrlSafely(rawUrl); if (!parsed) return false; return isSignupPageHost(parsed.hostname) && /\/email-verification(?:[/?#]|$)/i.test(parsed.pathname || ''); } function is163MailHost(hostname = '') { if (typeof navigationUtils !== 'undefined' && navigationUtils?.is163MailHost) { return navigationUtils.is163MailHost(hostname); } return hostname === 'mail.163.com' || hostname.endsWith('.mail.163.com') || hostname === 'mail.126.com' || hostname.endsWith('.mail.126.com') || hostname === 'webmail.vip.163.com'; } function isLocalhostOAuthCallbackUrl(rawUrl) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.isLocalhostOAuthCallbackUrl) { return navigationUtils.isLocalhostOAuthCallbackUrl(rawUrl); } const parsed = parseUrlSafely(rawUrl); if (!parsed) return false; if (!['http:', 'https:'].includes(parsed.protocol)) return false; if (!['localhost', '127.0.0.1'].includes(parsed.hostname)) return false; if (!['/auth/callback', '/codex/callback'].includes(parsed.pathname)) return false; const code = (parsed.searchParams.get('code') || '').trim(); const state = (parsed.searchParams.get('state') || '').trim(); return Boolean(code && state); } function isLocalCpaUrl(rawUrl) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.isLocalCpaUrl) { return navigationUtils.isLocalCpaUrl(rawUrl); } const parsed = parseUrlSafely(rawUrl); if (!parsed) return false; if (!['http:', 'https:'].includes(parsed.protocol)) return false; return ['localhost', '127.0.0.1'].includes(parsed.hostname); } function shouldBypassStep9ForLocalCpa(state) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.shouldBypassStep9ForLocalCpa) { return navigationUtils.shouldBypassStep9ForLocalCpa(state); } return normalizeLocalCpaStep9Mode(state?.localCpaStep9Mode) === 'bypass' && Boolean(state?.localhostUrl) && isLocalCpaUrl(state?.vpsUrl); } function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.matchesSourceUrlFamily) { return navigationUtils.matchesSourceUrlFamily(source, candidateUrl, referenceUrl); } const candidate = parseUrlSafely(candidateUrl); if (!candidate) return false; const reference = parseUrlSafely(referenceUrl); switch (source) { case 'signup-page': return isSignupPageHost(candidate.hostname) || isSignupEntryHost(candidate.hostname); case 'duck-mail': return candidate.hostname === 'duckduckgo.com' && candidate.pathname.startsWith('/email/'); case 'qq-mail': return candidate.hostname === 'mail.qq.com' || candidate.hostname === 'wx.mail.qq.com'; case 'mail-163': return is163MailHost(candidate.hostname); case 'gmail-mail': return candidate.hostname === 'mail.google.com'; case 'inbucket-mail': return Boolean(reference) && candidate.origin === reference.origin && candidate.pathname.startsWith('/m/'); case 'mail-2925': return candidate.hostname === '2925.com' || candidate.hostname === 'www.2925.com'; case 'vps-panel': return Boolean(reference) && candidate.origin === reference.origin && candidate.pathname === reference.pathname; case 'sub2api-panel': return Boolean(reference) && candidate.origin === reference.origin && (candidate.pathname.startsWith('/admin/accounts') || candidate.pathname.startsWith('/login') || candidate.pathname === '/'); default: return false; } } async function rememberSourceLastUrl(source, url) { return tabRuntime.rememberSourceLastUrl(source, url); } async function closeConflictingTabsForSource(source, currentUrl, options = {}) { return tabRuntime.closeConflictingTabsForSource(source, currentUrl, options); } function isLocalhostOAuthCallbackTabMatch(callbackUrl, candidateUrl) { return tabRuntime.isLocalhostOAuthCallbackTabMatch(callbackUrl, candidateUrl); } async function closeLocalhostCallbackTabs(callbackUrl, options = {}) { return tabRuntime.closeLocalhostCallbackTabs(callbackUrl, options); } function buildLocalhostCleanupPrefix(rawUrl) { return tabRuntime.buildLocalhostCleanupPrefix(rawUrl); } async function closeTabsByUrlPrefix(prefix, options = {}) { return tabRuntime.closeTabsByUrlPrefix(prefix, options); } async function pingContentScriptOnTab(tabId) { return tabRuntime.pingContentScriptOnTab(tabId); } async function waitForTabUrlFamily(source, tabId, referenceUrl, options = {}) { return tabRuntime.waitForTabUrlFamily(source, tabId, referenceUrl, options); } async function waitForTabUrlMatch(tabId, matcher, options = {}) { return tabRuntime.waitForTabUrlMatch(tabId, matcher, options); } async function waitForTabUrlMatchUntilStopped(tabId, matcher, options = {}) { const retryDelayMs = Math.max(100, Math.floor(Number(options.retryDelayMs) || 300)); while (true) { throwIfStopped(); const tab = await chrome.tabs.get(tabId).catch(() => null); if (!tab) { throw new Error('目标标签页已关闭,无法继续等待页面跳转。'); } if (typeof matcher === 'function' && matcher(tab.url || '', tab)) { return tab; } await sleepWithStop(retryDelayMs); } } async function waitForTabComplete(tabId, options = {}) { return tabRuntime.waitForTabComplete(tabId, options); } async function waitForTabStableComplete(tabId, options = {}) { return tabRuntime.waitForTabStableComplete(tabId, options); } async function waitForTabCompleteUntilStopped(tabId, options = {}) { const retryDelayMs = Math.max(100, Math.floor(Number(options.retryDelayMs) || 300)); while (true) { throwIfStopped(); const tab = await chrome.tabs.get(tabId).catch(() => null); if (!tab) { throw new Error('目标标签页已关闭,无法继续等待页面加载完成。'); } if (tab.status === 'complete') { return tab; } await sleepWithStop(retryDelayMs); } } async function ensureContentScriptReadyOnTab(source, tabId, options = {}) { return tabRuntime.ensureContentScriptReadyOnTab(source, tabId, options); } function isContentScriptReadyPong(source, pong) { if (!pong?.ok) return false; if (pong.source && pong.source !== source) return false; if (source === 'plus-checkout') { return Boolean(pong.plusCheckoutReady); } return true; } function isUnrecoverableContentScriptInjectError(error) { return /Could not load file/i.test(String(error?.message || error || '')); } async function ensureContentScriptReadyOnTabUntilStopped(source, tabId, options = {}) { const { inject = null, injectSource = null, retryDelayMs = 700, logMessage = '', } = options; let logged = false; while (true) { throwIfStopped(); const pong = await pingContentScriptOnTab(tabId); if (isContentScriptReadyPong(source, pong)) { await registerTab(source, tabId); return; } if (!inject || !inject.length) { throw new Error(`${getSourceLabel(source)} 内容脚本未就绪,且未提供可用的注入文件。`); } try { if (injectSource) { await chrome.scripting.executeScript({ target: { tabId }, func: (injectedSource) => { window.__MULTIPAGE_SOURCE = injectedSource; }, args: [injectSource], }); } await chrome.scripting.executeScript({ target: { tabId }, files: inject, }); } catch (error) { console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTabUntilStopped] inject failed for ${source}:`, error?.message || error); if (isUnrecoverableContentScriptInjectError(error)) { throw new Error(`${getSourceLabel(source)} 内容脚本文件加载失败:${error?.message || error}。请在扩展管理页重新加载当前扩展,确认文件已包含在已加载的扩展目录中。`); } } const pongAfterInject = await pingContentScriptOnTab(tabId); if (isContentScriptReadyPong(source, pongAfterInject)) { await registerTab(source, tabId); return; } if (logMessage && !logged) { logged = true; await addLog(logMessage, 'warn'); } await sleepWithStop(retryDelayMs); } } // ============================================================ // Command Queue (for content scripts not yet ready) // ============================================================ const pendingCommands = new Map(); // source -> { message, resolve, reject, timer } function getContentScriptResponseTimeoutMs(message) { return tabRuntime.getContentScriptResponseTimeoutMs(message); } function getMessageDebugLabel(source, message, tabId = null) { return tabRuntime.getMessageDebugLabel(source, message, tabId); } function summarizeMessageResultForDebug(result) { return tabRuntime.summarizeMessageResultForDebug(result); } function sendTabMessageWithTimeout(tabId, source, message, responseTimeoutMs = getContentScriptResponseTimeoutMs(message)) { return tabRuntime.sendTabMessageWithTimeout(tabId, source, message, responseTimeoutMs); } async function sendTabMessageUntilStopped(tabId, source, message, options = {}) { const retryDelayMs = Math.max(100, Math.floor(Number(options.retryDelayMs) || 300)); while (true) { throwIfStopped(); try { return await chrome.tabs.sendMessage(tabId, message); } catch (error) { if (!isRetryableContentScriptTransportError(error)) { throw error; } await sleepWithStop(retryDelayMs); } } } function queueCommand(source, message, timeout = 15000) { return tabRuntime.queueCommand(source, message, timeout); } function flushCommand(source, tabId) { return tabRuntime.flushCommand(source, tabId); } function cancelPendingCommands(reason = STOP_ERROR_MESSAGE) { return tabRuntime.cancelPendingCommands(reason); } // ============================================================ // Reuse or create tab // ============================================================ async function reuseOrCreateTab(source, url, options = {}) { return tabRuntime.reuseOrCreateTab(source, url, options); } // ============================================================ // Send command to content script (with readiness check) // ============================================================ async function sendToContentScript(source, message, options = {}) { return tabRuntime.sendToContentScript(source, message, options); } async function sendToContentScriptResilient(source, message, options = {}) { return tabRuntime.sendToContentScriptResilient(source, message, options); } async function sendToMailContentScriptResilient(mail, message, options = {}) { return tabRuntime.sendToMailContentScriptResilient(mail, message, options); } // ============================================================ // Logging // ============================================================ async function addLog(message, level = 'info') { if (typeof loggingStatus !== 'undefined' && loggingStatus?.addLog) { return loggingStatus.addLog(message, level); } const state = await getState(); const logs = state.logs || []; const entry = { message, level, timestamp: Date.now() }; logs.push(entry); if (logs.length > 500) logs.splice(0, logs.length - 500); await setState({ logs }); chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => { }); } function getStep8CallbackUrlFromNavigation(details, signupTabId) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.getStep8CallbackUrlFromNavigation) { return navigationUtils.getStep8CallbackUrlFromNavigation(details, signupTabId); } if (!Number.isInteger(signupTabId) || !details) return ''; if (details.tabId !== signupTabId) return ''; if (details.frameId !== 0) return ''; return isLocalhostOAuthCallbackUrl(details.url) ? details.url : ''; } function getStep8CallbackUrlFromTabUpdate(tabId, changeInfo, tab, signupTabId) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.getStep8CallbackUrlFromTabUpdate) { return navigationUtils.getStep8CallbackUrlFromTabUpdate(tabId, changeInfo, tab, signupTabId); } if (!Number.isInteger(signupTabId) || tabId !== signupTabId) return ''; const candidates = [changeInfo?.url, tab?.url]; for (const candidate of candidates) { if (isLocalhostOAuthCallbackUrl(candidate)) return candidate; } return ''; } function getSourceLabel(source) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.getSourceLabel) { return loggingStatus.getSourceLabel(source); } const labels = { 'gmail-mail': 'Gmail 邮箱', 'sidepanel': '侧边栏', 'signup-page': '认证页', 'vps-panel': 'CPA 面板', 'sub2api-panel': 'SUB2API 后台', 'qq-mail': 'QQ 邮箱', 'mail-163': '163 邮箱', 'mail-2925': '2925 邮箱', 'inbucket-mail': 'Inbucket 邮箱', 'duck-mail': 'Duck 邮箱', 'hotmail-api': 'Hotmail(API对接/本地助手)', 'luckmail-api': 'LuckMail(API 购邮)', 'cloudflare-temp-email': 'Cloudflare Temp Email', 'plus-checkout': 'Plus Checkout', 'paypal-flow': 'PayPal 授权页', }; return labels[source] || source || '未知来源'; } // ============================================================ // Step Status Management // ============================================================ async function setStepStatus(step, status) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.setStepStatus) { return loggingStatus.setStepStatus(step, status); } const state = await getState(); const statuses = { ...state.stepStatuses }; statuses[step] = status; await setState({ stepStatuses: statuses, currentStep: step }); chrome.runtime.sendMessage({ type: 'STEP_STATUS_CHANGED', payload: { step, status }, }).catch(() => { }); } function isStopError(error) { const message = typeof error === 'string' ? error : error?.message; return message === STOP_ERROR_MESSAGE; } function isRetryableContentScriptTransportError(error) { const message = String(typeof error === 'string' ? error : error?.message || ''); return /back\/forward cache|message channel is closed|Receiving end does not exist|port closed before a response was received|A listener indicated an asynchronous response|did not respond in \d+s|failed to fetch|networkerror|network error|fetch failed|load failed/i.test(message); } function isStepFetchNetworkRetryableError(error) { const message = String(getErrorMessage(error) || '').toLowerCase(); return /failed to fetch|networkerror|network error|fetch failed|load failed|net::err_/i.test(message); } function getStepFetchNetworkRetryPolicy(step) { if (typeof STEP_FETCH_NETWORK_RETRY_POLICIES === 'undefined' || !(STEP_FETCH_NETWORK_RETRY_POLICIES instanceof Map)) { return null; } const policy = STEP_FETCH_NETWORK_RETRY_POLICIES.get(Number(step)); if (!policy) { return null; } return { maxAttempts: Math.max(1, Math.floor(Number(policy.maxAttempts) || 1)), cooldownMs: Math.max(0, Math.floor(Number(policy.cooldownMs) || 0)), }; } const navigationUtils = self.MultiPageBackgroundNavigationUtils?.createNavigationUtils({ DEFAULT_CODEX2API_URL, DEFAULT_SUB2API_URL, normalizeLocalCpaStep9Mode, }); const loggingStatus = self.MultiPageBackgroundLoggingStatus?.createLoggingStatus({ chrome, DEFAULT_STATE, getState, isRecoverableStep9AuthFailure, LOG_PREFIX, setState, STOP_ERROR_MESSAGE, }); const tabRuntime = self.MultiPageBackgroundTabRuntime?.createTabRuntime({ addLog, chrome, getSourceLabel, getState, isLocalhostOAuthCallbackUrl, isRetryableContentScriptTransportError, LOG_PREFIX, matchesSourceUrlFamily, setState, sleepWithStop, STOP_ERROR_MESSAGE, throwIfStopped, }); function getErrorMessage(error) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.getErrorMessage) { return loggingStatus.getErrorMessage(error); } return String(typeof error === 'string' ? error : error?.message || ''); } function isCloudflareSecurityBlockedError(error) { return getErrorMessage(error).startsWith(CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX); } function isTerminalSecurityBlockedError(error) { return isCloudflareSecurityBlockedError(error); } function getCloudflareSecurityBlockedMessage(error) { const message = getErrorMessage(error); if (message.startsWith(CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX)) { return message.slice(CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX.length).trim() || CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE; } return CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE; } function getTerminalSecurityBlockedMessage(error) { return getCloudflareSecurityBlockedMessage(error); } function getTerminalSecurityBlockedAlertText(error) { return '检测到 Cloudflare 风控,请暂停当前操作。'; } function getTerminalSecurityBlockedTitle(error) { return 'Cloudflare 风控拦截'; } function isBrowserSwitchRequiredError(error) { return getErrorMessage(error).startsWith(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX); } function getBrowserSwitchRequiredMessage(error) { const message = getErrorMessage(error); return message.startsWith(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX) ? message.slice(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX.length).trim() : message; } function broadcastSecurityBlockedAlert(title = '流程已完全停止', message = CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE, alertText = '检测到 Cloudflare 风控,请暂停当前操作。') { chrome.runtime.sendMessage({ type: 'SECURITY_BLOCKED_ALERT', payload: { title, message, alert: { text: alertText, tone: 'danger', }, }, }).catch(() => { }); } async function handleCloudflareSecurityBlocked(error) { const title = getTerminalSecurityBlockedTitle(error); const message = getTerminalSecurityBlockedMessage(error); const alertText = getTerminalSecurityBlockedAlertText(error); await requestStop({ logMessage: message }); broadcastSecurityBlockedAlert(title, message, alertText); return message; } async function handleBrowserSwitchRequired(error) { const message = getBrowserSwitchRequiredMessage(error) || '检测到第 10 步的特殊冲突状态,请更换浏览器后重新进行注册登录。'; await requestStop({ logMessage: message }); return message; } function isVerificationMailPollingError(error) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.isVerificationMailPollingError) { return loggingStatus.isVerificationMailPollingError(error); } const message = getErrorMessage(error); return /未在 .*邮箱中找到新的匹配邮件|未在 Hotmail 收件箱中找到新的匹配验证码|邮箱轮询结束,但未获取到验证码|无法获取新的(?:注册|登录)验证码|页面未能重新就绪|页面通信异常|did not respond in \d+s/i.test(message); } function isAddPhoneAuthFailure(error) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.isAddPhoneAuthFailure) { return loggingStatus.isAddPhoneAuthFailure(error); } const message = getErrorMessage(error); if (/\u624b\u673a\u53f7\u8f93\u5165\u6a21\u5f0f|phone\s+entry/i.test(message)) { return false; } return /https:\/\/auth\.openai\.com\/add-phone(?:[/?#]|$)|\badd-phone\b|\u6dfb\u52a0\u624b\u673a\u53f7|\u624b\u673a\u53f7\u7801|\u8fdb\u5165\u624b\u673a\u53f7\u9875\u9762|\u624b\u673a\u53f7\u9875|\u624b\u673a\u53f7\u9875\u9762|phone\s+number|telephone/i.test(message); } function getLoginAuthStateLabel(state) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.getLoginAuthStateLabel) { return loggingStatus.getLoginAuthStateLabel(state); } switch (state) { case 'verification_page': return '登录验证码页'; case 'password_page': return '密码页'; case 'email_page': return '邮箱输入页'; case 'login_timeout_error_page': return '登录超时报错页'; case 'oauth_consent_page': return 'OAuth 授权页'; case 'add_phone_page': return '手机号页'; default: return '未知页面'; } } function isRestartCurrentAttemptError(error) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.isRestartCurrentAttemptError) { return loggingStatus.isRestartCurrentAttemptError(error); } const message = String(typeof error === 'string' ? error : error?.message || ''); return /当前邮箱已存在,需要重新开始新一轮/.test(message); } function isSignupUserAlreadyExistsFailure(error) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.isSignupUserAlreadyExistsFailure) { return loggingStatus.isSignupUserAlreadyExistsFailure(error); } const message = getErrorMessage(error); return /SIGNUP_USER_ALREADY_EXISTS::|user_already_exists/i.test(message); } function isStep4Route405RecoveryLimitFailure(error) { const message = getErrorMessage(error); return /STEP4_405_RECOVERY_LIMIT::|步骤\s*4:检测到\s*405\s*错误页面,已连续点击“重试”恢复/i.test(message); } function isPhoneSmsPlatformRateLimitFailure(error) { const message = getErrorMessage(error); return /FIVE_SIM_RATE_LIMIT::|5sim[\s\S]*(?:限流|rate\s*limit)/i.test(message); } function isPlusCheckoutNonFreeTrialFailure(error) { const message = getErrorMessage(error); return /PLUS_CHECKOUT_NON_FREE_TRIAL::|今日应付金额不是\s*0|没有免费试用资格/i.test(message); } function isGoPayCheckoutRestartRequiredFailure(error) { const message = getErrorMessage(error); return /GOPAY_RESTART_FROM_STEP6::|GOPAY_RETRY_REQUIRED::/i.test(message); } function isStep9RecoverableAuthError(error) { const message = String(typeof error === 'string' ? error : error?.message || ''); return /STEP9_OAUTH_RETRY::/i.test(message) || isRecoverableStep9AuthFailure(message); } function isLegacyStep9RecoverableAuthError(error) { const message = String(typeof error === 'string' ? error : error?.message || ''); return /STEP9_OAUTH_TIMEOUT::|认证失败:\s*(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded)/i.test(message); } function isStepDoneStatus(status) { return status === 'completed' || status === 'manual_completed' || status === 'skipped'; } function getFirstUnfinishedStep(statuses = {}, stateOverride = null) { const state = stateOverride || {}; const activeStepIds = typeof getStepIdsForState === 'function' ? getStepIdsForState(state) : (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length ? STEP_IDS : Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1)); for (const step of activeStepIds) { if (!isStepDoneStatus(statuses[step] || 'pending')) return step; } return null; } function hasSavedProgress(statuses = {}, stateOverride = null) { const state = stateOverride || {}; const merged = { ...DEFAULT_STATE.stepStatuses, ...statuses }; const activeStepIds = typeof getStepIdsForState === 'function' ? getStepIdsForState(state) : (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length ? STEP_IDS : Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1)); return activeStepIds.some((step) => (merged[step] || 'pending') !== 'pending'); } function getDownstreamStateResets(step, state = {}) { const stepKey = getStepExecutionKeyForState(step, state); const plusRuntimeResets = { plusCheckoutTabId: null, plusCheckoutUrl: null, plusCheckoutCountry: 'DE', plusCheckoutCurrency: 'EUR', plusBillingCountryText: '', plusBillingAddress: null, plusPaypalApprovedAt: null, plusGoPayApprovedAt: null, plusReturnUrl: '', plusManualConfirmationPending: false, plusManualConfirmationRequestId: '', plusManualConfirmationStep: 0, plusManualConfirmationMethod: '', plusManualConfirmationTitle: '', plusManualConfirmationMessage: '', }; if (step <= 1) { return { ...plusRuntimeResets, oauthUrl: null, cpaOAuthState: null, cpaManagementOrigin: null, sub2apiSessionId: null, sub2apiOAuthState: null, sub2apiGroupId: null, sub2apiDraftName: null, codex2apiSessionId: null, codex2apiOAuthState: null, flowStartTime: null, password: null, lastEmailTimestamp: null, signupVerificationRequestedAt: null, loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, pendingPhoneActivationConfirmation: null, lastSignupCode: null, lastLoginCode: null, localhostUrl: null, currentPhoneVerificationCode: '', currentPhoneVerificationCountdownEndsAt: 0, currentPhoneVerificationCountdownWindowIndex: 0, currentPhoneVerificationCountdownWindowTotal: 0, }; } if (step === 2) { return { ...plusRuntimeResets, password: null, lastEmailTimestamp: null, signupVerificationRequestedAt: null, loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, pendingPhoneActivationConfirmation: null, lastSignupCode: null, lastLoginCode: null, localhostUrl: null, currentPhoneVerificationCode: '', currentPhoneVerificationCountdownEndsAt: 0, currentPhoneVerificationCountdownWindowIndex: 0, currentPhoneVerificationCountdownWindowTotal: 0, }; } if (step === 3 || step === 4) { return { ...plusRuntimeResets, lastEmailTimestamp: null, signupVerificationRequestedAt: null, loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, pendingPhoneActivationConfirmation: null, lastSignupCode: null, lastLoginCode: null, localhostUrl: null, currentPhoneVerificationCode: '', currentPhoneVerificationCountdownEndsAt: 0, currentPhoneVerificationCountdownWindowIndex: 0, currentPhoneVerificationCountdownWindowTotal: 0, }; } if (step === 5 || step === 6 || step === 7 || step === 8) { return { ...(step <= 6 ? plusRuntimeResets : {}), ...(step === 7 ? { plusBillingCountryText: '', plusBillingAddress: null, plusPaypalApprovedAt: null, plusGoPayApprovedAt: null, plusReturnUrl: '', plusManualConfirmationPending: false, plusManualConfirmationRequestId: '', plusManualConfirmationStep: 0, plusManualConfirmationMethod: '', plusManualConfirmationTitle: '', plusManualConfirmationMessage: '', } : {}), ...(step === 8 ? { plusPaypalApprovedAt: null, plusGoPayApprovedAt: null, plusReturnUrl: '', } : {}), lastLoginCode: null, loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, pendingPhoneActivationConfirmation: null, localhostUrl: null, currentPhoneVerificationCode: '', currentPhoneVerificationCountdownEndsAt: 0, currentPhoneVerificationCountdownWindowIndex: 0, currentPhoneVerificationCountdownWindowTotal: 0, }; } if (step === 9) { return { pendingPhoneActivationConfirmation: null, plusReturnUrl: '', localhostUrl: null, currentPhoneVerificationCode: '', currentPhoneVerificationCountdownEndsAt: 0, currentPhoneVerificationCountdownWindowIndex: 0, currentPhoneVerificationCountdownWindowTotal: 0, }; } if (stepKey === 'oauth-login' || stepKey === 'fetch-login-code') { return { lastLoginCode: null, loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, pendingPhoneActivationConfirmation: null, localhostUrl: null, currentPhoneVerificationCode: '', currentPhoneVerificationCountdownEndsAt: 0, currentPhoneVerificationCountdownWindowIndex: 0, currentPhoneVerificationCountdownWindowTotal: 0, }; } if (stepKey === 'confirm-oauth') { return { pendingPhoneActivationConfirmation: null, localhostUrl: null, }; } return {}; } async function invalidateDownstreamAfterStepRestart(step, options = {}) { const { logLabel = `步骤 ${step} 重新执行` } = options; const state = await getState(); const statuses = { ...(state.stepStatuses || {}) }; const changedSteps = []; const activeStepIds = typeof getStepIdsForState === 'function' ? getStepIdsForState(state) : (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length ? STEP_IDS : Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1)); for (const downstream of activeStepIds) { if (downstream <= step) { continue; } if (statuses[downstream] !== 'pending') { statuses[downstream] = 'pending'; changedSteps.push(downstream); } } if (changedSteps.length) { await setState({ stepStatuses: statuses }); for (const downstream of changedSteps) { chrome.runtime.sendMessage({ type: 'STEP_STATUS_CHANGED', payload: { step: downstream, status: 'pending' }, }).catch(() => { }); } await addLog(`${logLabel},已重置后续步骤状态:${changedSteps.join(', ')}`, 'warn'); } const resets = getDownstreamStateResets(step, state); if (Object.keys(resets).length) { await setState(resets); broadcastDataUpdate(resets); } } function clearStopRequest() { stopRequested = false; } function getRunningSteps(statuses = {}, stateOverride = null) { const state = stateOverride || {}; const merged = { ...DEFAULT_STATE.stepStatuses, ...statuses }; const activeStepIds = typeof getStepIdsForState === 'function' ? getStepIdsForState(state) : (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length ? STEP_IDS : Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1)); return activeStepIds .filter((step) => merged[step] === 'running') .sort((a, b) => a - b); } function inferStoppedRecordStep(state = {}) { const statuses = { ...DEFAULT_STATE.stepStatuses, ...(state?.stepStatuses || {}) }; const stepIds = typeof getStepIdsForState === 'function' ? getStepIdsForState(state) : (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length ? STEP_IDS : Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1)); const runningSteps = stepIds.filter((step) => statuses[step] === 'running'); if (runningSteps.length) { return runningSteps[0]; } const hasProgress = stepIds.some((step) => statuses[step] !== 'pending'); if (!hasProgress) { return null; } for (const step of stepIds) { const status = statuses[step] || 'pending'; if (!(status === 'completed' || status === 'manual_completed' || status === 'skipped')) { return step; } } return null; } function resolveAccountRunRecordStatusForStop(status, state = {}) { const normalizedStatus = String(status || '').trim().toLowerCase(); if (normalizedStatus === 'stopped') { const inferredStep = inferStoppedRecordStep(state); if (Number.isInteger(inferredStep) && inferredStep > 0) { return `step${inferredStep}_stopped`; } } return status; } function extractStoppedStepFromRecordStatus(status = '') { const match = String(status || '').trim().toLowerCase().match(/^step(\d+)_stopped$/); if (!match) { return null; } const step = Number(match[1]); return Number.isInteger(step) && step > 0 ? step : null; } function resolveAccountRunRecordReasonForStop(status, reason = '') { const text = String(reason || '').trim(); const stoppedStep = extractStoppedStepFromRecordStatus(status); if (!stoppedStep) { if (!text || text === STOP_ERROR_MESSAGE || /^流程已被用户停止。?$/.test(text)) { return '流程已停止。'; } return text; } if (!text || text === STOP_ERROR_MESSAGE || /^流程已被用户停止。?$/.test(text)) { return `步骤 ${stoppedStep} 已被用户停止。`; } if (/流程尚未完成/.test(text) || /已使用邮箱/.test(text)) { return `步骤 ${stoppedStep} 已停止:邮箱已设置,流程尚未完成。`; } if (/步骤\s*\d+\s*已(?:被用户)?停止/.test(text)) { return text.replace(/步骤\s*\d+/, `步骤 ${stoppedStep}`); } return text; } function getAutoRunStatusPayload(phase, payload = {}) { const normalizedPayload = { ...payload, currentRun: payload.currentRun ?? autoRunCurrentRun, totalRuns: payload.totalRuns ?? autoRunTotalRuns, attemptRun: payload.attemptRun ?? autoRunAttemptRun, sessionId: payload.sessionId ?? payload.autoRunSessionId ?? autoRunSessionId, }; if (typeof loggingStatus !== 'undefined' && loggingStatus?.getAutoRunStatusPayload) { return loggingStatus.getAutoRunStatusPayload(phase, normalizedPayload); } return { autoRunning: phase === 'scheduled' || phase === 'running' || phase === 'waiting_step' || phase === 'waiting_email' || phase === 'retrying' || phase === 'waiting_interval', autoRunPhase: phase, autoRunCurrentRun: normalizedPayload.currentRun ?? 0, autoRunTotalRuns: normalizedPayload.totalRuns ?? 1, autoRunAttemptRun: normalizedPayload.attemptRun ?? 0, autoRunSessionId: normalizeAutoRunSessionId(normalizedPayload.sessionId), scheduledAutoRunAt: Number.isFinite(Number(normalizedPayload.scheduledAt)) ? Number(normalizedPayload.scheduledAt) : null, autoRunCountdownAt: Number.isFinite(Number(normalizedPayload.countdownAt)) ? Number(normalizedPayload.countdownAt) : null, autoRunCountdownTitle: normalizedPayload.countdownTitle === undefined ? '' : String(normalizedPayload.countdownTitle || ''), autoRunCountdownNote: normalizedPayload.countdownNote === undefined ? '' : String(normalizedPayload.countdownNote || ''), }; } async function broadcastAutoRunStatus(phase, payload = {}, extraState = {}) { const rawScheduledAt = phase === 'scheduled' ? (payload.scheduledAt ?? payload.scheduledAutoRunAt ?? null) : null; const rawCountdownAt = payload.countdownAt ?? payload.autoRunCountdownAt ?? null; const statusPayload = { phase, currentRun: payload.currentRun ?? autoRunCurrentRun, totalRuns: payload.totalRuns ?? autoRunTotalRuns, attemptRun: payload.attemptRun ?? autoRunAttemptRun, sessionId: payload.sessionId ?? payload.autoRunSessionId ?? autoRunSessionId, scheduledAt: rawScheduledAt === null ? null : Number(rawScheduledAt), countdownAt: rawCountdownAt === null ? null : Number(rawCountdownAt), countdownTitle: payload.countdownTitle === undefined ? '' : String(payload.countdownTitle || ''), countdownNote: payload.countdownNote === undefined ? '' : String(payload.countdownNote || ''), }; await setState({ ...extraState, ...getAutoRunStatusPayload(phase, statusPayload), }); chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: statusPayload, }).catch(() => { }); } function isAutoRunLockedState(state) { return Boolean(state.autoRunning) && ( state.autoRunPhase === 'running' || state.autoRunPhase === 'waiting_step' || state.autoRunPhase === 'retrying' || state.autoRunPhase === 'waiting_interval' ); } function isAutoRunPausedState(state) { return Boolean(state.autoRunning) && state.autoRunPhase === 'waiting_email'; } function isAutoRunScheduledState(state) { const plan = normalizeAutoRunTimerPlanFromState(state); const scheduledAt = state.scheduledAutoRunAt === null ? null : Number(state.scheduledAutoRunAt); return Boolean(state.autoRunning) && state.autoRunPhase === 'scheduled' && Number.isFinite(scheduledAt) && plan?.kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START; } function getPendingAutoRunTimerPlan(state = {}) { return normalizeAutoRunTimerPlanFromState(state); } function formatAutoRunScheduleTime(timestamp) { return new Date(timestamp).toLocaleString('zh-CN', { hour12: false, timeZone: DISPLAY_TIMEZONE, month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', }); } async function setAutoRunDelayEnabledState(enabled) { const normalized = Boolean(enabled); await setPersistentSettings({ autoRunDelayEnabled: normalized }); await setState({ autoRunDelayEnabled: normalized }); broadcastDataUpdate({ autoRunDelayEnabled: normalized }); } async function ensureAutoRunTimerAlarm(fireAt) { if (!Number.isFinite(fireAt) || fireAt <= Date.now()) { return false; } const existingAlarm = await chrome.alarms.get(AUTO_RUN_TIMER_ALARM_NAME); if (!existingAlarm || Math.abs((existingAlarm.scheduledTime || 0) - fireAt) > 1000) { await chrome.alarms.clear(AUTO_RUN_TIMER_ALARM_NAME); await chrome.alarms.create(AUTO_RUN_TIMER_ALARM_NAME, { when: fireAt }); } return true; } async function clearAutoRunTimerAlarm() { await chrome.alarms.clear(AUTO_RUN_TIMER_ALARM_NAME); } async function persistAutoRunTimerPlan(plan, extraState = {}) { const normalizedPlan = normalizeAutoRunTimerPlan(plan); if (!normalizedPlan) { throw new Error('自动运行计时计划无效。'); } const statusPayload = getAutoRunTimerStatusPayload(normalizedPlan); await broadcastAutoRunStatus( statusPayload.phase, statusPayload, { ...extraState, autoRunTimerPlan: normalizedPlan, scheduledAutoRunPlan: null, } ); await ensureAutoRunTimerAlarm(normalizedPlan.fireAt); return normalizedPlan; } function getAutoRunTimerResumeOptions(plan) { const normalizedPlan = normalizeAutoRunTimerPlan(plan); if (!normalizedPlan) { return null; } if (normalizedPlan.kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START) { return { loopOptions: { autoRunSessionId: normalizedPlan.autoRunSessionId, autoRunSkipFailures: normalizedPlan.autoRunSkipFailures, mode: normalizedPlan.mode, }, statusPayload: { currentRun: 0, totalRuns: normalizedPlan.totalRuns, attemptRun: 0, sessionId: normalizedPlan.autoRunSessionId, }, }; } if (normalizedPlan.kind === AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS) { const nextRun = Math.min(normalizedPlan.currentRun + 1, normalizedPlan.totalRuns); return { loopOptions: { autoRunSessionId: normalizedPlan.autoRunSessionId, autoRunSkipFailures: normalizedPlan.autoRunSkipFailures, mode: 'restart', resumeCurrentRun: nextRun, resumeAttemptRun: 1, resumeRoundSummaries: normalizedPlan.roundSummaries, }, statusPayload: { currentRun: nextRun, totalRuns: normalizedPlan.totalRuns, attemptRun: 1, sessionId: normalizedPlan.autoRunSessionId, }, }; } return { loopOptions: { autoRunSessionId: normalizedPlan.autoRunSessionId, autoRunSkipFailures: normalizedPlan.autoRunSkipFailures, mode: 'restart', resumeCurrentRun: normalizedPlan.currentRun, resumeAttemptRun: normalizedPlan.attemptRun, resumeRoundSummaries: normalizedPlan.roundSummaries, }, statusPayload: { currentRun: normalizedPlan.currentRun, totalRuns: normalizedPlan.totalRuns, attemptRun: normalizedPlan.attemptRun, sessionId: normalizedPlan.autoRunSessionId, }, }; } let autoRunTimerLaunching = false; async function launchAutoRunTimerPlan(trigger = 'alarm', options = {}) { const { expectedKinds = [] } = options; if (autoRunTimerLaunching) { return false; } autoRunTimerLaunching = true; try { const state = await getState(); const plan = getPendingAutoRunTimerPlan(state); if (!plan) { return false; } if (expectedKinds.length && !expectedKinds.includes(plan.kind)) { return false; } if (autoRunActive) { return false; } if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) { return false; } const resumeOptions = getAutoRunTimerResumeOptions(plan); if (!resumeOptions) { await clearAutoRunTimerAlarm(); await broadcastAutoRunStatus('idle', { currentRun: 0, totalRuns: 1, attemptRun: 0, }, { autoRunRoundSummaries: [], autoRunTimerPlan: null, scheduledAutoRunPlan: null, }); return false; } await clearAutoRunTimerAlarm(); if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) { return false; } autoRunCurrentRun = resumeOptions.statusPayload.currentRun; autoRunTotalRuns = plan.totalRuns; autoRunAttemptRun = resumeOptions.statusPayload.attemptRun; autoRunSessionId = normalizeAutoRunSessionId(plan.autoRunSessionId); if (plan.kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START && trigger !== 'manual' && state.autoRunDelayEnabled) { await setAutoRunDelayEnabledState(false); } await broadcastAutoRunStatus( 'running', resumeOptions.statusPayload, { autoRunSkipFailures: plan.autoRunSkipFailures, autoRunRoundSummaries: serializeAutoRunRoundSummaries(plan.totalRuns, plan.roundSummaries), autoRunTimerPlan: null, scheduledAutoRunPlan: null, } ); if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) { return false; } clearStopRequest(); let logMessage = '倒计时结束,自动运行开始执行。'; if (plan.kind === AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS) { logMessage = trigger === 'manual' ? '已手动跳过线程间隔,自动流程立即开始下一轮。' : '线程间隔结束,自动流程开始下一轮。'; } else if (plan.kind === AUTO_RUN_TIMER_KIND_BEFORE_RETRY) { logMessage = trigger === 'manual' ? `已手动跳过线程间隔,立即开始第 ${plan.currentRun}/${plan.totalRuns} 轮第 ${plan.attemptRun} 次尝试。` : `线程间隔结束,开始第 ${plan.currentRun}/${plan.totalRuns} 轮第 ${plan.attemptRun} 次尝试。`; } else if (trigger === 'manual') { logMessage = '已手动跳过倒计时,自动运行立即开始。'; } await addLog(logMessage, 'info'); if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) { return false; } startAutoRunLoop(plan.totalRuns, resumeOptions.loopOptions); return true; } finally { autoRunTimerLaunching = false; } } async function scheduleAutoRun(totalRuns, options = {}) { const state = await getState(); if (isAutoRunLockedState(state) || isAutoRunPausedState(state) || autoRunActive) { throw new Error('自动运行已在进行中,请先停止后再重新计划。'); } if (getPendingAutoRunTimerPlan(state)) { throw new Error('已有自动运行倒计时计划,请先取消或立即开始。'); } const delayMinutes = normalizeAutoRunDelayMinutes(options.delayMinutes); const sessionId = createAutoRunSessionId(); const timerPlan = normalizeAutoRunTimerPlan({ kind: AUTO_RUN_TIMER_KIND_SCHEDULED_START, fireAt: Date.now() + delayMinutes * 60 * 1000, totalRuns, autoRunSkipFailures: options.autoRunSkipFailures, autoRunSessionId: sessionId, mode: options.mode, }); autoRunCurrentRun = 0; autoRunTotalRuns = timerPlan.totalRuns; autoRunAttemptRun = 0; autoRunSessionId = sessionId; await persistAutoRunTimerPlan(timerPlan, { autoRunSkipFailures: timerPlan.autoRunSkipFailures, autoRunRoundSummaries: serializeAutoRunRoundSummaries(timerPlan.totalRuns, []), }); await addLog( `自动运行已计划:${delayMinutes} 分钟后启动(${formatAutoRunScheduleTime(timerPlan.fireAt)}),目标 ${timerPlan.totalRuns} 轮。`, 'info' ); return { ok: true, scheduledAt: timerPlan.fireAt }; } async function cancelScheduledAutoRun(options = {}) { const state = await getState(); const plan = getPendingAutoRunTimerPlan(state); if (!plan || plan.kind !== AUTO_RUN_TIMER_KIND_SCHEDULED_START) { return false; } autoRunCurrentRun = 0; autoRunTotalRuns = plan.totalRuns; autoRunAttemptRun = 0; clearCurrentAutoRunSessionId(plan.autoRunSessionId); await broadcastAutoRunStatus( 'idle', { currentRun: 0, totalRuns: plan.totalRuns, attemptRun: 0, sessionId: 0, }, { autoRunSessionId: 0, autoRunRoundSummaries: [], autoRunTimerPlan: null, scheduledAutoRunPlan: null, } ); await clearAutoRunTimerAlarm(); if (options.logMessage !== false) { await addLog(options.logMessage || '已取消自动运行倒计时计划。', 'warn'); } return true; } async function restoreAutoRunTimerIfNeeded() { const state = await getState(); let plan = getPendingAutoRunTimerPlan(state); if (!plan) { clearCurrentAutoRunSessionId(); if (state.autoRunPhase === 'scheduled' || state.autoRunPhase === 'waiting_interval') { await clearAutoRunTimerAlarm(); await broadcastAutoRunStatus('idle', { currentRun: 0, totalRuns: 1, attemptRun: 0, sessionId: 0, }, { autoRunSessionId: 0, autoRunRoundSummaries: [], autoRunTimerPlan: null, scheduledAutoRunPlan: null, }); } return; } if (!plan.autoRunSessionId) { const restoredSessionId = createAutoRunSessionId(); plan = await persistAutoRunTimerPlan({ ...plan, autoRunSessionId: restoredSessionId, }, { autoRunSkipFailures: plan.autoRunSkipFailures, autoRunRoundSummaries: serializeAutoRunRoundSummaries(plan.totalRuns, plan.roundSummaries), }); } else { setCurrentAutoRunSessionId(plan.autoRunSessionId); } if (plan.fireAt <= Date.now()) { await launchAutoRunTimerPlan('restore'); return; } const statusPayload = getAutoRunTimerStatusPayload(plan); await broadcastAutoRunStatus( statusPayload.phase, statusPayload, { autoRunSessionId: plan.autoRunSessionId, autoRunSkipFailures: plan.autoRunSkipFailures, autoRunRoundSummaries: serializeAutoRunRoundSummaries(plan.totalRuns, plan.roundSummaries), autoRunTimerPlan: plan, scheduledAutoRunPlan: null, } ); await ensureAutoRunTimerAlarm(plan.fireAt); } async function ensureManualInteractionAllowed(actionLabel) { const state = await getState(); if (isAutoRunLockedState(state)) { throw new Error(`自动流程运行中,请先停止后再${actionLabel}。`); } if (isAutoRunPausedState(state)) { throw new Error(`自动流程当前已暂停。请点击“继续”,或先确认接管自动流程后再${actionLabel}。`); } if (isAutoRunScheduledState(state)) { throw new Error(`自动流程已计划启动。请先取消计划,或立即开始后再${actionLabel}。`); } return state; } async function skipStep(step) { const state = await ensureManualInteractionAllowed('跳过步骤'); const activeStepIds = typeof getStepIdsForState === 'function' ? getStepIdsForState(state) : (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length ? STEP_IDS : Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1)); if (!Number.isInteger(step) || !activeStepIds.includes(step)) { throw new Error(`无效步骤:${step}`); } const statuses = { ...(state.stepStatuses || {}) }; const currentStatus = statuses[step]; if (currentStatus === 'running') { throw new Error(`步骤 ${step} 正在运行中,不能跳过。`); } if (isStepDoneStatus(currentStatus)) { throw new Error(`步骤 ${step} 已完成,无需再跳过。`); } const currentIndex = activeStepIds.indexOf(step); if (currentIndex > 0) { const prevStep = activeStepIds[currentIndex - 1]; const prevStatus = statuses[prevStep]; if (!isStepDoneStatus(prevStatus)) { throw new Error(`请先完成步骤 ${prevStep},再跳过步骤 ${step}。`); } } await setStepStatus(step, 'skipped'); await addLog(`步骤 ${step} 已跳过`, 'warn'); if (step === 1) { const latestState = await getState(); const skippedSteps = []; for (let linkedStep = 2; linkedStep <= 5; linkedStep += 1) { const linkedStatus = latestState.stepStatuses?.[linkedStep]; if (!isStepDoneStatus(linkedStatus) && linkedStatus !== 'running') { await setStepStatus(linkedStep, 'skipped'); skippedSteps.push(linkedStep); } } if (skippedSteps.length) { await addLog(`步骤 1 已跳过,步骤 ${skippedSteps.join('、')} 也已同时跳过。`, 'warn'); } } return { ok: true, step, status: 'skipped' }; } function throwIfStopped(error = null) { const errorMessage = typeof error === 'string' ? error : error?.message; if (errorMessage === STOP_ERROR_MESSAGE) { throw error instanceof Error ? error : new Error(STOP_ERROR_MESSAGE); } if (stopRequested) { throw new Error(STOP_ERROR_MESSAGE); } } async function sleepWithStop(ms) { const start = Date.now(); while (Date.now() - start < ms) { throwIfStopped(); await new Promise(r => setTimeout(r, Math.min(100, ms - (Date.now() - start)))); } } async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY_MAX) { const duration = Math.floor(Math.random() * (max - min + 1)) + min; await sleepWithStop(duration); } async function clickWithDebugger(tabId, rect) { throwIfStopped(); if (!tabId) { throw new Error('未找到用于调试点击的认证页面标签页。'); } if (!rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) { throw new Error('步骤 9 的调试器兜底点击需要有效的按钮坐标。'); } const target = { tabId }; try { await chrome.debugger.attach(target, '1.3'); } catch (err) { throw new Error( `步骤 9 的调试器兜底点击附加失败:${err.message}。` + '如果认证页标签已打开 DevTools,请先关闭后重试。' ); } try { throwIfStopped(); const x = Math.round(rect.centerX); const y = Math.round(rect.centerY); await chrome.debugger.sendCommand(target, 'Page.bringToFront'); throwIfStopped(); await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x, y, button: 'none', buttons: 0, clickCount: 0, }); throwIfStopped(); await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', buttons: 1, clickCount: 1, }); throwIfStopped(); await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', buttons: 0, clickCount: 1, }); } finally { await chrome.debugger.detach(target).catch(() => { }); } } async function broadcastStopToContentScripts() { const registry = await getTabRegistry(); for (const entry of Object.values(registry)) { if (!entry?.tabId) continue; try { await chrome.tabs.sendMessage(entry.tabId, { type: 'STOP_FLOW', source: 'background', payload: {}, }); } catch { } } } let stopRequested = false; // ============================================================ // Message Handler (central router) // ============================================================ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { console.log(LOG_PREFIX, `Received: ${message.type} from ${message.source || 'sidepanel'}`, message); handleMessage(message, sender).then(response => { sendResponse(response); }).catch(err => { console.error(LOG_PREFIX, 'Handler error:', err); sendResponse({ error: err.message }); }); return true; // async response }); async function handleMessage(message, sender) { return messageRouter.handleMessage(message, sender); } // ============================================================ // Step Data Handlers // ============================================================ async function handleStepData(step, payload) { if (typeof messageRouter !== 'undefined' && messageRouter?.handleStepData) { return messageRouter.handleStepData(step, payload); } switch (step) { case 1: { const updates = {}; if (payload.oauthUrl) { updates.oauthUrl = payload.oauthUrl; broadcastDataUpdate({ oauthUrl: payload.oauthUrl }); } if (payload.sub2apiSessionId !== undefined) updates.sub2apiSessionId = payload.sub2apiSessionId || null; if (payload.sub2apiOAuthState !== undefined) updates.sub2apiOAuthState = payload.sub2apiOAuthState || null; if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null; if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null; if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null; if (payload.cpaOAuthState !== undefined) updates.cpaOAuthState = payload.cpaOAuthState || null; if (payload.cpaManagementOrigin !== undefined) updates.cpaManagementOrigin = payload.cpaManagementOrigin || null; if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null; if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null; if (payload.sub2apiGroupIds !== undefined) updates.sub2apiGroupIds = Array.isArray(payload.sub2apiGroupIds) ? payload.sub2apiGroupIds : []; if (Object.keys(updates).length) { await setState(updates); } break; } case 2: if (payload.email) await setEmailState(payload.email); if (payload.skippedPasswordStep) { const latestState = await getState(); const step3Status = latestState.stepStatuses?.[3]; if (step3Status !== 'running' && step3Status !== 'completed' && step3Status !== 'manual_completed') { await setStepStatus(3, 'skipped'); await addLog('步骤 2:提交邮箱后页面直接进入邮箱验证码页,已自动跳过步骤 3。', 'warn'); } } break; case 3: if (payload.email) await setEmailState(payload.email); if (payload.signupVerificationRequestedAt) { await setState({ signupVerificationRequestedAt: payload.signupVerificationRequestedAt }); } if (payload.loginVerificationRequestedAt) { await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt }); } break; case 7: if (payload.loginVerificationRequestedAt) { await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt }); } break; case 4: await setState({ lastEmailTimestamp: payload.emailTimestamp || null, signupVerificationRequestedAt: null, }); break; case 8: await setState({ lastEmailTimestamp: payload.emailTimestamp || null, loginVerificationRequestedAt: null, }); break; case 9: if (payload.localhostUrl) { if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) { throw new Error('步骤 9 返回了无效的 localhost OAuth 回调地址。'); } await setState({ localhostUrl: payload.localhostUrl, oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, }); broadcastDataUpdate({ localhostUrl: payload.localhostUrl }); } break; case 10: case 13: { if (payload.localhostUrl) { await closeLocalhostCallbackTabs(payload.localhostUrl); } const latestState = await getState(); const lastStepId = typeof getLastStepIdForState === 'function' ? getLastStepIdForState(latestState) : 10; if (Number(step) !== Number(lastStepId)) { break; } if (latestState.currentHotmailAccountId && isHotmailProvider(latestState)) { await patchHotmailAccount(latestState.currentHotmailAccountId, { used: true, lastUsedAt: Date.now(), }); await addLog('当前 Hotmail 账号已自动标记为已用。', 'ok'); } if (isLuckmailProvider(latestState)) { const currentPurchase = getCurrentLuckmailPurchase(latestState); if (currentPurchase?.id) { await setLuckmailPurchaseUsedState(currentPurchase.id, true); await addLog(`当前 LuckMail 邮箱 ${currentPurchase.email_address} 已在本地标记为已用。`, 'ok'); } await clearLuckmailRuntimeState({ clearEmail: true }); await addLog('当前 LuckMail 邮箱运行态已清空,下轮将优先复用未用邮箱或重新购买邮箱。', 'ok'); } const localhostPrefix = buildLocalhostCleanupPrefix(payload.localhostUrl); if (localhostPrefix) { await closeTabsByUrlPrefix(localhostPrefix, { excludeUrls: [payload.localhostUrl], excludeLocalhostCallbacks: true, }); } await finalizeIcloudAliasAfterSuccessfulFlow(latestState); if (typeof markCurrentCustomEmailPoolEntryUsed === 'function') { await markCurrentCustomEmailPoolEntryUsed(latestState); } const shouldClearCustomPoolEmail = String(latestState?.emailGenerator || '').trim().toLowerCase() === ( typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string' ? CUSTOM_EMAIL_POOL_GENERATOR : 'custom-pool' ); if ((shouldUseCustomRegistrationEmail(latestState) || shouldClearCustomPoolEmail) && latestState.email) { await setEmailStateSilently(null); } await finalizePhoneActivationAfterSuccessfulFlow(latestState); break; } } } // ============================================================ // Step Completion Waiting // ============================================================ // Map of step -> { resolve, reject } for waiting on step completion const stepWaiters = new Map(); let resumeWaiter = null; const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000; const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 7, 8, 9]); const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 10, 12]); const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([ 'open-chatgpt', 'submit-signup-email', 'fetch-signup-code', 'clear-login-cookies', 'plus-checkout-create', 'plus-checkout-billing', 'paypal-approve', 'plus-checkout-return', 'oauth-login', 'fetch-login-code', 'confirm-oauth', ]); const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([ 'fill-password', 'fill-profile', 'gopay-subscription-confirm', 'platform-verify', ]); const STEP_COMPLETION_SIGNAL_TIMEOUTS_BY_STEP_KEY = new Map([ ['gopay-subscription-confirm', 1800000], ]); const AUTO_RUN_PRE_EXECUTION_DELAYS_BY_STEP_KEY = new Map([ ['plus-checkout-create', 20000], ]); function waitForStepComplete(step, timeoutMs = 120000) { throwIfStopped(); const normalizedStep = Number(step); const existingWaiter = stepWaiters.get(normalizedStep); if (existingWaiter?.promise) { console.log(LOG_PREFIX, `[waitForStepComplete] reuse existing waiter for step ${normalizedStep}`); return existingWaiter.promise; } console.log(LOG_PREFIX, `[waitForStepComplete] register step ${normalizedStep}, timeout=${timeoutMs}ms`); const waiter = { promise: null, resolve: null, reject: null, }; waiter.promise = new Promise((resolve, reject) => { const timer = setTimeout(() => { if (stepWaiters.get(normalizedStep) === waiter) { stepWaiters.delete(normalizedStep); } console.warn(LOG_PREFIX, `[waitForStepComplete] timeout for step ${normalizedStep} after ${timeoutMs}ms`); reject(new Error(`步骤 ${normalizedStep} 等待超时(>${timeoutMs / 1000} 秒)`)); }, timeoutMs); waiter.resolve = (data) => { clearTimeout(timer); if (stepWaiters.get(normalizedStep) === waiter) { stepWaiters.delete(normalizedStep); } resolve(data); }; waiter.reject = (err) => { clearTimeout(timer); if (stepWaiters.get(normalizedStep) === waiter) { stepWaiters.delete(normalizedStep); } reject(err); }; }); stepWaiters.set(normalizedStep, waiter); return waiter.promise; } function getStepExecutionKeyForState(step, state = {}) { if (typeof getStepDefinitionForState !== 'function') { return ''; } return String(getStepDefinitionForState(step, state)?.key || '').trim(); } function doesStepUseBackgroundCompletion(step, state = {}) { const stepKey = getStepExecutionKeyForState(step, state); if (stepKey) { return AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS.has(stepKey); } return AUTO_RUN_BACKGROUND_COMPLETED_STEPS.has(step); } function doesStepUseCompletionSignal(step, state = {}) { const stepKey = getStepExecutionKeyForState(step, state); if (stepKey) { return STEP_COMPLETION_SIGNAL_STEP_KEYS.has(stepKey); } return STEP_COMPLETION_SIGNAL_STEPS.has(step); } function getAutoRunPreExecutionDelayMs(step, state = {}) { const stepKey = getStepExecutionKeyForState(step, state); if (stepKey) { return AUTO_RUN_PRE_EXECUTION_DELAYS_BY_STEP_KEY.get(stepKey) || 0; } return 0; } function getStepCompletionSignalTimeoutMs(step, state = {}) { const stepKey = getStepExecutionKeyForState(step, state); if (stepKey) { return STEP_COMPLETION_SIGNAL_TIMEOUTS_BY_STEP_KEY.get(stepKey) || AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS; } return AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS; } function notifyStepComplete(step, payload) { const waiter = stepWaiters.get(step); console.log(LOG_PREFIX, `[notifyStepComplete] step ${step}, hasWaiter=${Boolean(waiter)}`); if (waiter) waiter.resolve(payload); } function notifyStepError(step, error) { const waiter = stepWaiters.get(step); console.warn(LOG_PREFIX, `[notifyStepError] step ${step}, hasWaiter=${Boolean(waiter)}, error=${error}`); if (waiter) waiter.reject(new Error(error)); } async function runCompletedStepSideEffects(step, payload, completionState, lastStepId) { await handleStepData(step, payload); if (step === lastStepId) { await appendAndBroadcastAccountRunRecord('success', completionState); } } async function reportCompletedStepSideEffectError(step, error) { const message = getErrorMessage(error); console.warn(LOG_PREFIX, `[completeStepFromBackground] step ${step} post-completion side effect failed:`, error); await addLog(`步骤 ${step} 已完成,但完成后的收尾处理失败:${message}`, 'warn'); } async function completeStepFromBackground(step, payload = {}) { if (stopRequested) { await setStepStatus(step, 'stopped'); await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, null, STOP_ERROR_MESSAGE); notifyStepError(step, STOP_ERROR_MESSAGE); return; } const latestState = await getState(); const lastStepId = typeof getLastStepIdForState === 'function' ? getLastStepIdForState(latestState) : (typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10); const completionState = step === lastStepId ? latestState : null; await setStepStatus(step, 'completed'); await addLog(`步骤 ${step} 已完成`, 'ok'); if (step === lastStepId) { notifyStepComplete(step, payload); void runCompletedStepSideEffects(step, payload, completionState, lastStepId) .catch((error) => reportCompletedStepSideEffectError(step, error)); return; } await runCompletedStepSideEffects(step, payload, completionState, lastStepId); notifyStepComplete(step, payload); } async function appendManualAccountRunRecordIfNeeded(status, stateOverride = null, reason = '') { if (!accountRunHistoryHelpers?.appendAccountRunRecord) { return null; } const state = stateOverride || await getState(); return appendAndBroadcastAccountRunRecord(status, state, reason); } async function finalizeDeferredStepExecutionError(step, error) { const latestState = await getState(); const currentStatus = latestState.stepStatuses?.[step]; if (currentStatus === 'completed' || currentStatus === 'failed' || currentStatus === 'stopped') { return; } if (isStopError(error)) { await setStepStatus(step, 'stopped'); await addLog(`步骤 ${step} 已被用户停止`, 'warn'); await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, latestState, getErrorMessage(error)); return; } await setStepStatus(step, 'failed'); await addLog(`步骤 ${step} 失败:${getErrorMessage(error)}`, 'error'); await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, latestState, getErrorMessage(error)); } async function executeStepViaCompletionSignal(step, timeoutMs = 0) { const executionState = await getState(); const resolvedTimeoutMs = Number(timeoutMs) > 0 ? timeoutMs : getStepCompletionSignalTimeoutMs(step, executionState); const completionResultPromise = waitForStepComplete(step, resolvedTimeoutMs).then( payload => ({ ok: true, payload }), error => ({ ok: false, error }), ); let executeError = null; try { await executeStep(step, { deferRetryableTransportError: true }); } catch (err) { executeError = err; if (isStopError(err) || !isRetryableContentScriptTransportError(err)) { notifyStepError(step, getErrorMessage(err)); } } const completionResult = await completionResultPromise; if (completionResult.ok) { if (executeError) { console.warn( LOG_PREFIX, `[executeStepViaCompletionSignal] step ${step} completed after deferred execute error: ${getErrorMessage(executeError)}` ); } return completionResult.payload; } if (executeError && isRetryableContentScriptTransportError(executeError)) { const completionMessage = getErrorMessage(completionResult.error); if (/等待超时/.test(completionMessage)) { await finalizeDeferredStepExecutionError(step, executeError); throw executeError; } throw completionResult.error; } if (executeError) { throw executeError; } throw completionResult.error; } async function waitForRunningStepsToFinish(payload = {}) { let currentState = await getState(); let runningSteps = getRunningSteps(currentState.stepStatuses, currentState); if (!runningSteps.length) { return currentState; } await addLog(`自动继续:检测到步骤 ${runningSteps.join(', ')} 正在运行,等待完成后再继续自动流程...`, 'info'); await broadcastAutoRunStatus('waiting_step', payload); while (runningSteps.length) { await sleepWithStop(250); currentState = await getState(); runningSteps = getRunningSteps(currentState.stepStatuses, currentState); } await addLog('自动继续:当前运行步骤已结束,准备按最新进度继续自动流程...', 'info'); return currentState; } const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10, 11, 12, 13]); const AUTH_CHAIN_STEP_KEYS = new Set([ 'oauth-login', 'fetch-login-code', 'confirm-oauth', 'platform-verify', ]); let activeTopLevelAuthChainExecution = null; function isAuthChainStep(step, state = {}) { const stepKey = typeof getStepDefinitionForState === 'function' ? String(getStepDefinitionForState(step, state)?.key || '').trim() : ''; if (stepKey && typeof AUTH_CHAIN_STEP_KEYS !== 'undefined') { return AUTH_CHAIN_STEP_KEYS.has(stepKey); } return AUTH_CHAIN_STEP_IDS.has(Number(step)); } async function acquireTopLevelAuthChainExecution(step, state = {}) { const normalizedStep = Number(step); if (!isAuthChainStep(normalizedStep, state)) { return { joined: false, release() {}, }; } if (activeTopLevelAuthChainExecution) { const activeExecution = activeTopLevelAuthChainExecution; await addLog( `步骤 ${normalizedStep}:检测到步骤 ${activeExecution.step} 正在运行,本次请求将复用当前授权链,不再重复启动。`, 'warn' ); const result = await activeExecution.promise; if (result?.error) { throw result.error; } return { joined: true, release() {}, }; } let settleExecution = () => {}; const promise = new Promise((resolve) => { settleExecution = (error = null) => resolve({ error }); }); const execution = { step: normalizedStep, promise, }; activeTopLevelAuthChainExecution = execution; return { joined: false, release(error = null) { if (activeTopLevelAuthChainExecution === execution) { activeTopLevelAuthChainExecution = null; } settleExecution(error); }, }; } async function markRunningStepsStopped() { const state = await getState(); const runningSteps = getRunningSteps(state.stepStatuses, state); for (const step of runningSteps) { await setStepStatus(step, 'stopped'); } } async function requestStop(options = {}) { const { logMessage = '已收到停止请求,正在取消当前操作...' } = options; const state = await getState(); const runningSteps = getRunningSteps(state.stepStatuses, state); const inferredStopStep = inferStoppedRecordStep(state); const timerPlan = getPendingAutoRunTimerPlan(state); if (timerPlan?.kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START && !autoRunActive) { await cancelScheduledAutoRun({ logMessage: options.logMessage === false ? false : (options.logMessage || '已取消自动运行倒计时计划。'), }); return; } if (timerPlan && !autoRunActive) { autoRunCurrentRun = timerPlan.currentRun; autoRunTotalRuns = timerPlan.totalRuns; autoRunAttemptRun = timerPlan.attemptRun; clearCurrentAutoRunSessionId(timerPlan.autoRunSessionId); if (options.logMessage !== false) { await addLog(options.logMessage || '已停止等待中的自动流程。', 'warn'); } await broadcastAutoRunStatus('stopped', { currentRun: timerPlan.currentRun, totalRuns: timerPlan.totalRuns, attemptRun: timerPlan.attemptRun, sessionId: 0, }, { autoRunSessionId: 0, autoRunSkipFailures: timerPlan.autoRunSkipFailures, autoRunRoundSummaries: serializeAutoRunRoundSummaries(timerPlan.totalRuns, timerPlan.roundSummaries), autoRunTimerPlan: null, scheduledAutoRunPlan: null, }); await clearAutoRunTimerAlarm(); clearStopRequest(); return; } if (stopRequested) return; stopRequested = true; clearCurrentAutoRunSessionId(); cancelPendingCommands(); abortActiveIcloudRequests(); cleanupStep8NavigationListeners(); rejectPendingStep8(new Error(STOP_ERROR_MESSAGE)); await addLog(logMessage, 'warn'); await broadcastStopToContentScripts(); if (!runningSteps.length && Number.isInteger(inferredStopStep) && inferredStopStep > 0) { await appendAndBroadcastAccountRunRecord('stopped', state, STOP_ERROR_MESSAGE); } for (const waiter of stepWaiters.values()) { waiter.reject(new Error(STOP_ERROR_MESSAGE)); } stepWaiters.clear(); if (state.plusManualConfirmationPending) { const clearManualConfirmationState = { plusManualConfirmationPending: false, plusManualConfirmationRequestId: '', plusManualConfirmationStep: 0, plusManualConfirmationMethod: '', plusManualConfirmationTitle: '', plusManualConfirmationMessage: '', }; await setState(clearManualConfirmationState); broadcastDataUpdate(clearManualConfirmationState); } if (resumeWaiter) { resumeWaiter.reject(new Error(STOP_ERROR_MESSAGE)); resumeWaiter = null; } await markRunningStepsStopped(); autoRunActive = false; await broadcastAutoRunStatus('stopped', { currentRun: autoRunCurrentRun, totalRuns: autoRunTotalRuns, attemptRun: autoRunAttemptRun, sessionId: 0, }, { autoRunSessionId: 0, autoRunTimerPlan: null, scheduledAutoRunPlan: null, }); } // ============================================================ // Step Execution // ============================================================ const STEP_FETCH_NETWORK_RETRY_POLICIES = new Map([ [4, { maxAttempts: 3, cooldownMs: 12000 }], [8, { maxAttempts: 3, cooldownMs: 12000 }], [9, { maxAttempts: 3, cooldownMs: 12000 }], ]); async function executeStep(step, options = {}) { const { deferRetryableTransportError = false } = options; console.log(LOG_PREFIX, `Executing step ${step}`); let state = await getState(); const authChainClaim = await acquireTopLevelAuthChainExecution(step, state); if (authChainClaim.joined) { return; } let executionError = null; throwIfStopped(); try { await setStepStatus(step, 'running'); await addLog(`步骤 ${step} 开始执行`); await humanStepDelay(); const fetchRetryPolicy = typeof getStepFetchNetworkRetryPolicy === 'function' ? getStepFetchNetworkRetryPolicy(step) : null; const isFetchRetryable = (error) => { if (typeof isStepFetchNetworkRetryableError === 'function') { return isStepFetchNetworkRetryableError(error); } return isRetryableContentScriptTransportError(error); }; let attempt = 1; while (true) { state = await getState(); // Set flow start time on first step if (step === 1 && !state.flowStartTime) { await setState({ flowStartTime: Date.now() }); } const activeStepRegistry = getStepRegistryForState(state); if (!activeStepRegistry?.getStepDefinition?.(step)) { throw new Error(`当前模式下不存在步骤:${step}`); } try { await activeStepRegistry.executeStep(step, { ...state, visibleStep: Number(step), stepDefinition: getStepDefinitionForState(step, state), }); if (attempt > 1) { await addLog( `[NETWORK_FETCH_RETRY] 步骤 ${step}:网络请求异常已恢复,当前重试成功(${attempt}/${fetchRetryPolicy?.maxAttempts || attempt})。`, 'ok' ); } break; } catch (attemptError) { if (!fetchRetryPolicy || !isFetchRetryable(attemptError) || attempt >= fetchRetryPolicy.maxAttempts) { throw attemptError; } const nextAttempt = attempt + 1; const cooldownMs = fetchRetryPolicy.cooldownMs; const cooldownSeconds = Math.max(1, Math.ceil(cooldownMs / 1000)); await addLog( `[NETWORK_FETCH_RETRY] 步骤 ${step}:检测到网络请求异常(${getErrorMessage(attemptError)}),${cooldownSeconds} 秒后重试(${nextAttempt}/${fetchRetryPolicy.maxAttempts})。`, 'warn' ); if (cooldownMs > 0) { await sleepWithStop(cooldownMs); } attempt = nextAttempt; } } } catch (err) { executionError = err; const errorState = await getState(); if (isStopError(err)) { await setStepStatus(step, 'stopped'); await addLog(`步骤 ${step} 已被用户停止`, 'warn'); await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, errorState, getErrorMessage(err)); throw err; } if (isTerminalSecurityBlockedError(err)) { await handleCloudflareSecurityBlocked(err); throw new Error(STOP_ERROR_MESSAGE); } if (isBrowserSwitchRequiredError(err)) { await handleBrowserSwitchRequired(err); throw new Error(STOP_ERROR_MESSAGE); } if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step, errorState) && isRetryableContentScriptTransportError(err))) { await setStepStatus(step, 'failed'); await addLog(`步骤 ${step} 失败:${err.message}`, 'error'); await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, errorState, getErrorMessage(err)); } else { console.warn( LOG_PREFIX, `[executeStep] deferring retryable transport error for step ${step}: ${getErrorMessage(err)}` ); } throw err; } finally { authChainClaim.release(executionError); } } /** * Execute a step and wait for it to complete before returning. * @param {number} step * @param {number} delayAfter - ms to wait after completion (for page transitions) */ async function executeStepAndWait(step, delayAfter = 2000) { throwIfStopped(); const delaySeconds = normalizeAutoStepDelaySeconds((await getState()).autoStepDelaySeconds, null); if (delaySeconds > 0) { await addLog( `自动运行:步骤 ${step} 执行前额外等待 ${delaySeconds} 秒,避免节奏过快。`, 'info' ); await sleepWithStop(delaySeconds * 1000); } let executionState = await getState(); const preExecutionDelayMs = getAutoRunPreExecutionDelayMs(step, executionState); if (preExecutionDelayMs > 0) { await addLog( `自动运行:步骤 ${step} 执行前固定等待 ${Math.round(preExecutionDelayMs / 1000)} 秒,确保 Plus Checkout 创建前页面稳定。`, 'info' ); await sleepWithStop(preExecutionDelayMs); executionState = await getState(); } if (doesStepUseBackgroundCompletion(step, executionState)) { await addLog(`自动运行:步骤 ${step} 由后台流程负责收尾,执行函数返回后将直接进入下一步。`, 'info'); await executeStep(step); const latestState = await getState(); await addLog(`自动运行:步骤 ${step} 已执行返回,当前状态为 ${latestState.stepStatuses?.[step] || 'pending'},准备继续后续步骤。`, 'info'); } else if (doesStepUseCompletionSignal(step, executionState)) { await addLog(`自动运行:步骤 ${step} 已发起,正在等待完成信号(超时 ${AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS / 1000} 秒)。`, 'info'); await executeStepViaCompletionSignal(step, AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS); await addLog(`自动运行:步骤 ${step} 已收到完成信号,准备继续后续步骤。`, 'info'); } else { await executeStep(step); } if (step === 5) { const signupTabId = await getTabId('signup-page'); if (signupTabId) { await addLog('自动运行:步骤 5 已收到完成信号,正在等待当前页面完成加载并稳定...', 'info'); await waitForTabStableComplete(signupTabId, { timeoutMs: 30000, retryDelayMs: 300, stableMs: 1000, initialDelayMs: 800, }); } } // Extra delay for page transitions / DOM updates if (delayAfter > 0) { await sleepWithStop(delayAfter + Math.floor(Math.random() * 1200)); } } function getEmailGeneratorLabel(generator) { const customEmailPoolGenerator = typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string' ? CUSTOM_EMAIL_POOL_GENERATOR : 'custom-pool'; const gmailAliasGenerator = typeof GMAIL_ALIAS_GENERATOR === 'string' ? GMAIL_ALIAS_GENERATOR : 'gmail-alias'; if (generator === 'custom') { return '自定义邮箱'; } if (generator === gmailAliasGenerator) { return 'Gmail +tag 邮箱'; } if (generator === customEmailPoolGenerator) { return '自定义邮箱池'; } if (generator === 'icloud') { return 'iCloud 隐私邮箱'; } if (generator === 'cloudflare') return 'Cloudflare 邮箱'; if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email'; return 'Duck 邮箱'; } const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMail2925SessionManager({ addLog, broadcastDataUpdate, chrome, findMail2925Account, getMail2925AccountStatus, getState, isAutoRunLockedState, isMail2925AccountAvailable: self.Mail2925Utils?.isMail2925AccountAvailable, MAIL2925_LIMIT_COOLDOWN_MS, normalizeMail2925Account, normalizeMail2925Accounts, pickMail2925AccountForRun, requestStop, ensureContentScriptReadyOnTab, reuseOrCreateTab, sendToContentScriptResilient, sendToMailContentScriptResilient, setPersistentSettings, setState, sleepWithStop, throwIfStopped, upsertMail2925AccountInList, waitForTabComplete, waitForTabUrlMatch, }); async function upsertMail2925Account(input = {}) { return mail2925SessionManager.upsertMail2925Account(input); } async function deleteMail2925Account(accountId) { return mail2925SessionManager.deleteMail2925Account(accountId); } async function deleteMail2925Accounts(mode = 'all') { return mail2925SessionManager.deleteMail2925Accounts(mode); } async function patchMail2925Account(accountId, updates = {}) { return mail2925SessionManager.patchMail2925Account(accountId, updates); } async function setCurrentMail2925Account(accountId, options = {}) { return mail2925SessionManager.setCurrentMail2925Account(accountId, options); } function getCurrentMail2925Account(state = null) { return mail2925SessionManager.getCurrentMail2925Account(state || {}); } async function ensureMail2925AccountForFlow(options = {}) { return mail2925SessionManager.ensureMail2925AccountForFlow(options); } async function ensureMail2925MailboxSession(options = {}) { return mail2925SessionManager.ensureMail2925MailboxSession(options); } async function handleMail2925LimitReachedError(step, error) { return mail2925SessionManager.handleMail2925LimitReachedError(step, error); } function isMail2925LimitReachedError(error) { if (typeof mail2925SessionManager !== 'undefined' && mail2925SessionManager?.isMail2925LimitReachedError) { return mail2925SessionManager.isMail2925LimitReachedError(error); } const message = String(typeof error === 'string' ? error : error?.message || ''); return /^MAIL2925_LIMIT_REACHED::/.test(message) || /子邮箱.{0,12}已达上限|已达上限邮箱|子邮箱上限|邮箱已达上限/i.test(message); } function isMail2925ThreadTerminatedError(error) { if (typeof mail2925SessionManager !== 'undefined' && mail2925SessionManager?.isMail2925ThreadTerminatedError) { return mail2925SessionManager.isMail2925ThreadTerminatedError(error); } const message = String(typeof error === 'string' ? error : error?.message || ''); return /^MAIL2925_THREAD_TERMINATED::/.test(message); } function isMail2925PoolExhaustedPauseError(error) { if (typeof mail2925SessionManager !== 'undefined' && mail2925SessionManager?.isMail2925PoolExhaustedPauseError) { return mail2925SessionManager.isMail2925PoolExhaustedPauseError(error); } const message = String(typeof error === 'string' ? error : error?.message || ''); return /^MAIL2925_POOL_EXHAUSTED_PAUSE::/.test(message); } const payPalAccountStore = self.MultiPageBackgroundPayPalAccountStore?.createPayPalAccountStore({ broadcastDataUpdate, findPayPalAccount, getState, normalizePayPalAccount, normalizePayPalAccounts, setPersistentSettings, setState, upsertPayPalAccountInList, }); async function syncPayPalAccounts(accounts) { return payPalAccountStore?.syncPayPalAccounts?.(accounts) || []; } async function upsertPayPalAccount(input = {}) { if (!payPalAccountStore?.upsertPayPalAccount) { throw new Error('PayPal 账号存储能力尚未接入。'); } return payPalAccountStore.upsertPayPalAccount(input); } async function setCurrentPayPalAccount(accountId) { if (!payPalAccountStore?.setCurrentPayPalAccount) { throw new Error('PayPal 账号选择能力尚未接入。'); } return payPalAccountStore.setCurrentPayPalAccount(accountId); } function getCurrentPayPalAccount(state = null) { return payPalAccountStore?.getCurrentPayPalAccount?.(state || {}) || null; } const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGeneratedEmailHelpers({ addLog, buildGeneratedAliasEmail, buildCloudflareTempEmailHeaders, CLOUDFLARE_TEMP_EMAIL_GENERATOR, CUSTOM_EMAIL_POOL_GENERATOR, DUCK_AUTOFILL_URL, fetch, fetchIcloudHideMyEmail, getCloudflareTempEmailAddressFromResponse, getCloudflareTempEmailConfig, getCustomEmailPoolEmail: getCustomEmailPoolEmailForRun, getState, ensureMail2925AccountForFlow, joinCloudflareTempEmailUrl, normalizeCloudflareDomain, normalizeCloudflareTempEmailAddress, normalizeEmailGenerator, isGeneratedAliasProvider, reuseOrCreateTab, sendToContentScript, setEmailState, throwIfStopped, }); function generateCloudflareAliasLocalPart() { return generatedEmailHelpers.generateCloudflareAliasLocalPart(); } async function fetchCloudflareEmail(state, options = {}) { return generatedEmailHelpers.fetchCloudflareEmail(state, options); } function ensureCloudflareTempEmailConfig(state, options = {}) { return generatedEmailHelpers.ensureCloudflareTempEmailConfig(state, options); } async function requestCloudflareTempEmailJson(config, path, options = {}) { return generatedEmailHelpers.requestCloudflareTempEmailJson(config, path, options); } async function fetchCloudflareTempEmailAddress(state, options = {}) { return generatedEmailHelpers.fetchCloudflareTempEmailAddress(state, options); } async function fetchDuckEmail(options = {}) { return generatedEmailHelpers.fetchDuckEmail(options); } async function fetchGeneratedEmail(state, options = {}) { return generatedEmailHelpers.fetchGeneratedEmail(state, options); } // ============================================================ // Auto Run Flow // ============================================================ let autoRunActive = false; let autoRunCurrentRun = 0; let autoRunTotalRuns = 1; let autoRunAttemptRun = 0; let autoRunSessionId = 0; let autoRunSessionSeed = 0; let ipProxyAutoSyncRunning = false; const EMAIL_FETCH_MAX_ATTEMPTS = 5; const VERIFICATION_POLL_MAX_ROUNDS = 5; const STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS = 25000; const MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15; const MAIL_2925_VERIFICATION_INTERVAL_MS = 15000; const AUTO_STEP_DELAYS = { 1: 2000, 2: 2000, 3: 3000, 4: 2000, 5: 0, 6: 3000, 7: 2000, 8: 2000, 9: 1000, }; const accountRunHistoryHelpers = self.MultiPageBackgroundAccountRunHistory?.createAccountRunHistoryHelpers({ ACCOUNT_RUN_HISTORY_STORAGE_KEY, addLog, buildLocalHelperEndpoint: (baseUrl, path) => buildHotmailLocalEndpoint(baseUrl, path), chrome, getErrorMessage, getState, normalizeAccountRunHistoryHelperBaseUrl, }); const contributionOAuthManager = self.MultiPageBackgroundContributionOAuth?.createContributionOAuthManager({ addLog, broadcastDataUpdate, chrome, closeLocalhostCallbackTabs, getState, setState, }); contributionOAuthManager?.ensureCallbackListeners?.(); async function broadcastAccountRunHistoryUpdate() { if (!accountRunHistoryHelpers?.getPersistedAccountRunHistory) { return []; } const history = await accountRunHistoryHelpers.getPersistedAccountRunHistory(); broadcastDataUpdate({ accountRunHistory: history }); return history; } async function appendAndBroadcastAccountRunRecord(status, stateOverride = null, reason = '') { if (!accountRunHistoryHelpers?.appendAccountRunRecord) { return null; } const state = stateOverride || await getState(); const resolvedStatus = resolveAccountRunRecordStatusForStop(status, state); const resolvedReason = resolveAccountRunRecordReasonForStop(resolvedStatus, reason); const record = await accountRunHistoryHelpers.appendAccountRunRecord(resolvedStatus, state, resolvedReason); if (!record) { return null; } await broadcastAccountRunHistoryUpdate(); return record; } async function clearAndBroadcastAccountRunHistory(stateOverride = null) { if (!accountRunHistoryHelpers?.clearAccountRunHistory) { return { clearedCount: 0 }; } const result = await accountRunHistoryHelpers.clearAccountRunHistory(stateOverride); await broadcastAccountRunHistoryUpdate(); return result; } async function deleteAndBroadcastAccountRunHistoryRecords(recordIds = [], stateOverride = null) { if (!accountRunHistoryHelpers?.deleteAccountRunHistoryRecords) { return { deletedCount: 0, remainingCount: 0 }; } const result = await accountRunHistoryHelpers.deleteAccountRunHistoryRecords(recordIds, stateOverride); await broadcastAccountRunHistoryUpdate(); return result; } function resolveIpProxyCandidateCountForAutoSwitch(state = {}, mode = 'account', provider = DEFAULT_IP_PROXY_SERVICE) { const normalizedMode = typeof normalizeIpProxyMode === 'function' ? normalizeIpProxyMode(mode) : String(mode || 'account').trim().toLowerCase(); const normalizedProvider = typeof normalizeIpProxyProviderValue === 'function' ? normalizeIpProxyProviderValue(provider) : String(provider || DEFAULT_IP_PROXY_SERVICE).trim().toLowerCase(); if (normalizedMode === 'account' && typeof getAccountModeProxyPoolFromState === 'function') { const pool = getAccountModeProxyPoolFromState(state, normalizedProvider); return Array.isArray(pool) ? pool.length : 0; } if (typeof getIpProxyRuntimeSnapshot === 'function') { const runtime = getIpProxyRuntimeSnapshot(state, normalizedMode, normalizedProvider); return Array.isArray(runtime?.pool) ? runtime.pool.length : 0; } return 0; } function resolveIpProxyAutoSyncIntervalMinutes(value, fallback = IP_PROXY_AUTO_SYNC_DEFAULT_INTERVAL_MINUTES) { return normalizeIpProxyAutoSyncIntervalMinutes(value, fallback); } async function clearIpProxyAutoSyncAlarm() { await chrome.alarms.clear(IP_PROXY_AUTO_SYNC_ALARM_NAME); } async function ensureIpProxyAutoSyncAlarm(stateOverride = null) { const state = stateOverride || await getState(); const enabled = Boolean(state?.ipProxyAutoSyncEnabled); if (!enabled) { await clearIpProxyAutoSyncAlarm(); return false; } const intervalMinutes = resolveIpProxyAutoSyncIntervalMinutes( state?.ipProxyAutoSyncIntervalMinutes, PERSISTED_SETTING_DEFAULTS.ipProxyAutoSyncIntervalMinutes ); const existingAlarm = await chrome.alarms.get(IP_PROXY_AUTO_SYNC_ALARM_NAME); const existingPeriod = Number(existingAlarm?.periodInMinutes) || 0; if (!existingAlarm || Math.abs(existingPeriod - intervalMinutes) > 0.0001) { await chrome.alarms.clear(IP_PROXY_AUTO_SYNC_ALARM_NAME); await chrome.alarms.create(IP_PROXY_AUTO_SYNC_ALARM_NAME, { periodInMinutes: intervalMinutes, delayInMinutes: intervalMinutes, }); } return true; } async function runIpProxyAutoSync(trigger = 'alarm') { if (ipProxyAutoSyncRunning) { return { skipped: true, reason: 'running' }; } ipProxyAutoSyncRunning = true; try { const state = await getState(); if (!state?.ipProxyAutoSyncEnabled) { await clearIpProxyAutoSyncAlarm(); return { skipped: true, reason: 'disabled' }; } if (!state?.ipProxyEnabled) { return { skipped: true, reason: 'proxy_disabled' }; } const mode = typeof normalizeIpProxyMode === 'function' ? normalizeIpProxyMode(state?.ipProxyMode) : String(state?.ipProxyMode || 'account').trim().toLowerCase(); const result = await refreshIpProxyPool({ state, mode, skipExitProbe: true, }); if (typeof addLog === 'function') { const display = String(result?.display || '').trim(); await addLog( display ? `IP 代理自动同步完成(${trigger}):${display}` : `IP 代理自动同步完成(${trigger})。`, 'info' ).catch(() => {}); } return { skipped: false, result }; } catch (error) { if (typeof addLog === 'function') { await addLog( `IP 代理自动同步失败:${error?.message || String(error || '未知错误')}`, 'warn' ).catch(() => {}); } return { skipped: true, reason: 'error', error: error?.message || String(error || '未知错误') }; } finally { ipProxyAutoSyncRunning = false; } } async function maybeSwitchIpProxyAfterAutoRunRoundSuccess(payload = {}) { if (typeof switchIpProxy !== 'function') { return null; } const successfulRuns = Number(payload?.successfulRuns) || 0; if (successfulRuns <= 0) { return null; } const state = await getState(); if (!state?.ipProxyEnabled) { return null; } const mode = typeof normalizeIpProxyMode === 'function' ? normalizeIpProxyMode(state?.ipProxyMode) : String(state?.ipProxyMode || 'account').trim().toLowerCase(); const provider = typeof normalizeIpProxyProviderValue === 'function' ? normalizeIpProxyProviderValue(state?.ipProxyService) : String(state?.ipProxyService || DEFAULT_IP_PROXY_SERVICE).trim().toLowerCase(); const threshold = typeof resolveIpProxyAutoSwitchThreshold === 'function' ? resolveIpProxyAutoSwitchThreshold(state) : Math.max(1, Math.min(500, Number(state?.ipProxyPoolTargetCount) || 20)); if (successfulRuns % threshold !== 0) { return null; } const candidateCount = resolveIpProxyCandidateCountForAutoSwitch(state, mode, provider); if (candidateCount <= 1) { await addLog( `任务切换阈值命中(成功 ${successfulRuns} 轮 / 阈值 ${threshold}),但当前仅 ${candidateCount} 条可切换代理,已跳过自动切换。`, 'info' ); return { skipped: true, reason: 'insufficient_candidates', candidateCount, threshold, successfulRuns, }; } const switchResult = await switchIpProxy('next', { mode, state, forceRefresh: mode === 'api', maxItems: typeof resolveIpProxyPoolTargetCountForMode === 'function' ? resolveIpProxyPoolTargetCountForMode(state, mode) : undefined, }); const display = String(switchResult?.display || '').trim(); const routingApplied = Boolean(switchResult?.proxyRouting?.applied); await addLog( routingApplied ? `任务切换阈值命中(成功 ${successfulRuns} 轮 / 阈值 ${threshold}),已自动切换代理:${display || '已切换到下一条'}。` : `任务切换阈值命中(成功 ${successfulRuns} 轮 / 阈值 ${threshold}),已尝试自动切换代理,但连通性仍异常。`, routingApplied ? 'ok' : 'warn' ); return switchResult; } const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({ addLog, appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args), AUTO_RUN_MAX_RETRIES_PER_ROUND, AUTO_RUN_RETRY_DELAY_MS, AUTO_RUN_TIMER_KIND_BEFORE_RETRY, AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS, broadcastAutoRunStatus, broadcastStopToContentScripts, cancelPendingCommands, clearStopRequest: () => clearStopRequest(), createAutoRunSessionId: () => createAutoRunSessionId(), ensureHotmailMailboxReadyForAutoRunRound: (...args) => ensureHotmailMailboxReadyForAutoRunRound(...args), getAutoRunStatusPayload, getErrorMessage, getFirstUnfinishedStep, getPendingAutoRunTimerPlan, getRunningSteps, getState, getStopRequested: () => stopRequested, hasSavedProgress, isAddPhoneAuthFailure, isPhoneSmsPlatformRateLimitFailure, isPlusCheckoutNonFreeTrialFailure, isRestartCurrentAttemptError, isStep4Route405RecoveryLimitFailure, isSignupUserAlreadyExistsFailure, isStopError, launchAutoRunTimerPlan, normalizeAutoRunFallbackThreadIntervalMinutes, onAutoRunRoundSuccess: (payload = {}) => maybeSwitchIpProxyAfterAutoRunRoundSuccess(payload), persistAutoRunTimerPlan, resetState, runAutoSequenceFromStep: (...args) => runAutoSequenceFromStep(...args), runtime: { get: () => ({ autoRunActive, autoRunCurrentRun, autoRunTotalRuns, autoRunAttemptRun, autoRunSessionId, }), set: (updates = {}) => { if (updates.autoRunActive !== undefined) autoRunActive = Boolean(updates.autoRunActive); if (updates.autoRunCurrentRun !== undefined) autoRunCurrentRun = Number(updates.autoRunCurrentRun) || 0; if (updates.autoRunTotalRuns !== undefined) autoRunTotalRuns = Number(updates.autoRunTotalRuns) || 0; if (updates.autoRunAttemptRun !== undefined) autoRunAttemptRun = Number(updates.autoRunAttemptRun) || 0; if (updates.autoRunSessionId !== undefined) autoRunSessionId = normalizeAutoRunSessionId(updates.autoRunSessionId); }, }, setState, sleepWithStop, throwIfAutoRunSessionStopped: (sessionId) => throwIfAutoRunSessionStopped(sessionId), waitForRunningStepsToFinish, throwIfStopped: () => throwIfStopped(), chrome, }); async function resumeAutoRunIfWaitingForEmail(options = {}) { const { silent = false } = options; const state = await getState(); if (!state.email || !isAutoRunPausedState(state)) { return false; } if (resumeWaiter) { if (!silent) { await addLog('邮箱已就绪,自动继续后续步骤...', 'info'); } resumeWaiter.resolve(); resumeWaiter = null; return true; } return false; } function shouldStopIcloudAutoFetchRetries(error) { if (!error) { return false; } if (error.code === 'ICLOUD_TRANSIENT_CONTEXT') { return true; } const message = getErrorMessage(error).toLowerCase(); if (message.includes('请先在新打开的 icloud 页面中完成登录')) { return true; } return message.includes('网络/上下文波动') || message.includes('could not validate icloud session') || message.includes('status 421') || message.includes('failed to fetch') || message.includes('network request failed') || message.includes('networkerror') || message.includes('cors') || message.includes('address space') || message.includes('timed out') || message.includes('timeout'); } function shouldStopEmailAutoFetchRetries(generator, error) { if (generator === 'icloud' && shouldStopIcloudAutoFetchRetries(error)) { return true; } const message = String(error?.message || ''); if (generator === 'cloudflare' && /域名/.test(message)) { return true; } return generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR && /(服务地址|Admin Auth|域名)/.test(message); } async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { const currentState = await getState(); if (isHotmailProvider(currentState)) { const account = await ensureHotmailAccountForFlow({ allowAllocate: true, markUsed: true, preferredAccountId: null, }); await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:已分配 Hotmail 账号 ${account.email}(第 ${attemptRuns} 次尝试)===`, 'ok'); return account.email; } if (isLuckmailProvider(currentState)) { const purchase = await ensureLuckmailPurchaseForFlow({ allowReuse: true }); await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:LuckMail 邮箱已就绪:${purchase.email_address}(第 ${attemptRuns} 次尝试)===`, 'ok'); return purchase.email_address; } if (isGeneratedAliasProvider(currentState)) { if (currentState.mailProvider === GMAIL_PROVIDER) { if (!currentState.emailPrefix) { throw new Error('Gmail 原邮箱未设置,请先在侧边栏填写。'); } await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:Gmail +tag 模式已启用,将在步骤 3 自动生成邮箱(第 ${attemptRuns} 次尝试)===`, 'info'); return null; } if (!currentState.emailPrefix) { throw new Error('2925 邮箱前缀未设置,请先在侧边栏填写。'); } await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:2925 模式已启用,将在步骤 3 自动生成邮箱(第 ${attemptRuns} 次尝试)===`, 'info'); return null; } if (currentState.email) { return currentState.email; } if (isCustomMailProvider(currentState)) { const poolSize = getCustomMailProviderPool(currentState).length; if (poolSize > 0) { const queuedEmail = getCustomMailProviderPoolEmailForRun(currentState, targetRun); if (!queuedEmail) { throw new Error(`自定义邮箱号池第 ${targetRun} 个邮箱不存在,请检查号池数量是否与自动轮数一致。`); } await setEmailState(queuedEmail); await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:自定义邮箱号池已就绪:${queuedEmail}(第 ${attemptRuns} 次尝试;第 4/8 步仍需手动输入验证码)===`, 'ok'); return queuedEmail; } } if (isCustomEmailPoolGenerator(currentState)) { const queuedEmail = getCustomEmailPoolEmailForRun(currentState, targetRun); if (!queuedEmail) { const poolSize = getCustomEmailPool(currentState).length; throw new Error( poolSize > 0 ? `自定义邮箱池第 ${targetRun} 个邮箱不存在,请检查邮箱池数量是否与自动轮数一致。` : '自定义邮箱池为空,请先至少填写 1 个邮箱。' ); } await setEmailState(queuedEmail); await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:自定义邮箱池已就绪:${queuedEmail}(第 ${attemptRuns} 次尝试)===`, 'ok'); return queuedEmail; } if (shouldUseCustomRegistrationEmail(currentState)) { await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先填写自定义注册邮箱,然后继续 ===`, 'warn'); await broadcastAutoRunStatus('waiting_email', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns, }); await waitForResume(); const resumedState = await getState(); if (!resumedState.email) { throw new Error('无法继续:当前没有注册邮箱。'); } return resumedState.email; } const generator = normalizeEmailGenerator(currentState.emailGenerator); const generatorLabel = getEmailGeneratorLabel(generator); let lastError = null; let attemptedFetches = 0; for (let attempt = 1; attempt <= EMAIL_FETCH_MAX_ATTEMPTS; attempt++) { attemptedFetches = attempt; try { if (attempt > 1) { await addLog(`${generatorLabel}:正在进行第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次自动获取重试...`, 'warn'); } const generatedEmail = await fetchGeneratedEmail(currentState, { generateNew: generator !== 'icloud' || normalizeIcloudFetchMode(currentState.icloudFetchMode) === 'always_new', generator, }); await addLog( `=== 目标 ${targetRun}/${totalRuns} 轮:${generatorLabel}已就绪:${generatedEmail}(第 ${attemptRuns} 次尝试,第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次获取)===`, 'ok' ); return generatedEmail; } catch (err) { lastError = err; await addLog(`${generatorLabel}自动获取失败(${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS}):${err.message}`, 'warn'); if (generator === 'icloud' && shouldStopIcloudAutoFetchRetries(err)) { await addLog('iCloud:检测到会话/网络异常,本轮将停止重复重试。请先确认 iCloud 页面已登录,再点击“我已登录”或手动粘贴邮箱继续。', 'warn'); } if (shouldStopEmailAutoFetchRetries(generator, err)) { break; } } } const totalAttempts = Math.max(1, attemptedFetches); await addLog(`${generatorLabel}自动获取已连续失败 ${totalAttempts} 次:${lastError?.message || '未知错误'}`, 'error'); await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先自动获取邮箱或手动粘贴邮箱,然后继续 ===`, 'warn'); await broadcastAutoRunStatus('waiting_email', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns, }); await waitForResume(); const resumedState = await getState(); if (!resumedState.email) { throw new Error('无法继续:当前没有邮箱地址。'); } return resumedState.email; } async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { const currentState = await getState(); if (isHotmailProvider(currentState)) { const account = await ensureHotmailAccountForFlow({ allowAllocate: true, markUsed: true, preferredAccountId: null, }); await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:已分配 Hotmail 账号 ${account.email}(第 ${attemptRuns} 次尝试)===`, 'ok'); return account.email; } if (isLuckmailProvider(currentState)) { const purchase = await ensureLuckmailPurchaseForFlow({ allowReuse: true }); await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:LuckMail 邮箱已就绪:${purchase.email_address}(第 ${attemptRuns} 次尝试)===`, 'ok'); return purchase.email_address; } if (isGeneratedAliasProvider(currentState)) { if (isReusableGeneratedAliasEmail(currentState)) { await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:当前已复用 ${currentState.email},将直接继续执行(第 ${attemptRuns} 次尝试)===`, 'info'); return currentState.email; } let managedAliasState = currentState; if ( String(currentState.mailProvider || '').trim().toLowerCase() === '2925' && Boolean(currentState.mail2925UseAccountPool) ) { const account = await ensureMail2925AccountForFlow({ allowAllocate: true, preferredAccountId: currentState.currentMail2925AccountId || null, markUsed: true, }); managedAliasState = { ...(await getState()), currentMail2925AccountId: account.id, }; await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:已分配 2925 账号 ${account.email}(第 ${attemptRuns} 次尝试)===`, 'ok'); } const baseEmail = getManagedAliasBaseEmail(managedAliasState); if (!baseEmail && !managedAliasState.email) { const baseLabel = currentState.mailProvider === GMAIL_PROVIDER ? 'Gmail 原邮箱' : '2925 基邮箱'; throw new Error(`${baseLabel}未设置,请先填写,或直接在“注册邮箱”中手动填写完整邮箱。`); } await addLog( `=== 目标 ${targetRun}/${totalRuns} 轮:${currentState.mailProvider === GMAIL_PROVIDER ? 'Gmail +tag' : '2925'} 模式已启用,将在步骤 3 自动生成邮箱(第 ${attemptRuns} 次尝试)===`, 'info' ); return null; } if (currentState.email) { return currentState.email; } if (isCustomMailProvider(currentState)) { const poolSize = getCustomMailProviderPool(currentState).length; if (poolSize > 0) { const queuedEmail = getCustomMailProviderPoolEmailForRun(currentState, targetRun); if (!queuedEmail) { throw new Error(`自定义邮箱号池第 ${targetRun} 个邮箱不存在,请检查号池数量是否与自动轮数一致。`); } await setEmailState(queuedEmail); await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:自定义邮箱号池已就绪:${queuedEmail}(第 ${attemptRuns} 次尝试;第 4/8 步仍需手动输入验证码)===`, 'ok'); return queuedEmail; } } if (isCustomEmailPoolGenerator(currentState)) { const queuedEmail = getCustomEmailPoolEmailForRun(currentState, targetRun); if (!queuedEmail) { const poolSize = getCustomEmailPool(currentState).length; throw new Error( poolSize > 0 ? `自定义邮箱池第 ${targetRun} 个邮箱不存在,请检查邮箱池数量是否与自动轮数一致。` : '自定义邮箱池为空,请先至少填写 1 个邮箱。' ); } await setEmailState(queuedEmail); await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:自定义邮箱池已就绪:${queuedEmail}(第 ${attemptRuns} 次尝试)===`, 'ok'); return queuedEmail; } if (shouldUseCustomRegistrationEmail(currentState)) { await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先填写自定义注册邮箱,然后继续 ===`, 'warn'); await broadcastAutoRunStatus('waiting_email', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns, }); await waitForResume(); const resumedState = await getState(); if (!resumedState.email) { throw new Error('无法继续:当前没有注册邮箱。'); } return resumedState.email; } const generator = normalizeEmailGenerator(currentState.emailGenerator); const generatorLabel = getEmailGeneratorLabel(generator); let lastError = null; let attemptedFetches = 0; for (let attempt = 1; attempt <= EMAIL_FETCH_MAX_ATTEMPTS; attempt++) { attemptedFetches = attempt; try { if (attempt > 1) { await addLog(`${generatorLabel}:正在进行第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次自动获取重试...`, 'warn'); } const generatedEmail = await fetchGeneratedEmail(currentState, { generateNew: generator !== 'icloud' || normalizeIcloudFetchMode(currentState.icloudFetchMode) === 'always_new', generator, }); await addLog( `=== 目标 ${targetRun}/${totalRuns} 轮:${generatorLabel}已就绪:${generatedEmail}(第 ${attemptRuns} 次尝试,第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次获取)===`, 'ok' ); return generatedEmail; } catch (err) { lastError = err; await addLog(`${generatorLabel}自动获取失败(${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS}):${err.message}`, 'warn'); if (generator === 'icloud' && shouldStopIcloudAutoFetchRetries(err)) { await addLog('iCloud:检测到会话/网络异常,本轮将停止重复重试。请先确认 iCloud 页面已登录,再点击“我已登录”或手动粘贴邮箱继续。', 'warn'); } if (shouldStopEmailAutoFetchRetries(generator, err)) { break; } } } const totalAttempts = Math.max(1, attemptedFetches); await addLog(`${generatorLabel}自动获取已连续失败 ${totalAttempts} 次:${lastError?.message || '未知错误'}`, 'error'); await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先自动获取邮箱或手动粘贴邮箱,然后继续 ===`, 'warn'); await broadcastAutoRunStatus('waiting_email', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns, }); await waitForResume(); const resumedState = await getState(); if (!resumedState.email) { throw new Error('无法继续:当前没有邮箱地址。'); } return resumedState.email; } async function runAutoSequenceFromStep(startStep, context = {}) { const { targetRun, totalRuns, attemptRuns, continued = false } = context; let postStep7RestartCount = 0; let goPayCheckoutRestartCount = 0; let step4RestartCount = 0; let currentStartStep = startStep; let continueCurrentAttempt = continued; while (true) { if (continueCurrentAttempt) { await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续当前进度,从步骤 ${startStep} 开始(第 ${attemptRuns} 次尝试)===`, 'info'); } else { await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,打开官网并进入密码页 ===`, 'info'); } if (currentStartStep <= 1) { await executeStepAndWait(1, AUTO_STEP_DELAYS[1]); } if (currentStartStep <= 2) { await ensureAutoEmailReady(targetRun, totalRuns, attemptRuns); await executeStepAndWait(2, AUTO_STEP_DELAYS[2]); } if (currentStartStep <= 3) { const latestState = await getState(); const step3Status = latestState.stepStatuses?.[3] || 'pending'; await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,填写密码、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info'); await broadcastAutoRunStatus('running', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns, }); if (isStepDoneStatus(step3Status)) { await addLog(`自动运行:步骤 3 当前状态为 ${step3Status},将直接继续后续流程。`, 'info'); } else { await executeStepAndWait(3, AUTO_STEP_DELAYS[3]); } } else { await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续执行剩余流程(第 ${attemptRuns} 次尝试)===`, 'info'); } const signupTabId = await getTabId('signup-page'); if (signupTabId) { await chrome.tabs.update(signupTabId, { active: true }); } let restartFromStep1WithCurrentEmail = false; let step = Math.max(currentStartStep, 4); while (step <= (typeof getLastStepIdForState === 'function' ? getLastStepIdForState(await getState()) : (typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10))) { const latestState = await getState(); if (typeof getStepDefinitionForState === 'function' && !getStepDefinitionForState(step, latestState)) { step += 1; continue; } const currentStatus = latestState.stepStatuses?.[step] || 'pending'; if (isStepDoneStatus(currentStatus)) { await addLog(`自动运行:步骤 ${step} 当前状态为 ${currentStatus},将直接继续后续流程。`, 'info'); step += 1; continue; } try { await executeStepAndWait(step, AUTO_STEP_DELAYS[step]); step += 1; } catch (err) { if (isStopError(err)) { throw err; } if (step === 8 && isGoPayCheckoutRestartRequiredFailure(err)) { goPayCheckoutRestartCount += 1; if (goPayCheckoutRestartCount > 3) { await addLog(`步骤 8:GoPay Checkout 已连续重建 ${goPayCheckoutRestartCount - 1} 次仍失败,停止自动重试。原因:${getErrorMessage(err)}`, 'error'); throw err; } await addLog( `步骤 8:检测到 GoPay 支付页失效/卡死,准备关闭旧页并回到步骤 6 重新创建 Checkout(第 ${goPayCheckoutRestartCount}/3 次)。原因:${getErrorMessage(err)}`, 'warn' ); await invalidateDownstreamAfterStepRestart(5, { logLabel: `步骤 8 GoPay 支付页失效后准备回到步骤 6 重试(第 ${goPayCheckoutRestartCount}/3 次)`, }); step = 6; continue; } if (step === 4) { if (isSignupUserAlreadyExistsFailure(err)) { throw err; } if (isMail2925ThreadTerminatedError(err)) { await addLog(`步骤 4:2925 已切换账号并要求结束当前尝试:${getErrorMessage(err)}`, 'warn'); throw err; } step4RestartCount += 1; const preservedState = await getState(); const preservedEmail = String(preservedState.email || '').trim(); const preservedPassword = String(preservedState.password || '').trim(); const emailSuffix = preservedEmail ? `当前邮箱:${preservedEmail};` : ''; await addLog( `步骤 4:执行失败,准备沿用当前邮箱回到步骤 1 重新开始(第 ${step4RestartCount} 次重开)。${emailSuffix}原因:${getErrorMessage(err)}`, 'warn' ); await invalidateDownstreamAfterStepRestart(1, { logLabel: `步骤 4 报错后准备回到步骤 1 沿用当前邮箱重试(第 ${step4RestartCount} 次重开)`, }); const restorePayload = {}; if (preservedEmail) restorePayload.email = preservedEmail; if (preservedPassword) restorePayload.password = preservedPassword; if (Object.keys(restorePayload).length) { await setState(restorePayload); } currentStartStep = 1; continueCurrentAttempt = true; restartFromStep1WithCurrentEmail = true; break; } const restartDecision = await getPostStep6AutoRestartDecision(step, err); if (restartDecision.shouldRestart) { postStep7RestartCount += 1; const restartStep = restartDecision.restartStep || (typeof getAuthChainStartStepId === 'function' ? getAuthChainStartStepId(await getState()) : FINAL_OAUTH_CHAIN_START_STEP); const resetAfterStep = Math.max(1, restartStep - 1); const authState = restartDecision.authState; const authStateLabel = authState?.state ? getLoginAuthStateLabel(authState.state) : '未知页面'; const authStateSuffix = authState?.url ? `当前认证页:${authStateLabel}(${authState.url})` : authState?.state ? `当前认证页:${authStateLabel}` : '未获取到认证页状态'; await addLog( `步骤 ${step}:检测到报错且当前未进入 add-phone,正在回到步骤 ${restartStep} 重新开始授权流程(第 ${postStep7RestartCount} 次重开)。${authStateSuffix};原因:${restartDecision.errorMessage || '未知错误'}`, 'warn' ); await invalidateDownstreamAfterStepRestart(resetAfterStep, { logLabel: `步骤 ${step} 报错后准备回到步骤 ${restartStep} 重试(第 ${postStep7RestartCount} 次重开)`, }); step = restartStep; continue; } if (restartDecision.blockedByAddPhone) { const addPhoneUrl = restartDecision.authState?.url || 'https://auth.openai.com/add-phone'; const authChainStartStep = typeof getAuthChainStartStepId === 'function' ? getAuthChainStartStepId(await getState()) : FINAL_OAUTH_CHAIN_START_STEP; await addLog(`步骤 ${step}:检测到认证流程进入 add-phone(${addPhoneUrl}),停止自动回到步骤 ${authChainStartStep} 重开。`, 'warn'); } throw err; } } if (restartFromStep1WithCurrentEmail) { continue; } break; } } async function waitForResume() { throwIfStopped(); const state = await getState(); if (state.email) { await addLog('邮箱已就绪,自动继续后续步骤...', 'info'); return; } return new Promise((resolve, reject) => { resumeWaiter = { resolve, reject }; }); } function createAutoRunRoundSummary(round) { return autoRunController.createAutoRunRoundSummary(round); } function normalizeAutoRunRoundSummary(summary, round) { return autoRunController.normalizeAutoRunRoundSummary(summary, round); } function buildAutoRunRoundSummaries(totalRuns, rawSummaries = []) { return autoRunController.buildAutoRunRoundSummaries(totalRuns, rawSummaries); } function serializeAutoRunRoundSummaries(totalRuns, roundSummaries = []) { return autoRunController.serializeAutoRunRoundSummaries(totalRuns, roundSummaries); } function getAutoRunRoundRetryCount(summary) { return autoRunController.getAutoRunRoundRetryCount(summary); } function formatAutoRunFailureReasons(reasons = []) { return autoRunController.formatAutoRunFailureReasons(reasons); } async function logAutoRunFinalSummary(totalRuns, roundSummaries = []) { return autoRunController.logAutoRunFinalSummary(totalRuns, roundSummaries); } async function skipAutoRunCountdown() { return autoRunController.skipAutoRunCountdown(); } async function waitBetweenAutoRunRounds(targetRun, totalRuns, roundSummary, options = {}) { return autoRunController.waitBetweenAutoRunRounds(targetRun, totalRuns, roundSummary, options); } async function waitBeforeAutoRunRetry(targetRun, totalRuns, nextAttemptRun, options = {}) { return autoRunController.waitBeforeAutoRunRetry(targetRun, totalRuns, nextAttemptRun, options); } async function handleAutoRunLoopUnhandledError(error) { return autoRunController.handleAutoRunLoopUnhandledError(error); } function startAutoRunLoop(totalRuns, options = {}) { return autoRunController.startAutoRunLoop(totalRuns, options); } async function autoRunLoop(totalRuns, options = {}) { return autoRunController.autoRunLoop(totalRuns, options); } async function resumeAutoRun() { throwIfStopped(); const state = await getState(); if (!state.email) { await addLog('无法继续:当前没有邮箱地址,请先在侧边栏填写邮箱。', 'error'); return false; } const resumedInMemory = await resumeAutoRunIfWaitingForEmail({ silent: true }); if (resumedInMemory) { return true; } if (!isAutoRunPausedState(state)) { return false; } if (autoRunActive) { return false; } const totalRuns = state.autoRunTotalRuns || 1; const currentRun = state.autoRunCurrentRun || 1; const attemptRun = state.autoRunAttemptRun || 1; await addLog('检测到自动流程暂停上下文已丢失,正在从当前进度恢复自动运行...', 'warn'); startAutoRunLoop(totalRuns, { autoRunSessionId: normalizeAutoRunSessionId(state.autoRunSessionId), autoRunSkipFailures: Boolean(state.autoRunSkipFailures), mode: 'continue', resumeCurrentRun: currentRun, resumeAttemptRun: attemptRun, resumeRoundSummaries: state.autoRunRoundSummaries, }); return true; } // ============================================================ // Signup / OAuth Helpers // ============================================================ const SIGNUP_ENTRY_URL = 'https://chatgpt.com/'; const SIGNUP_PAGE_INJECT_FILES = ['content/utils.js', 'content/auth-page-recovery.js', 'content/phone-auth.js', 'content/signup-page.js']; const panelBridge = self.MultiPageBackgroundPanelBridge?.createPanelBridge({ chrome, addLog, closeConflictingTabsForSource, ensureContentScriptReadyOnTab, getPanelMode, normalizeCodex2ApiUrl, normalizeSub2ApiUrl, rememberSourceLastUrl, sendToContentScript, sendToContentScriptResilient, waitForTabUrlFamily, DEFAULT_SUB2API_GROUP_NAME, SUB2API_STEP1_RESPONSE_TIMEOUT_MS, }); const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpers({ addLog, buildGeneratedAliasEmail, chrome, ensureContentScriptReadyOnTab, ensureHotmailAccountForFlow, ensureMail2925AccountForFlow, ensureLuckmailPurchaseForFlow, getTabId, isGeneratedAliasProvider, isReusableGeneratedAliasEmail, isSignupEmailVerificationPageUrl, isRetryableContentScriptTransportError, isHotmailProvider, isLuckmailProvider, isSignupPasswordPageUrl, isTabAlive, reuseOrCreateTab, sendToContentScriptResilient, setEmailState, SIGNUP_ENTRY_URL, SIGNUP_PAGE_INJECT_FILES, waitForTabUrlMatch, }); const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.createVerificationFlowHelpers({ addLog, chrome, closeConflictingTabsForSource, CLOUDFLARE_TEMP_EMAIL_PROVIDER, completeStepFromBackground, confirmCustomVerificationStepBypassRequest: (step) => chrome.runtime.sendMessage({ type: 'REQUEST_CUSTOM_VERIFICATION_BYPASS_CONFIRMATION', payload: { step }, }), getHotmailVerificationPollConfig, getHotmailVerificationRequestTimestamp, handleMail2925LimitReachedError, getState, getTabId, HOTMAIL_PROVIDER, isMail2925LimitReachedError, isRetryableContentScriptTransportError, isStopError, LUCKMAIL_PROVIDER, MAIL_2925_VERIFICATION_INTERVAL_MS, MAIL_2925_VERIFICATION_MAX_ATTEMPTS, pollCloudflareTempEmailVerificationCode, pollHotmailVerificationCode, pollLuckmailVerificationCode, sendToContentScript, sendToContentScriptResilient, sendToMailContentScriptResilient, setState, setStepStatus, sleepWithStop, throwIfStopped, VERIFICATION_POLL_MAX_ROUNDS, }); const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.createPhoneVerificationHelpers({ addLog, broadcastDataUpdate, DEFAULT_FIVE_SIM_BASE_URL, DEFAULT_FIVE_SIM_COUNTRY_ORDER, DEFAULT_FIVE_SIM_OPERATOR, DEFAULT_FIVE_SIM_PRODUCT, DEFAULT_NEX_SMS_BASE_URL, DEFAULT_NEX_SMS_COUNTRY_ORDER, DEFAULT_NEX_SMS_SERVICE_CODE, 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, getOAuthFlowRemainingMs, getOAuthFlowStepTimeoutMs, getState, HERO_SMS_COUNTRY_ID, HERO_SMS_COUNTRY_LABEL, HERO_SMS_SERVICE_CODE, HERO_SMS_SERVICE_LABEL, sendToContentScript, sendToContentScriptResilient, setState, sleepWithStop, throwIfStopped, createFiveSimProvider: self.PhoneSmsFiveSimProvider?.createProvider, }); const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({ addLog, completeStepFromBackground, openSignupEntryTab, }); const step2Executor = self.MultiPageBackgroundStep2?.createStep2Executor({ addLog, chrome, completeStepFromBackground, ensureContentScriptReadyOnTab, ensureSignupAuthEntryPageReady, ensureSignupEntryPageReady, ensureSignupPostEmailPageReadyInTab, getTabId, isTabAlive, resolveSignupEmailForFlow, sendToContentScriptResilient, SIGNUP_PAGE_INJECT_FILES, }); const step3Executor = self.MultiPageBackgroundStep3?.createStep3Executor({ addLog, chrome, ensureContentScriptReadyOnTab, generatePassword, getTabId, isTabAlive, sendToContentScript, setPasswordState, setState, SIGNUP_PAGE_INJECT_FILES, }); async function ensureIcloudMailSessionForVerification(options = {}) { const flowState = options?.state || await getState().catch(() => ({})); const hostPreference = getConfiguredIcloudHostPreference(flowState) || normalizeIcloudHost(flowState?.preferredIcloudHost); return checkIcloudSession({ ...(hostPreference ? { hostPreference } : {}), actionLabel: options?.actionLabel || '检查 iCloud 会话', }); } const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({ addLog, chrome, completeStepFromBackground, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, generateRandomBirthday, generateRandomName, ensureMail2925MailboxSession, ensureIcloudMailSession: ensureIcloudMailSessionForVerification, getMailConfig, getTabId, HOTMAIL_PROVIDER, isTabAlive, LUCKMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER, resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep, reuseOrCreateTab, sendToContentScript, sendToContentScriptResilient, isRetryableContentScriptTransportError, shouldUseCustomRegistrationEmail, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, throwIfStopped, }); const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({ addLog, generateRandomBirthday, generateRandomName, sendToContentScript, }); const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({ completeStepFromBackground, runPreStep6CookieCleanup, }); const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({ addLog, completeStepFromBackground, getErrorMessage, getLoginAuthStateLabel, getOAuthFlowStepTimeoutMs, getState, isAddPhoneAuthFailure, isStep6RecoverableResult, isStep6SuccessResult, refreshOAuthUrlBeforeStep6, reuseOrCreateTab, sendToContentScriptResilient, startOAuthFlowTimeoutWindow, STEP6_MAX_ATTEMPTS, throwIfStopped, }); const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ addLog, chrome, CLOUDFLARE_TEMP_EMAIL_PROVIDER, completeStepFromBackground, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, ensureMail2925MailboxSession, ensureIcloudMailSession: ensureIcloudMailSessionForVerification, ensureStep8VerificationPageReady, getOAuthFlowRemainingMs, getOAuthFlowStepTimeoutMs, getPanelMode, getMailConfig, getState, getTabId, HOTMAIL_PROVIDER, isTabAlive, isVerificationMailPollingError, LUCKMAIL_PROVIDER, resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep, rerunStep7ForStep8Recovery: (...args) => rerunStep7ForStep8Recovery(...args), reuseOrCreateTab, setState, shouldUseCustomRegistrationEmail, sleepWithStop, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS, throwIfStopped, }); const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.createPlusCheckoutCreateExecutor({ addLog, chrome, completeStepFromBackground, ensureContentScriptReadyOnTabUntilStopped, registerTab, sendTabMessageUntilStopped, setState, sleepWithStop, throwIfStopped, waitForTabCompleteUntilStopped, }); const plusCheckoutBillingExecutor = self.MultiPageBackgroundPlusCheckoutBilling?.createPlusCheckoutBillingExecutor({ addLog, chrome, completeStepFromBackground, ensureContentScriptReadyOnTabUntilStopped, fetch: typeof fetch === 'function' ? fetch.bind(globalThis) : null, generateRandomName, getAddressSeedForCountry: self.MultiPageAddressSources?.getAddressSeedForCountry, getTabId, isTabAlive, markCurrentRegistrationAccountUsed, sendTabMessageUntilStopped, setState, sleepWithStop, waitForTabCompleteUntilStopped, waitForTabUrlMatchUntilStopped, probeIpProxyExit, }); const goPayManualConfirmExecutor = self.MultiPageBackgroundGoPayManualConfirm?.createGoPayManualConfirmExecutor({ addLog, broadcastDataUpdate, chrome, getTabId, isTabAlive, registerTab, setState, }); const payPalApproveExecutor = self.MultiPageBackgroundPayPalApprove?.createPayPalApproveExecutor({ addLog, chrome, completeStepFromBackground, ensureContentScriptReadyOnTabUntilStopped, getTabId, isTabAlive, sendTabMessageUntilStopped, setState, sleepWithStop, waitForTabCompleteUntilStopped, waitForTabUrlMatchUntilStopped, }); const goPayApproveExecutor = self.MultiPageBackgroundGoPayApprove?.createGoPayApproveExecutor({ addLog, chrome, completeStepFromBackground, ensureContentScriptReadyOnTabUntilStopped, getTabId, isTabAlive, registerTab, sendTabMessageUntilStopped, setState, sleepWithStop, waitForTabCompleteUntilStopped, clickWithDebugger, requestGoPayOtpInput: (payload = {}) => chrome.runtime.sendMessage({ type: 'REQUEST_GOPAY_OTP_INPUT', payload, }), }); const plusReturnConfirmExecutor = self.MultiPageBackgroundPlusReturnConfirm?.createPlusReturnConfirmExecutor({ addLog, completeStepFromBackground, getTabId, isTabAlive, setState, sleepWithStop, waitForTabUrlMatchUntilStopped, }); const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({ addLog, chrome, closeConflictingTabsForSource, completeStepFromBackground, ensureContentScriptReadyOnTab, getPanelMode, getTabId, isLocalhostOAuthCallbackUrl, isTabAlive, normalizeCodex2ApiUrl, normalizeSub2ApiUrl, rememberSourceLastUrl, reuseOrCreateTab, sendToContentScript, sendToContentScriptResilient, shouldBypassStep9ForLocalCpa, SUB2API_STEP9_RESPONSE_TIMEOUT_MS, }); const stepExecutorsByKey = { 'open-chatgpt': () => step1Executor.executeStep1(), 'submit-signup-email': (state) => step2Executor.executeStep2(state), 'fill-password': (state) => step3Executor.executeStep3(state), 'fetch-signup-code': (state) => step4Executor.executeStep4(state), 'fill-profile': (state) => step5Executor.executeStep5(state), 'clear-login-cookies': () => step6Executor.executeStep6(), 'plus-checkout-create': (state) => plusCheckoutCreateExecutor.executePlusCheckoutCreate(state), 'plus-checkout-billing': (state) => plusCheckoutBillingExecutor.executePlusCheckoutBilling(state), 'paypal-approve': (state) => normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY ? goPayApproveExecutor.executeGoPayApprove(state) : payPalApproveExecutor.executePayPalApprove(state), 'plus-checkout-return': (state) => plusReturnConfirmExecutor.executePlusReturnConfirm(state), 'oauth-login': (state) => step7Executor.executeStep7(state), 'fetch-login-code': (state) => step8Executor.executeStep8(state), 'confirm-oauth': (state) => step9Executor.executeStep9(state), 'platform-verify': (state) => executeStep10(state), }; const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({ addLog, appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args), batchUpdateLuckmailPurchases, buildLocalhostCleanupPrefix, buildLuckmailSessionSettingsPayload, buildPersistentSettingsPayload, broadcastDataUpdate, applyIpProxySettingsFromState, cancelScheduledAutoRun, checkIcloudSession, clearAccountRunHistory: (...args) => clearAndBroadcastAccountRunHistory(...args), deleteAccountRunHistoryRecords: (...args) => deleteAndBroadcastAccountRunHistoryRecords(...args), clearAutoRunTimerAlarm, clearLuckmailRuntimeState, clearStopRequest, closeLocalhostCallbackTabs, closeTabsByUrlPrefix, completeStepFromBackground, deleteHotmailAccount, deleteHotmailAccounts, deleteIcloudAlias, deleteUsedIcloudAliases, findPayPalAccount, disableUsedLuckmailPurchases, doesStepUseCompletionSignal, ensureMail2925MailboxSession, ensureManualInteractionAllowed, executeStep, executeStepViaCompletionSignal, exportSettingsBundle, fetchGeneratedEmail, finalizePhoneActivationAfterSuccessfulFlow, finalizeStep3Completion: async () => { const currentState = await getState(); const signupTabId = await getTabId('signup-page'); return signupFlowHelpers.finalizeSignupPasswordSubmitInTab( signupTabId, currentState.password || currentState.customPassword || '', 3 ); }, finalizeIcloudAliasAfterSuccessfulFlow, findHotmailAccount, flushCommand, getCurrentLuckmailPurchase, getPendingAutoRunTimerPlan, getSourceLabel, getState, getStepDefinitionForState, getStepIdsForState, getLastStepIdForState, getTabId, getStopRequested: () => stopRequested, handleCloudflareSecurityBlocked, handleAutoRunLoopUnhandledError, importSettingsBundle, invalidateDownstreamAfterStepRestart, isCloudflareSecurityBlockedError: isTerminalSecurityBlockedError, isAutoRunLockedState, isHotmailProvider, isLocalhostOAuthCallbackUrl, isLuckmailProvider, isStopError, isTabAlive, launchAutoRunTimerPlan, ensureIpProxyAutoSyncAlarm, clearIpProxyAutoSyncAlarm, runIpProxyAutoSync, listIcloudAliases, listLuckmailPurchasesForManagement, markCurrentCustomEmailPoolEntryUsed, markCurrentRegistrationAccountUsed, getCurrentMail2925Account, normalizeHotmailAccounts, normalizeMail2925Accounts, normalizePayPalAccounts, normalizeRunCount, AUTO_RUN_TIMER_KIND_SCHEDULED_START, notifyStepComplete, notifyStepError, patchHotmailAccount, patchMail2925Account, registerTab, requestStop, probeIpProxyExit, resetState, resumeAutoRun, scheduleAutoRun, selectLuckmailPurchase, switchIpProxy, changeIpProxyExit, setCurrentPayPalAccount, setCurrentHotmailAccount, setCurrentMail2925Account, setContributionMode, setEmailState, setEmailStateSilently, setIcloudAliasPreservedState, setIcloudAliasUsedState, setLuckmailPurchaseDisabledState, setLuckmailPurchasePreservedState, setLuckmailPurchaseUsedState, setPersistentSettings, setState, setStepStatus, skipAutoRunCountdown, skipStep, startContributionFlow: (...args) => contributionOAuthManager?.startContributionFlow?.(...args), startAutoRunLoop, pollContributionStatus: (...args) => contributionOAuthManager?.pollContributionStatus?.(...args), syncHotmailAccounts, syncPayPalAccounts, deleteMail2925Account, deleteMail2925Accounts, testHotmailAccountMailAccess, upsertPayPalAccount, upsertMail2925Account, upsertHotmailAccount, verifyHotmailAccount, }); function buildStepRegistry(definitions = []) { return self.MultiPageBackgroundStepRegistry?.createStepRegistry( definitions.map((definition) => ({ ...definition, execute: stepExecutorsByKey[definition.key], })) ); } const normalStepRegistry = buildStepRegistry(NORMAL_STEP_DEFINITIONS); const plusPayPalStepRegistry = buildStepRegistry(PLUS_PAYPAL_STEP_DEFINITIONS); const plusGoPayStepRegistry = buildStepRegistry(PLUS_GOPAY_STEP_DEFINITIONS); function getStepRegistryForState(state = {}) { if (!isPlusModeState(state)) { return normalStepRegistry; } return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY ? plusGoPayStepRegistry : plusPayPalStepRegistry; } async function requestOAuthUrlFromPanel(state, options = {}) { return panelBridge.requestOAuthUrlFromPanel(state, options); } async function requestCpaOAuthUrl(state, options = {}) { return panelBridge.requestCpaOAuthUrl(state, options); } async function requestSub2ApiOAuthUrl(state, options = {}) { return panelBridge.requestSub2ApiOAuthUrl(state, options); } async function openSignupEntryTab(step = 1) { return signupFlowHelpers.openSignupEntryTab(step); } async function ensureSignupEntryPageReady(step = 1) { return signupFlowHelpers.ensureSignupEntryPageReady(step); } async function ensureSignupAuthEntryPageReady(step = 1) { return signupFlowHelpers.ensureSignupEntryPageReady(step); } async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) { return signupFlowHelpers.ensureSignupPasswordPageReadyInTab(tabId, step, options); } async function ensureSignupPostEmailPageReadyInTab(tabId, step = 2, options = {}) { return signupFlowHelpers.ensureSignupPostEmailPageReadyInTab(tabId, step, options); } async function resolveSignupEmailForFlow(state) { return signupFlowHelpers.resolveSignupEmailForFlow(state); } // ============================================================ // Step 1: Open ChatGPT homepage // ============================================================ async function executeStep1() { return step1Executor.executeStep1(); } // ============================================================ // Step 2: Click signup, fill email, continue to password page // ============================================================ async function executeStep2(state) { return step2Executor.executeStep2(state); } // ============================================================ // Step 3: Fill Password (via signup-page.js) // ============================================================ async function executeStep3(state) { return step3Executor.executeStep3(state); } // ============================================================ // Step 4: Get Signup Verification Code (qq-mail.js polls, then fills in signup-page.js) // ============================================================ function getMailConfig(state) { const provider = state.mailProvider || 'qq'; if (provider === 'custom') { return { provider: 'custom', label: '自定义邮箱' }; } if (provider === HOTMAIL_PROVIDER) { return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(API对接/本地助手)' }; } if (provider === ICLOUD_PROVIDER) { const configuredHost = getConfiguredIcloudHostPreference(state) || normalizeIcloudHost(state?.preferredIcloudHost) || 'icloud.com'; const targetMailboxType = normalizeIcloudTargetMailboxType(state?.icloudTargetMailboxType); const useForwardMailbox = targetMailboxType === 'forward-mailbox'; if (useForwardMailbox) { const forwardProvider = normalizeIcloudForwardMailProvider(state?.icloudForwardMailProvider); const forwardConfig = getSharedIcloudForwardMailConfig(forwardProvider); return { ...forwardConfig, label: `iCloud 转发(${forwardConfig.label})`, icloudForwarding: true, }; } const loginUrl = getIcloudLoginUrlForHost(configuredHost) || 'https://www.icloud.com/'; const mailUrl = getIcloudMailUrlForHost(configuredHost) || loginUrl; return { source: 'icloud-mail', url: mailUrl, label: 'iCloud 邮箱', navigateOnReuse: true, }; } if (provider === GMAIL_PROVIDER) { return { source: 'gmail-mail', url: 'https://mail.google.com/mail/u/0/#inbox', label: 'Gmail 邮箱', inject: ['content/activation-utils.js', 'content/utils.js', 'content/gmail-mail.js'], injectSource: 'gmail-mail', }; } if (provider === LUCKMAIL_PROVIDER) { return { provider: LUCKMAIL_PROVIDER, label: 'LuckMail(API 购邮)' }; } if (provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { return { provider: CLOUDFLARE_TEMP_EMAIL_PROVIDER, label: 'Cloudflare Temp Email' }; } if (provider === '163') { return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 邮箱' }; } if (provider === '163-vip') { return { source: 'mail-163', url: 'https://webmail.vip.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 VIP 邮箱' }; } if (provider === '126') { return { source: 'mail-163', url: 'https://mail.126.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '126 邮箱' }; } if (provider === 'inbucket') { const host = normalizeInbucketOrigin(state.inbucketHost); const mailbox = (state.inbucketMailbox || '').trim(); if (!host) { return { error: 'Inbucket 主机地址为空或无效。' }; } if (!mailbox) { return { error: 'Inbucket 邮箱名称为空。' }; } return { source: 'inbucket-mail', url: `${host}/m/${encodeURIComponent(mailbox)}/`, label: `Inbucket 邮箱(${mailbox})`, navigateOnReuse: true, inject: ['content/activation-utils.js', 'content/utils.js', 'content/inbucket-mail.js'], injectSource: 'inbucket-mail', }; } if (provider === '2925') { return { provider: '2925', source: 'mail-2925', url: 'https://2925.com/#/mailList', label: '2925 邮箱', inject: ['content/utils.js', 'content/mail-2925.js'], injectSource: 'mail-2925', }; } return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ 邮箱' }; } function normalizeInbucketOrigin(rawValue) { const value = (rawValue || '').trim(); if (!value) return ''; const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`; try { const parsed = new URL(candidate); return parsed.origin; } catch { return ''; } } function getVerificationCodeStateKey(step) { return verificationFlowHelpers.getVerificationCodeStateKey(step); } function getVerificationCodeLabel(step) { return verificationFlowHelpers.getVerificationCodeLabel(step); } async function confirmCustomVerificationStepBypass(step) { return verificationFlowHelpers.confirmCustomVerificationStepBypass(step); } function getVerificationPollPayload(step, state, overrides = {}) { return verificationFlowHelpers.getVerificationPollPayload(step, state, overrides); } async function requestVerificationCodeResend(step) { return verificationFlowHelpers.requestVerificationCodeResend(step); } async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) { return verificationFlowHelpers.pollFreshVerificationCode(step, state, mail, pollOverrides); } async function pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides = {}) { return verificationFlowHelpers.pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides); } async function submitVerificationCode(step, code) { return verificationFlowHelpers.submitVerificationCode(step, code); } async function resolveVerificationStep(step, state, mail, options = {}) { return verificationFlowHelpers.resolveVerificationStep(step, state, mail, options); } async function executeStep4(state) { return step4Executor.executeStep4(state); } // ============================================================ // Step 5: Fill Name & Birthday (via signup-page.js) // ============================================================ async function executeStep5(state) { return step5Executor.executeStep5(state); } // ============================================================ // Step 6 Cookie Cleanup // ============================================================ function normalizeCookieDomainForMatch(domain) { return String(domain || '').trim().replace(/^\.+/, '').toLowerCase(); } function shouldClearPreLoginCookie(cookie) { const domain = normalizeCookieDomainForMatch(cookie?.domain); if (!domain) return false; return PRE_LOGIN_COOKIE_CLEAR_DOMAINS.some((target) => ( domain === target || domain.endsWith(`.${target}`) )); } function buildCookieRemovalUrl(cookie) { const host = normalizeCookieDomainForMatch(cookie?.domain); const path = String(cookie?.path || '/').startsWith('/') ? String(cookie?.path || '/') : `/${String(cookie?.path || '')}`; return `https://${host}${path}`; } async function collectCookiesForPreLoginCleanup() { if (!chrome.cookies?.getAll) { return []; } const stores = chrome.cookies.getAllCookieStores ? await chrome.cookies.getAllCookieStores() : [{ id: undefined }]; const cookies = []; const seen = new Set(); for (const store of stores) { const storeId = store?.id; const batch = await chrome.cookies.getAll(storeId ? { storeId } : {}); for (const cookie of batch || []) { if (!shouldClearPreLoginCookie(cookie)) continue; const key = [ cookie.storeId || storeId || '', cookie.domain || '', cookie.path || '', cookie.name || '', cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '', ].join('|'); if (seen.has(key)) continue; seen.add(key); cookies.push(cookie); } } return cookies; } async function removeCookieDirectly(cookie) { const details = { url: buildCookieRemovalUrl(cookie), name: cookie.name, }; if (cookie.storeId) { details.storeId = cookie.storeId; } if (cookie.partitionKey) { details.partitionKey = cookie.partitionKey; } try { const result = await chrome.cookies.remove(details); return Boolean(result); } catch (err) { console.warn(LOG_PREFIX, '[removeCookieDirectly] failed', { domain: cookie?.domain, name: cookie?.name, message: getErrorMessage(err), }); return false; } } async function runPreStep6CookieCleanup() { await addLog( `步骤 6:开始前等待 ${Math.round(STEP6_PRE_LOGIN_COOKIE_CLEAR_DELAY_MS / 1000)} 秒,然后直接删除 ChatGPT / OpenAI cookies...`, 'info' ); await sleepWithStop(STEP6_PRE_LOGIN_COOKIE_CLEAR_DELAY_MS); if (!chrome.cookies?.getAll || !chrome.cookies?.remove) { await addLog('步骤 6:当前浏览器不支持 cookies API,无法直接删除 cookies。', 'warn'); return; } const cookies = await collectCookiesForPreLoginCleanup(); let removedCount = 0; for (const cookie of cookies) { throwIfStopped(); if (await removeCookieDirectly(cookie)) { removedCount += 1; } } if (chrome.browsingData?.removeCookies) { try { await chrome.browsingData.removeCookies({ since: 0, origins: PRE_LOGIN_COOKIE_CLEAR_ORIGINS, }); } catch (err) { await addLog(`步骤 6:browsingData 补扫 cookies 失败:${getErrorMessage(err)}`, 'warn'); } } await addLog(`步骤 6:已直接删除 ${removedCount} 个 ChatGPT / OpenAI cookies,准备继续获取链接并登录。`, 'ok'); } // ============================================================ // Step 7: Login and ensure the auth page reaches the login verification page // ============================================================ async function refreshOAuthUrlBeforeStep6(state, options = {}) { const visibleStep = Number(options.visibleStep) || Number(state?.visibleStep) || 7; if (state?.contributionModeExpected && !state?.contributionMode) { throw new Error(`步骤 ${visibleStep}:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入贡献模式后再点击自动。`); } if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) { await addLog(`步骤 ${visibleStep}:contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...`, 'info'); const contributionState = await contributionOAuthManager.startContributionFlow({ nickname: state.contributionNickname || '', openAuthTab: false, stateOverride: state, }); const oauthUrl = String(contributionState?.contributionAuthUrl || '').trim(); if (!oauthUrl) { throw new Error('贡献模式未返回可用的登录地址,请稍后重试。'); } await handleStepData(1, { oauthUrl }); return oauthUrl; } await addLog(`步骤 ${visibleStep}:contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info'); console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel'); const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: `步骤 ${visibleStep}` }); await handleStepData(1, refreshResult); if (!refreshResult?.oauthUrl) { throw new Error('刷新 OAuth 链接后仍未拿到可用链接。'); } return refreshResult.oauthUrl; } function buildOAuthFlowTimeoutError(step, actionLabel = '后续授权流程', state = {}) { const restartStep = typeof getAuthChainStartStepId === 'function' ? getAuthChainStartStepId(state) : FINAL_OAUTH_CHAIN_START_STEP; return new Error( `步骤 ${step}:从拿到 OAuth 登录地址开始,${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟内未完成${actionLabel},结束当前链路,准备从步骤 ${restartStep} 重新开始。` ); } function normalizeOAuthFlowDeadlineAt(value) { const numeric = Number(value); if (!Number.isFinite(numeric) || numeric <= 0) { return null; } return Math.floor(numeric); } function normalizeOAuthFlowSourceUrl(value) { const normalized = String(value || '').trim(); return normalized || null; } async function startOAuthFlowTimeoutWindow(options = {}) { const step = Number(options.step) || 7; const state = options.state || await getState(); if (state?.oauthFlowTimeoutEnabled === false) { await setState({ oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, }); await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,授权后链总超时已关闭,仅保留各步骤本地等待超时。`, 'info'); return null; } const deadlineAt = Date.now() + OAUTH_FLOW_TIMEOUT_MS; await setState({ oauthFlowDeadlineAt: deadlineAt, oauthFlowDeadlineSourceUrl: normalizeOAuthFlowSourceUrl(options.oauthUrl), }); await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,开始 ${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟倒计时。`, 'info'); return deadlineAt; } async function getOAuthFlowRemainingMs(options = {}) { const step = Number(options.step) || 7; const actionLabel = String(options.actionLabel || '后续授权流程').trim() || '后续授权流程'; const state = options.state || await getState(); if (state?.oauthFlowTimeoutEnabled === false) { return null; } const deadlineAt = normalizeOAuthFlowDeadlineAt(state?.oauthFlowDeadlineAt); const deadlineSourceUrl = normalizeOAuthFlowSourceUrl(state?.oauthFlowDeadlineSourceUrl); const currentOauthUrl = normalizeOAuthFlowSourceUrl(options.oauthUrl !== undefined ? options.oauthUrl : state?.oauthUrl); if (!deadlineAt) { return null; } if (deadlineSourceUrl && currentOauthUrl && deadlineSourceUrl !== currentOauthUrl) { console.warn(LOG_PREFIX, '[oauth-flow] ignoring stale deadline due to oauth url mismatch', { step, actionLabel, deadlineSourceUrl, currentOauthUrl, }); return null; } const remainingMs = deadlineAt - Date.now(); if (remainingMs <= 0) { throw buildOAuthFlowTimeoutError(step, actionLabel, state); } return remainingMs; } async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) { const normalizedDefault = Math.max(1000, Number(defaultTimeoutMs) || 1000); const reserveMs = Math.max(0, Number(options.reserveMs) || 0); const remainingMs = await getOAuthFlowRemainingMs(options); if (remainingMs === null) { return normalizedDefault; } const budgetMs = remainingMs - reserveMs; if (budgetMs <= 0) { const stateForError = options.state || await getState(); throw buildOAuthFlowTimeoutError( Number(options.step) || 7, String(options.actionLabel || '后续授权流程').trim() || '后续授权流程', stateForError ); } return Math.max(1000, Math.min(normalizedDefault, budgetMs)); } function isStep6SuccessResult(result) { return result?.step6Outcome === 'success'; } function isStep6RecoverableResult(result) { return result?.step6Outcome === 'recoverable'; } function isAddPhoneAuthUrl(url) { return /https:\/\/auth\.openai\.com\/(?:add-phone|phone-verification)(?:[/?#]|$)/i.test(String(url || '').trim()); } function isAddPhoneAuthState(authState = {}) { return authState?.state === 'add_phone_page' || authState?.state === 'phone_verification_page' || Boolean(authState?.addPhonePage) || Boolean(authState?.phoneVerificationPage) || isAddPhoneAuthUrl(authState?.url); } async function getPostStep6AutoRestartDecision(step, error) { const resolveStepKey = (stepId, state) => { if (typeof getStepExecutionKeyForState === 'function') { return getStepExecutionKeyForState(stepId, state); } return String( typeof getStepDefinitionForState === 'function' ? (getStepDefinitionForState(stepId, state)?.key || '') : '' ).trim(); }; const findStepIdByKeyForState = (targetKey, state = {}) => { const normalizedKey = String(targetKey || '').trim(); if (!normalizedKey) { return null; } const stepIds = typeof getStepIdsForState === 'function' ? getStepIdsForState(state) : []; for (const stepId of stepIds) { if (resolveStepKey(stepId, state) === normalizedKey) { return Number(stepId); } } return null; }; const isPlatformVerifyTransientRetryError = (errorMessage = '') => { const normalizedMessage = String(errorMessage || ''); const mentionsTokenExchange = /auth\.openai\.com\/oauth\/token|token\s*exchange|token_exchange_user_error/i.test(normalizedMessage); const hasTransientNetworkSignal = /connect:\s*connection refused|failed to fetch|i\/o timeout|context deadline exceeded|eof|connection reset by peer/i.test(normalizedMessage); const hasTransientTokenExchangeSignal = /token_exchange_user_error|invalid request\.?\s*please try again later/i.test(normalizedMessage); return mentionsTokenExchange && (hasTransientNetworkSignal || hasTransientTokenExchangeSignal); }; const isPhoneVerificationLocalFailure = (errorMessage = '') => { const normalizedMessage = String(errorMessage || ''); if (isPhoneSmsPlatformRateLimitFailure(normalizedMessage)) { return false; } return /HeroSMS|phone verification did not succeed|number replacements|sms_timeout_after_resend|phone number is already linked|add-phone keeps rejecting current number|接码|手机号|手机验证码|步骤\s*9.*(?:手机号|验证码)|Step\s*9.*phone verification/i.test(normalizedMessage); }; const normalizedStep = Number(step); const errorMessage = getErrorMessage(error); const shouldForceRestartFromStep7 = /restart step 7 with a new number/i.test(errorMessage); const latestState = await getState(); const authChainStartStep = typeof getAuthChainStartStepId === 'function' ? getAuthChainStartStepId(latestState) : FINAL_OAUTH_CHAIN_START_STEP; const lastStepId = typeof getLastStepIdForState === 'function' ? getLastStepIdForState(latestState) : (typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10); const currentStepKey = resolveStepKey(normalizedStep, latestState); const confirmOauthStep = findStepIdByKeyForState('confirm-oauth', latestState); const shouldRetryFromConfirmStep = currentStepKey === 'platform-verify' && Number.isFinite(confirmOauthStep) && confirmOauthStep > 0 && confirmOauthStep < normalizedStep && isPlatformVerifyTransientRetryError(errorMessage); const restartAnchorStep = shouldRetryFromConfirmStep ? confirmOauthStep : authChainStartStep; if (isPhoneSmsPlatformRateLimitFailure(errorMessage)) { return { shouldRestart: false, blockedByAddPhone: false, forcedByPhoneVerificationTimeout: false, restartStep: authChainStartStep, errorMessage, authState: null, }; } if (!Number.isFinite(normalizedStep) || normalizedStep < authChainStartStep || normalizedStep > lastStepId) { return { shouldRestart: false, blockedByAddPhone: false, forcedByPhoneVerificationTimeout: false, restartStep: authChainStartStep, errorMessage, authState: null, }; } if (isPhoneVerificationLocalFailure(errorMessage)) { return { shouldRestart: false, blockedByAddPhone: true, forcedByPhoneVerificationTimeout: false, restartStep: authChainStartStep, errorMessage, authState: null, }; } if (shouldForceRestartFromStep7) { return { shouldRestart: true, blockedByAddPhone: false, forcedByPhoneVerificationTimeout: true, restartStep: authChainStartStep, errorMessage, authState: null, }; } if (isAddPhoneAuthFailure(error) || isAddPhoneAuthUrl(errorMessage)) { return { shouldRestart: false, blockedByAddPhone: true, forcedByPhoneVerificationTimeout: false, restartStep: authChainStartStep, errorMessage, authState: null, }; } let authState = null; try { authState = await getLoginAuthStateFromContent({ logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 ${restartAnchorStep} 重开...`, }); } catch (inspectError) { console.warn(LOG_PREFIX, '[AutoRun] failed to inspect login auth state after post-step6 error', { step: normalizedStep, sourceError: errorMessage, inspectError: inspectError?.message || inspectError, }); } if (isAddPhoneAuthState(authState) && !isPhoneSmsPlatformRateLimitFailure(errorMessage)) { return { shouldRestart: false, blockedByAddPhone: true, forcedByPhoneVerificationTimeout: false, restartStep: authChainStartStep, errorMessage, authState, }; } return { shouldRestart: true, blockedByAddPhone: false, forcedByPhoneVerificationTimeout: false, restartStep: restartAnchorStep, errorMessage, authState, }; } async function getLoginAuthStateFromContent(options = {}) { const { logMessage = '步骤 8:认证页正在切换,等待页面重新就绪后继续确认验证码页状态...' } = options; const result = await sendToContentScriptResilient( 'signup-page', { type: 'GET_LOGIN_AUTH_STATE', source: 'background', payload: {}, }, { timeoutMs: options.timeoutMs ?? 15000, retryDelayMs: options.retryDelayMs ?? 600, responseTimeoutMs: options.responseTimeoutMs ?? (options.timeoutMs ?? 15000), logMessage, } ); if (result?.error) { throw new Error(result.error); } return result || {}; } async function ensureStep8VerificationPageReady(options = {}) { const visibleStep = Number(options.visibleStep) || 8; const authLoginStep = Number(options.authLoginStep) || (visibleStep >= 11 ? 10 : 7); const inspectState = async (overrides = {}) => getLoginAuthStateFromContent({ ...options, ...overrides, }); let pageState = await inspectState(); if (pageState.state === 'verification_page' || pageState.state === 'oauth_consent_page') { return pageState; } if (pageState.maxCheckAttemptsBlocked) { throw new Error(`${CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX}${CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE}`); } if (pageState.state === 'login_timeout_error_page') { let recovered = false; try { const recoverPayload = { flow: 'login', logLabel: `步骤 ${visibleStep}:检测到登录超时报错,正在点击“重试”恢复当前页面`, step: visibleStep, timeoutMs: 12000, }; const recoverMessage = { type: 'RECOVER_AUTH_RETRY_PAGE', source: 'background', payload: recoverPayload, }; let recoverResult = null; const recoverTimeoutMs = 15000; if (typeof sendToContentScriptResilient === 'function') { recoverResult = await sendToContentScriptResilient( 'signup-page', recoverMessage, { timeoutMs: recoverTimeoutMs, responseTimeoutMs: recoverTimeoutMs, retryDelayMs: 700, logMessage: `步骤 ${visibleStep}:认证页进入重试/超时报错状态,正在尝试点击“重试”恢复...`, } ); } else if (typeof sendToContentScript === 'function') { recoverResult = await sendToContentScript('signup-page', recoverMessage, { responseTimeoutMs: recoverTimeoutMs, }); } if (recoverResult?.error) { throw new Error(recoverResult.error); } recovered = Boolean(recoverResult?.recovered || Number(recoverResult?.clickCount) > 0); if (recovered && typeof addLog === 'function') { await addLog(`步骤 ${visibleStep}:认证页已点击“重试”,正在重新确认验证码页状态...`, 'warn'); } } catch (recoverError) { const recoverMessage = getErrorMessage(recoverError); if (/^CF_SECURITY_BLOCKED::/i.test(recoverMessage)) { throw recoverError; } if (typeof addLog === 'function') { await addLog(`步骤 ${visibleStep}:认证页“重试”恢复失败:${recoverMessage}`, 'warn'); } } if (recovered) { pageState = await inspectState({ timeoutMs: 10000, responseTimeoutMs: 10000, retryDelayMs: 500, logMessage: `步骤 ${visibleStep}:认证页恢复后,正在确认验证码页是否可继续...`, }); if (pageState.state === 'verification_page' || pageState.state === 'oauth_consent_page') { return pageState; } if (pageState.maxCheckAttemptsBlocked) { throw new Error(`${CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX}${CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE}`); } if (pageState.state === 'add_phone_page' || pageState.state === 'phone_verification_page') { const urlPart = pageState.url ? ` URL: ${pageState.url}` : ''; throw new Error(`步骤 ${visibleStep}:当前认证页进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim()); } } const urlPart = pageState.url ? ` URL: ${pageState.url}` : ''; throw new Error(`STEP8_RESTART_STEP7::步骤 ${visibleStep}:当前认证页进入登录超时报错页,请回到步骤 ${authLoginStep} 重新开始。${urlPart}`.trim()); } if (pageState.state === 'add_phone_page' || pageState.state === 'phone_verification_page') { const urlPart = pageState.url ? ` URL: ${pageState.url}` : ''; throw new Error(`步骤 ${visibleStep}:当前认证页进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim()); } const stateLabel = getLoginAuthStateLabel(pageState.state); const urlPart = pageState.url ? ` URL: ${pageState.url}` : ''; throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 ${authLoginStep}。当前状态:${stateLabel}.${urlPart}`.trim()); } async function rerunStep7ForStep8Recovery(options = {}) { const { logMessage = '步骤 8:正在回到步骤 7,重新发起登录验证码流程...', postStepDelayMs = 3000, } = options; throwIfStopped(); const initialState = await getState(); const authLoginStep = typeof getAuthChainStartStepId === 'function' ? getAuthChainStartStepId(initialState) : FINAL_OAUTH_CHAIN_START_STEP; await addLog(logMessage, 'warn'); await setStepStatus(authLoginStep, 'running'); await addLog(`步骤 ${authLoginStep} 开始执行`); try { await step7Executor.executeStep7({ ...initialState, visibleStep: authLoginStep, }); } catch (err) { const latestState = await getState(); if (isStopError(err)) { await setStepStatus(authLoginStep, 'stopped'); await addLog(`步骤 ${authLoginStep} 已被用户停止`, 'warn'); await appendManualAccountRunRecordIfNeeded(`step${authLoginStep}_stopped`, latestState, getErrorMessage(err)); throw err; } if (isTerminalSecurityBlockedError(err)) { await handleCloudflareSecurityBlocked(err); throw new Error(STOP_ERROR_MESSAGE); } await setStepStatus(authLoginStep, 'failed'); await addLog(`步骤 ${authLoginStep} 失败:${getErrorMessage(err)}`, 'error'); await appendManualAccountRunRecordIfNeeded(`step${authLoginStep}_failed`, latestState, getErrorMessage(err)); throw err; } if (postStepDelayMs > 0) { await sleepWithStop(postStepDelayMs); } } async function executeStep6() { return step6Executor.executeStep6(); } // ============================================================ // Step 7: Refresh OAuth and log in // ============================================================ async function executeStep7(state) { return step7Executor.executeStep7(state); } // ============================================================ // Step 8: Poll login verification mail and submit the login code // ============================================================ async function executeStep8(state) { return step8Executor.executeStep8(state); } // ============================================================ // Step 9: 完成 OAuth(自动点击 + localhost 回调监听) // ============================================================ let webNavListener = null; let webNavCommittedListener = null; let step8TabUpdatedListener = null; let step8PendingReject = null; const STEP8_CLICK_EFFECT_TIMEOUT_MS = 15000; const STEP8_CLICK_RETRY_DELAY_MS = 500; const STEP8_READY_WAIT_TIMEOUT_MS = 180000; const STEP8_MAX_ROUNDS = 5; const STEP8_STRATEGIES = [ { mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' }, { mode: 'debugger', label: 'debugger click' }, { mode: 'content', strategy: 'nativeClick', label: 'element.click' }, { mode: 'content', strategy: 'dispatchClick', label: 'dispatch click' }, { mode: 'debugger', label: 'debugger click retry' }, ]; 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; } function cleanupStep8NavigationListeners() { if (webNavListener) { chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener); webNavListener = null; } if (webNavCommittedListener) { chrome.webNavigation.onCommitted.removeListener(webNavCommittedListener); webNavCommittedListener = null; } if (step8TabUpdatedListener) { chrome.tabs.onUpdated.removeListener(step8TabUpdatedListener); step8TabUpdatedListener = null; } } function rejectPendingStep8(error) { if (!step8PendingReject) return; const reject = step8PendingReject; step8PendingReject = null; reject(error); } function throwIfStep8SettledOrStopped(isSettled = false) { if (isSettled || stopRequested) { throw new Error(STOP_ERROR_MESSAGE); } } async function ensureStep8SignupPageReady(tabId, options = {}) { await ensureContentScriptReadyOnTab('signup-page', tabId, { inject: SIGNUP_PAGE_INJECT_FILES, injectSource: 'signup-page', timeoutMs: options.timeoutMs ?? 15000, retryDelayMs: options.retryDelayMs ?? 600, logMessage: options.logMessage || '', }); } async function getStep8PageState(tabId, responseTimeoutMs = 1500) { try { const result = await sendTabMessageWithTimeout(tabId, 'signup-page', { type: 'STEP8_GET_STATE', source: 'background', payload: {}, }, responseTimeoutMs); if (result?.error) { throw new Error(result.error); } return result; } catch (err) { if (isRetryableContentScriptTransportError(err)) { return null; } throw err; } } async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) { const start = Date.now(); let recovered = false; while (Date.now() - start < timeoutMs) { throwIfStopped(); const pageState = await getStep8PageState(tabId); if (pageState?.maxCheckAttemptsBlocked) { throw new Error(`${CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX}${CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE}`); } if (pageState?.addPhonePage || pageState?.phoneVerificationPage) { const latestState = await getState(); if (!Boolean(latestState?.phoneVerificationEnabled)) { const urlPart = pageState?.url ? ` URL: ${pageState.url}` : ''; throw new Error( pageState?.phoneVerificationPage ? `步骤 9:当前认证页进入手机验证码页,但未开启接码功能,无法继续自动授权。${urlPart}`.trim() : `步骤 9:当前认证页进入手机号页面,但未开启接码功能,无法继续自动授权。${urlPart}`.trim() ); } await phoneVerificationHelpers.completePhoneVerificationFlow(tabId, pageState); recovered = false; await sleepWithStop(250); continue; } if (pageState?.addPhonePage) { throw new Error('步骤 9:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。'); } if (pageState?.retryPage) { const retryUrl = String(pageState?.url || '').trim(); const consentLikeRetry = Boolean( pageState?.consentReady || pageState?.consentPage || /\/sign-in-with-chatgpt\/[^/?#]+\/consent(?:[/?#]|$)/i.test(retryUrl) ); if (!consentLikeRetry) { throw new Error(`步骤 9:当前认证页已进入重试页,当前流程将直接报错。URL: ${pageState.url || 'unknown'}`); } } if (pageState?.consentReady) { return pageState; } if (pageState === null && !recovered) { recovered = true; await ensureStep8SignupPageReady(tabId, { timeoutMs: Math.min(10000, timeoutMs), logMessage: '步骤 9:认证页内容脚本已失联,正在等待页面重新就绪...', }); continue; } recovered = false; await sleepWithStop(250); } throw new Error('步骤 9:长时间未进入 OAuth 同意页,无法定位“继续”按钮。'); } async function prepareStep8DebuggerClick(tabId, options = {}) { const timeoutMs = options.timeoutMs ?? 15000; const responseTimeoutMs = options.responseTimeoutMs ?? timeoutMs; await ensureStep8SignupPageReady(tabId, { timeoutMs, logMessage: '步骤 9:认证页内容脚本已失联,正在恢复后继续定位按钮...', }); const result = await sendToContentScriptResilient('signup-page', { type: 'STEP8_FIND_AND_CLICK', source: 'background', payload: {}, }, { timeoutMs, responseTimeoutMs, retryDelayMs: 600, logMessage: '步骤 9:认证页正在切换,等待 OAuth 同意页按钮重新就绪...', }); if (result?.error) { throw new Error(result.error); } return result; } async function triggerStep8ContentStrategy(tabId, strategy, options = {}) { const timeoutMs = options.timeoutMs ?? 15000; const responseTimeoutMs = options.responseTimeoutMs ?? timeoutMs; await ensureStep8SignupPageReady(tabId, { timeoutMs, logMessage: '步骤 9:认证页内容脚本已失联,正在恢复后继续点击“继续”按钮...', }); const result = await sendToContentScriptResilient('signup-page', { type: 'STEP8_TRIGGER_CONTINUE', source: 'background', payload: { strategy, findTimeoutMs: 4000, enabledTimeoutMs: 3000, }, }, { timeoutMs, responseTimeoutMs, retryDelayMs: 600, logMessage: '步骤 9:认证页正在切换,等待“继续”按钮重新就绪...', }); if (result?.error) { throw new Error(result.error); } return result; } async function recoverAuthRetryPageOnTab(tabId, payload = {}, options = {}) { const readyTimeoutMs = options.readyTimeoutMs ?? 15000; const timeoutMs = options.timeoutMs ?? 15000; const responseTimeoutMs = options.responseTimeoutMs ?? timeoutMs; await ensureStep8SignupPageReady(tabId, { timeoutMs: readyTimeoutMs, retryDelayMs: options.retryDelayMs ?? 600, logMessage: options.readyLogMessage || '步骤 9:认证页内容脚本已失联,正在恢复后继续处理重试页...', }); const result = await sendToContentScriptResilient('signup-page', { type: 'RECOVER_AUTH_RETRY_PAGE', source: 'background', payload, }, { timeoutMs, responseTimeoutMs, retryDelayMs: options.retryDelayMs ?? 600, logMessage: options.logMessage || '步骤 9:认证页正在切换,等待“重试”按钮重新就绪...', }); if (result?.error) { throw new Error(result.error); } return result; } async function reloadStep8ConsentPage(tabId, timeoutMs = 30000) { if (!Number.isInteger(tabId)) { throw new Error('步骤 9:缺少有效的认证页标签页,无法刷新后重试。'); } await chrome.tabs.update(tabId, { active: true }).catch(() => { }); await new Promise((resolve, reject) => { let settled = false; const timer = setTimeout(() => { if (settled) return; settled = true; chrome.tabs.onUpdated.removeListener(listener); reject(new Error('步骤 9:刷新认证页后等待页面完成加载超时。')); }, timeoutMs); const listener = (updatedTabId, changeInfo) => { if (updatedTabId !== tabId) return; if (changeInfo.status !== 'complete') return; if (settled) return; settled = true; clearTimeout(timer); chrome.tabs.onUpdated.removeListener(listener); resolve(); }; chrome.tabs.onUpdated.addListener(listener); chrome.tabs.reload(tabId, { bypassCache: false }).catch((err) => { if (settled) return; settled = true; clearTimeout(timer); chrome.tabs.onUpdated.removeListener(listener); reject(err); }); }); await ensureStep8SignupPageReady(tabId, { timeoutMs: Math.min(15000, timeoutMs), logMessage: '步骤 9:认证页刷新后内容脚本尚未就绪,正在等待页面恢复...', }); } async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLICK_EFFECT_TIMEOUT_MS) { const start = Date.now(); let recovered = false; while (Date.now() - start < timeoutMs) { throwIfStopped(); const tab = await chrome.tabs.get(tabId).catch(() => null); if (!tab) { throw new Error('步骤 9:认证页面标签页已关闭,无法继续自动授权。'); } if (baselineUrl && typeof tab.url === 'string' && tab.url !== baselineUrl) { return { progressed: true, reason: 'url_changed', url: tab.url }; } const pageState = await getStep8PageState(tabId); if (pageState?.maxCheckAttemptsBlocked) { throw new Error(`${CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX}${CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE}`); } if (pageState?.addPhonePage) { throw new Error('步骤 9:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。'); } if (pageState?.retryPage) { const retryUrl = String(pageState?.url || baselineUrl || '').trim(); const consentLikeRetry = Boolean( pageState?.consentReady || pageState?.consentPage || /\/sign-in-with-chatgpt\/[^/?#]+\/consent(?:[/?#]|$)/i.test(retryUrl) ); if (!consentLikeRetry) { throw new Error(`步骤 9:点击“继续”后页面进入认证页重试页,当前流程将直接报错。URL: ${pageState.url || baselineUrl || 'unknown'}`); } } if (pageState === null) { if (!recovered) { recovered = true; await ensureStep8SignupPageReady(tabId, { timeoutMs: Math.max(1000, Math.min(8000, timeoutMs)), logMessage: '步骤 9:点击后认证页正在重载,正在等待内容脚本重新就绪...', }).catch(() => null); continue; } await sleepWithStop(200); continue; } recovered = false; if (pageState?.consentPage === false && !pageState?.verificationPage) { return { progressed: true, reason: 'left_consent_page', url: pageState.url || baselineUrl || '', }; } await sleepWithStop(200); } return { progressed: false, reason: 'no_effect' }; } function getStep8EffectLabel(effect) { switch (effect?.reason) { case 'url_changed': return `URL 已变化:${effect.url}`; case 'page_reloading': return '页面正在跳转或重载'; case 'left_consent_page': return `页面已离开 OAuth 同意页:${effect.url || 'unknown'}`; default: return '页面仍停留在 OAuth 同意页'; } } 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({ 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, }); async function executeStep9(state) { return step9Executor.executeStep9(state); } // ============================================================ // Step 10: 平台回调验证 // ============================================================ async function executeContributionStep10(state) { if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。'); } if (!state.localhostUrl) { throw new Error('缺少 localhost 回调地址,请先完成步骤 9。'); } if (!state.contributionSessionId) { throw new Error('缺少贡献会话信息,请重新从步骤 7 开始。'); } if (!contributionOAuthManager?.pollContributionStatus) { throw new Error('贡献 OAuth 流程尚未接入,无法完成贡献模式的步骤 10。'); } await addLog('步骤 10:贡献模式正在提交回调并等待最终结果...'); let latestState = await getState(); const callbackUrl = latestState.localhostUrl || state.localhostUrl; if (!latestState.contributionCallbackUrl && contributionOAuthManager?.handleCapturedCallback) { latestState = await contributionOAuthManager.handleCapturedCallback(callbackUrl, { source: 'step10', }); } else { latestState = await contributionOAuthManager.pollContributionStatus({ reason: 'step10_initial', stateOverride: latestState, }); } const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(120000, { step: 10, actionLabel: '贡献流程最终结果', }) : 120000; const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { const status = String(latestState.contributionStatus || '').trim().toLowerCase(); if (contributionOAuthManager?.isContributionFinalStatus?.(status)) { if (status === 'auto_approved') { await addLog(`步骤 10:贡献流程已结束,最终状态:${latestState.contributionStatusMessage || status}`, 'ok'); await completeStepFromBackground(10, { contributionStatus: status, contributionStatusMessage: latestState.contributionStatusMessage || '', localhostUrl: callbackUrl, }); return; } throw new Error(latestState.contributionStatusMessage || '贡献流程失败。'); } await sleepWithStop(2500); latestState = await contributionOAuthManager.pollContributionStatus({ reason: 'step10_wait_final', stateOverride: latestState, }); } throw new Error('步骤 10:等待贡献流程最终结果超时。'); } async function executeStep10(state) { if (state?.contributionModeExpected && !state?.contributionMode) { throw new Error('步骤 10:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 提交。请重新进入贡献模式后再点击自动。'); } if (state?.contributionMode) { return executeContributionStep10(state); } return step10Executor.executeStep10(state); } // ============================================================ // Open Side Panel on extension icon click // ============================================================ chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === AUTO_RUN_TIMER_ALARM_NAME) { launchAutoRunTimerPlan('alarm').catch((err) => { console.error(LOG_PREFIX, 'Failed to resume auto run from timer alarm:', err); }); return; } if (alarm.name === IP_PROXY_AUTO_SYNC_ALARM_NAME) { runIpProxyAutoSync('alarm').catch((err) => { console.error(LOG_PREFIX, 'Failed to run IP proxy auto sync alarm:', err); }); } }); chrome.runtime.onStartup.addListener(() => { restoreAutoRunTimerIfNeeded().catch((err) => { console.error(LOG_PREFIX, 'Failed to restore auto run timer on startup:', err); }); if (IP_PROXY_INIT_AUTO_APPLY) { ensureIpProxySettingsAppliedFromCurrentState({ skipExitProbe: !IP_PROXY_INIT_ENABLE_EXIT_PROBE, suppressAuthRebind: IP_PROXY_INIT_SUPPRESS_AUTH_REBIND, }).catch((err) => { console.error(LOG_PREFIX, 'Failed to restore IP proxy settings on startup:', err); }); } ensureIpProxyAutoSyncAlarm().catch((err) => { console.error(LOG_PREFIX, 'Failed to restore IP proxy auto sync alarm on startup:', err); }); }); chrome.runtime.onInstalled.addListener(() => { restoreAutoRunTimerIfNeeded().catch((err) => { console.error(LOG_PREFIX, 'Failed to restore auto run timer on install/update:', err); }); if (IP_PROXY_INIT_AUTO_APPLY) { ensureIpProxySettingsAppliedFromCurrentState({ skipExitProbe: !IP_PROXY_INIT_ENABLE_EXIT_PROBE, suppressAuthRebind: IP_PROXY_INIT_SUPPRESS_AUTH_REBIND, }).catch((err) => { console.error(LOG_PREFIX, 'Failed to restore IP proxy settings on install/update:', err); }); } ensureIpProxyAutoSyncAlarm().catch((err) => { console.error(LOG_PREFIX, 'Failed to restore IP proxy auto sync alarm on install/update:', err); }); }); restoreAutoRunTimerIfNeeded().catch((err) => { console.error(LOG_PREFIX, 'Failed to restore auto run timer:', err); }); if (IP_PROXY_INIT_AUTO_APPLY) { ensureIpProxySettingsAppliedFromCurrentState({ skipExitProbe: !IP_PROXY_INIT_ENABLE_EXIT_PROBE, suppressAuthRebind: IP_PROXY_INIT_SUPPRESS_AUTH_REBIND, }).catch((err) => { console.error(LOG_PREFIX, 'Failed to restore IP proxy settings:', err); }); } ensureIpProxyAutoSyncAlarm().catch((err) => { console.error(LOG_PREFIX, 'Failed to restore IP proxy auto sync alarm:', err); });