From f9f8df19d3fa5fcc6622edf27e57132b383934f6 Mon Sep 17 00:00:00 2001 From: cnxwzy Date: Mon, 27 Apr 2026 19:13:22 +0800 Subject: [PATCH] fix: harden CPA auth flow with API callback/state checks Move CPA oauth url/callback handling to management API first, persist state/origin runtime keys, and fail fast on invalid callback or missing management auth to reduce flaky panel-driven auth failures. --- README.md | 11 +- background.js | 1992 ++--------------- background/contribution-oauth.js | 165 +- background/message-router.js | 215 +- background/panel-bridge.js | 145 +- background/steps/confirm-oauth.js | 49 +- background/steps/oauth-login.js | 36 +- background/steps/platform-verify.js | 198 +- tests/background-cpa-state-plumbing.test.js | 39 + tests/background-panel-bridge-module.test.js | 45 + ...background-platform-verify-cpa-api.test.js | 239 ++ 项目完整链路说明.md | 71 +- 12 files changed, 911 insertions(+), 2294 deletions(-) create mode 100644 tests/background-cpa-state-plumbing.test.js create mode 100644 tests/background-platform-verify-cpa-api.test.js diff --git a/README.md b/README.md index 122562e..7cf5f51 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,12 @@ 一百五十个号,一个401: -进官网,获取最新交流群: - - + @@ -74,7 +75,6 @@ - 至少准备一种验证码接收方式: - DuckDuckGo `@duck.com` + QQ / 163 / Inbucket 转发 - Cloudflare 自定义域邮箱前缀 + QQ / 163 / Inbucket 转发 - - iCloud Hide My Email,可选择直接从 iCloud 收件箱收码,或转发到 QQ / 163 / 163 VIP / 126 / Gmail 后收码 - 手动填写一个可收信邮箱 - 如果使用 `QQ` / `163` / `163 VIP` / `126` / `Inbucket`,对应页面需要提前能正常打开 @@ -592,7 +592,6 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 支持: - `Hotmail`(远程服务 / 本地助手) -- `iCloud` 收件箱,或 iCloud Hide My Email 转发到 QQ / 163 / 163 VIP / 126 / Gmail 后收码 - `content/qq-mail.js` - `content/mail-163.js`(163 / 163 VIP / 126) - `content/inbucket-mail.js` @@ -774,7 +773,7 @@ content/utils.js 通用工具:等待元素、点击、日志、停 content/vps-panel.js CPA 面板步骤:内部 OAuth 刷新 / Step 10 content/signup-page.js ChatGPT 官网 + OpenAI 注册/登录页步骤:Step 1 / 2 / 3 / 5 / 7 / 9 hotmail-utils.js Hotmail 收信相关通用辅助 -mail-provider-utils.js 网页邮箱 provider 与 iCloud 转发收码配置辅助 +mail-provider-utils.js 网页邮箱 provider 配置辅助 content/duck-mail.js Duck 邮箱自动获取 content/qq-mail.js QQ 邮箱验证码轮询 content/mail-163.js 163 / 163 VIP / 126 邮箱验证码轮询 diff --git a/background.js b/background.js index 96ce33c..fc3a046 100644 --- a/background.js +++ b/background.js @@ -18,17 +18,12 @@ importScripts( '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/paypal-approve.js', - 'background/steps/plus-return-confirm.js', 'background/steps/oauth-login.js', 'background/steps/fetch-login-code.js', 'background/steps/confirm-oauth.js', @@ -39,32 +34,15 @@ importScripts( '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_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled: true }) || NORMAL_STEP_DEFINITIONS; -const ALL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getAllSteps?.() || [ - ...NORMAL_STEP_DEFINITIONS, - ...PLUS_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 +const SHARED_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.() || []; +const STEP_IDS = SHARED_STEP_DEFINITIONS .map((definition) => Number(definition?.id)) .filter(Number.isFinite) .sort((left, right) => left - right); -const PLUS_STEP_IDS = PLUS_STEP_DEFINITIONS - .map((definition) => Number(definition?.id)) - .filter(Number.isFinite) - .sort((left, right) => left - right); -const LAST_STEP_ID = Math.max( - NORMAL_STEP_IDS[NORMAL_STEP_IDS.length - 1] || 10, - PLUS_STEP_IDS[PLUS_STEP_IDS.length - 1] || 10 -); +const LAST_STEP_ID = STEP_IDS[STEP_IDS.length - 1] || 10; const FINAL_OAUTH_CHAIN_START_STEP = 7; const { @@ -142,16 +120,10 @@ const { getIcloudSetupUrlForHost, normalizeBooleanMap, normalizeIcloudAliasList, - normalizeIcloudAliasRecord, normalizeIcloudHost, pickReusableIcloudAlias, toNormalizedEmailSet, } = self.IcloudUtils; -const { - getIcloudForwardMailConfig: getSharedIcloudForwardMailConfig, - normalizeIcloudForwardMailProvider, - normalizeIcloudTargetMailboxType, -} = self.MailProviderUtils; const { isRecoverableStep9AuthFailure, } = self.MultiPageActivationUtils; @@ -166,22 +138,6 @@ const ICLOUD_LOGIN_URLS = [ 'https://www.icloud.com.cn/', 'https://www.icloud.com/', ]; -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'; @@ -206,10 +162,6 @@ 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 AUTO_RUN_TIMER_ALARM_NAME = 'auto-run-timer'; const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start'; @@ -244,18 +196,11 @@ const HERO_SMS_COUNTRY_ID = 52; const HERO_SMS_COUNTRY_LABEL = 'Thailand'; const DISPLAY_TIMEZONE = 'Asia/Shanghai'; const MICROSOFT_TOKEN_DNR_RULE_ID = 1001; -const PERSISTENT_ALIAS_STATE_KEYS = [ - 'manualAliasUsage', - 'preservedAliases', - 'icloudAliasCache', - 'icloudAliasCacheAt', -]; +const PERSISTENT_ALIAS_STATE_KEYS = ['manualAliasUsage', 'preservedAliases']; 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: '', @@ -273,66 +218,6 @@ const CONTRIBUTION_RUNTIME_DEFAULTS = self.MultiPageBackgroundContributionOAuth? const CONTRIBUTION_RUNTIME_KEYS = self.MultiPageBackgroundContributionOAuth?.RUNTIME_KEYS || Object.keys(CONTRIBUTION_RUNTIME_DEFAULTS); -function isPlusModeState(state = {}) { - return Boolean(state?.plusModeEnabled); -} - -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 = {}) { - return isPlusModeState(state) ? PLUS_STEP_DEFINITIONS : NORMAL_STEP_DEFINITIONS; -} - -function getStepIdsForState(state = {}) { - return isPlusModeState(state) ? PLUS_STEP_IDS : NORMAL_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(); @@ -379,9 +264,6 @@ const PERSISTED_SETTING_DEFAULTS = { codex2apiUrl: DEFAULT_CODEX2API_URL, codex2apiAdminKey: '', customPassword: '', - plusModeEnabled: false, - paypalEmail: '', - paypalPassword: '', autoRunSkipFailures: false, autoRunFallbackThreadIntervalMinutes: 0, autoRunDelayEnabled: false, @@ -397,8 +279,6 @@ const PERSISTED_SETTING_DEFAULTS = { customEmailPool: [], autoDeleteUsedIcloudAlias: false, icloudHostPreference: 'auto', - icloudTargetMailboxType: 'icloud-inbox', - icloudForwardMailProvider: 'qq', icloudFetchMode: 'reuse_existing', accountRunHistoryTextEnabled: true, accountRunHistoryHelperBaseUrl: DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL, @@ -459,12 +339,12 @@ const DEFAULT_STATE = { 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。 @@ -472,14 +352,6 @@ const DEFAULT_STATE = { 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, - plusReturnUrl: '', flowStartTime: null, // 当前流程开始时间。 tabRegistry: {}, // 程序维护的标签页注册表。 sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。 @@ -1103,14 +975,9 @@ function normalizePersistentSettingValue(key, value) { return String(value || '').trim(); case 'customPassword': return String(value || ''); - case 'paypalEmail': - return String(value || '').trim(); - case 'paypalPassword': - return String(value || ''); case 'autoRunSkipFailures': case 'autoRunDelayEnabled': case 'phoneVerificationEnabled': - case 'plusModeEnabled': return Boolean(value); case 'autoRunFallbackThreadIntervalMinutes': return normalizeAutoRunFallbackThreadIntervalMinutes(value); @@ -1137,10 +1004,6 @@ function normalizePersistentSettingValue(key, value) { 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': @@ -1257,24 +1120,15 @@ async function getPersistedSettings() { 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), + manualAliasUsage: normalizeBooleanMap(stored.manualAliasUsage), + preservedAliases: normalizeBooleanMap(stored.preservedAliases), }; } catch (err) { console.warn(LOG_PREFIX, 'Failed to read persisted iCloud alias state:', err?.message || err); return { manualAliasUsage: {}, preservedAliases: {}, - icloudAliasCache: [], - icloudAliasCacheAt: 0, }; } } @@ -1286,7 +1140,7 @@ async function getState() { getPersistedAliasState(), accountRunHistoryHelpers?.getPersistedAccountRunHistory?.() || [], ]); - return { ...DEFAULT_STATE, ...persistedSettings, ...persistedAliasState, ...state, accountRunHistory }; + return { ...DEFAULT_STATE, ...persistedSettings, ...persistedAliasState, accountRunHistory, ...state }; } async function initializeSessionStorageAccess() { @@ -1313,12 +1167,6 @@ async function setState(updates) { 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); } @@ -1434,18 +1282,11 @@ function buildContributionModeState(enabled, persistedSettings = {}, currentStat } if (enabled) { - const routing = resolveContributionModeRoutingState({ - ...persistedSettings, - ...currentState, - ...currentContributionState, - }); return { ...currentContributionState, contributionMode: true, contributionModeExpected: true, - contributionSource: routing.source, - contributionTargetGroupName: routing.targetGroupName, - panelMode: routing.source, + panelMode: 'cpa', customPassword: '', accountRunHistoryTextEnabled: false, }; @@ -1468,7 +1309,14 @@ async function setContributionMode(enabled) { getState(), ]); - const updates = buildContributionModeState(normalizedEnabled, persistedSettings, currentState); + if (normalizedEnabled) { + await setPersistentSettings({ panelMode: 'cpa' }); + } + + const updates = buildContributionModeState(normalizedEnabled, { + ...persistedSettings, + ...(normalizedEnabled ? { panelMode: 'cpa' } : {}), + }, currentState); await setState(updates); const nextState = await getState(); @@ -1584,80 +1432,6 @@ 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) { @@ -1715,7 +1489,10 @@ async function resetState() { getPersistedSettings(), getPersistedAliasState(), ]); - const contributionModeState = buildContributionModeState(Boolean(prev.contributionMode), persistedSettings, prev); + const contributionModeState = buildContributionModeState(Boolean(prev.contributionMode), { + ...persistedSettings, + ...(prev.contributionMode ? { panelMode: 'cpa' } : {}), + }, prev); await chrome.storage.session.clear(); await chrome.storage.session.set({ ...DEFAULT_STATE, @@ -3547,7 +3324,10 @@ async function pollCloudflareTempEmailVerificationCode(step, state, pollPayload async function getOpenIcloudHostPreference() { try { const tabs = await chrome.tabs.query({ - url: ICLOUD_TAB_URL_PATTERNS, + url: [ + 'https://www.icloud.com/*', + 'https://www.icloud.com.cn/*', + ], }); const activeTab = tabs.find((tab) => tab.active); @@ -3570,9 +3350,9 @@ async function getPreferredIcloudLoginUrl(error = null, state = null) { return getIcloudLoginUrlForHost(configuredHost); } - const openHost = await getOpenIcloudHostPreference(); - if (openHost) { - return getIcloudLoginUrlForHost(openHost); + const messageHint = getIcloudHostHintFromMessage(getErrorMessage(error)); + if (messageHint) { + return getIcloudLoginUrlForHost(messageHint); } const savedHost = normalizeIcloudHost(currentState?.preferredIcloudHost); @@ -3580,9 +3360,9 @@ async function getPreferredIcloudLoginUrl(error = null, state = null) { return getIcloudLoginUrlForHost(savedHost); } - const messageHint = getIcloudHostHintFromMessage(getErrorMessage(error)); - if (messageHint) { - return getIcloudLoginUrlForHost(messageHint); + const openHost = await getOpenIcloudHostPreference(); + if (openHost) { + return getIcloudLoginUrlForHost(openHost); } return ICLOUD_LOGIN_URLS[0]; @@ -3603,111 +3383,35 @@ async function getPreferredIcloudSetupUrls(state = null, error = null) { 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'); + return message.includes('could not validate icloud session') + || message.includes('hide my email service was unavailable') + || /\bstatus (401|403|409|421)\b/.test(message); } 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, + url: [ + 'https://www.icloud.com/*', + 'https://www.icloud.com.cn/*', + ], }); const preferredHost = new URL(preferredUrl).host; - const preferredIcloudHost = normalizeIcloudHost(preferredHost); - const existingSameHost = tabs.find((tab) => { + const existing = tabs.find((tab) => { try { - return normalizeIcloudHost(new URL(tab.url).host) === preferredIcloudHost; + return new URL(tab.url).host === preferredHost; } 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; + if (existing?.id) { + await chrome.tabs.update(existing.id, { active: true }); + if (existing.url !== preferredUrl) { + await chrome.tabs.update(existing.id, { url: preferredUrl }); + } + return existing.id; } const created = await chrome.tabs.create({ url: preferredUrl, active: true }); @@ -3744,511 +3448,40 @@ async function promptIcloudLogin(error, actionLabel = 'iCloud 操作') { } async function withIcloudLoginHelp(actionLabel, action) { - 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(`${actionLabel}:retry:${attempt}/${maxTransientAttempts}`)) { - await addLog(`iCloud:${actionLabel}受网络/上下文波动影响,正在重试(${attempt}/${maxTransientAttempts})...`, 'warn'); - } - await new Promise((resolve) => setTimeout(resolve, retryDelayMs * attempt)); - continue; - } - if (shouldEmitIcloudTransientLog(`${actionLabel}:final`)) { - await addLog(`iCloud:${actionLabel}受网络/上下文波动影响:${getErrorMessage(err)}`, 'warn'); - } - const transientError = new Error('iCloud 别名加载受网络/上下文波动影响,请稍后重试。'); - transientError.code = 'ICLOUD_TRANSIENT_CONTEXT'; - 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; + return await action(); + } catch (err) { + if (isIcloudLoginRequiredError(err)) { + await promptIcloudLogin(err, actionLabel); + throw new Error('请先在新打开的 iCloud 页面中完成登录,再回来点击“我已登录”。'); } - 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; + throw err; } } -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 : []; - if (tabList.some((tab) => isIcloudMailPageUrl(tab?.url))) { - return tabList; - } - - const fallbackHost = targetHost - || preferredHost - || await getOpenIcloudHostPreference() - || 'icloud.com'; - const fallbackMailUrl = getIcloudMailUrlForHost(fallbackHost) || getIcloudMailUrlForHost('icloud.com'); - if (!fallbackMailUrl) { - return tabList; - } - - try { - 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 targetHost = normalizeIcloudHost(new URL(url).hostname); - const preferredHost = 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); - } + const { data } = options; + let response; + try { + response = await fetch(url, { + method, + credentials: 'include', + headers: data !== undefined ? { 'Content-Type': 'application/json' } : undefined, + body: data !== undefined ? JSON.stringify(data) : undefined, + }); + } catch (err) { + throw new Error(`iCloud 请求失败:${method} ${url},${err.message}`); } - throw lastError || new Error(`iCloud 请求失败:${method} ${requestUrl}`); + if (!response.ok) { + throw new Error(`iCloud 请求失败:${method} ${url},status ${response.status}`); + } + + try { + return await response.json(); + } catch (err) { + throw new Error(`iCloud 返回的 JSON 无法解析:${method} ${url},${err.message}`); + } } async function validateIcloudSession(setupUrl) { @@ -4259,133 +3492,6 @@ async function validateIcloudSession(setupUrl) { 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) { - 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 preferredHost = 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(); @@ -4406,35 +3512,13 @@ async function resolveIcloudPremiumMailService(options = {}) { } return { setupUrl, - serviceUrl: rememberIcloudServiceUrl(data.webservices.premiummailsettings.url), + serviceUrl: String(data.webservices.premiummailsettings.url || '').replace(/\/$/, ''), }; } catch (err) { errors.push(`${new URL(setupUrl).host}: ${getErrorMessage(err)}`); } } - if (shouldTryIcloudPageContextFallback(errors)) { - const { - service, - errors: pageContextErrors, - noTab: pageContextNoTab = false, - } = await resolveIcloudPremiumMailServiceViaPageContext(setupUrls, state); - 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. 请先在当前浏览器登录 icloud.com.cn 或 icloud.com。'); @@ -4456,99 +3540,16 @@ async function checkIcloudSession(options = {}) { }); } -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; - } + return withIcloudLoginHelp('加载 iCloud 隐私邮箱列表', async () => { + const { serviceUrl } = await resolveIcloudPremiumMailService(options); + const response = await icloudRequest('GET', `${serviceUrl}/v2/hme/list`); 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; - } + return normalizeIcloudAliasList(response, { + usedEmails: getEffectiveUsedEmails(state), + preservedEmails: getPreservedAliasMap(state), + }); + }); } async function deleteIcloudAlias(payload) { @@ -4567,18 +3568,7 @@ async function deleteIcloudAlias(payload) { 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'); - } + const { serviceUrl } = await resolveIcloudPremiumMailService(); try { const directDelete = await icloudRequest('POST', `${serviceUrl}/v1/hme/delete`, { @@ -4646,19 +3636,17 @@ async function fetchIcloudHideMyEmail(options = {}) { return withIcloudLoginHelp('获取 iCloud 隐私邮箱', async () => { throwIfStopped(); const generateNew = Boolean(options?.generateNew); - await addLog('iCloud:正在加载别名列表并校验当前浏览器登录状态...', 'info'); + await addLog('iCloud:正在校验当前浏览器登录状态...', 'info'); const { serviceUrl, setupUrl } = await resolveIcloudPremiumMailService(); 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) - ); + const existingAliasesResponse = await icloudRequest('GET', `${serviceUrl}/v2/hme/list`); + const state = await getState(); + const existingAliases = normalizeIcloudAliasList(existingAliasesResponse, { + usedEmails: getEffectiveUsedEmails(state), + preservedEmails: getPreservedAliasMap(state), + }); if (!generateNew) { const reusableAlias = pickReusableIcloudAlias(existingAliases); @@ -4673,142 +3661,29 @@ async function fetchIcloudHideMyEmail(options = {}) { } 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(); - 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, - }); - } + const generated = await icloudRequest('POST', `${serviceUrl}/v1/hme/generate`); + if (!generated?.success || !generated?.result?.hme) { + throw new Error(generated?.error?.errorMessage || 'iCloud 隐私邮箱生成失败。'); + } - 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, + const reserved = await icloudRequest('POST', `${serviceUrl}/v1/hme/reserve`, { + data: { + hme: generated.result.hme, 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(); - 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 页面已登录且网络可访问,再重试。` - ); + if (!reserved?.success || !reserved?.result?.hme?.hme) { + throw new Error(reserved?.error?.errorMessage || 'iCloud 隐私邮箱保留失败。'); } + + const alias = String(reserved.result.hme.hme || '').trim().toLowerCase(); + await setEmailState(alias); + await addLog(`iCloud:已创建并保留新别名 ${alias}`, 'ok'); + broadcastIcloudAliasesChanged({ reason: 'created', email: alias }); + return alias; }); } @@ -4856,11 +3731,7 @@ async function finalizeIcloudAliasAfterSuccessfulFlow(state) { 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'); - } + await addLog(`iCloud:自动删除 ${email} 失败:${getErrorMessage(err)}`, 'warn'); return { handled: true, deleted: false }; } } @@ -5099,21 +3970,6 @@ 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); } @@ -5122,94 +3978,10 @@ 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) // ============================================================ @@ -5232,21 +4004,6 @@ function sendTabMessageWithTimeout(tabId, source, message, responseTimeoutMs = g 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); } @@ -5340,8 +4097,6 @@ function getSourceLabel(source) { '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 || '未知来源'; } @@ -5504,6 +4259,7 @@ function getLoginAuthStateLabel(state) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.getLoginAuthStateLabel) { return loggingStatus.getLoginAuthStateLabel(state); } + state = state === 'oauth_consent_page' ? 'unknown' : state; switch (state) { case 'verification_page': return '登录验证码页'; case 'password_page': return '密码页'; @@ -5546,47 +4302,29 @@ 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) { +function getFirstUnfinishedStep(statuses = {}) { + if (typeof loggingStatus !== 'undefined' && loggingStatus?.getFirstUnfinishedStep) { + return loggingStatus.getFirstUnfinishedStep(statuses); + } + for (const step of STEP_IDS) { 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 hasSavedProgress(statuses = {}) { + if (typeof loggingStatus !== 'undefined' && loggingStatus?.hasSavedProgress) { + return loggingStatus.hasSavedProgress(statuses); + } + return Object.values({ ...DEFAULT_STATE.stepStatuses, ...statuses }).some((status) => status !== '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, - plusReturnUrl: '', - }; - +function getDownstreamStateResets(step) { if (step <= 1) { return { - ...plusRuntimeResets, oauthUrl: null, + cpaOAuthState: null, + cpaManagementOrigin: null, sub2apiSessionId: null, sub2apiOAuthState: null, sub2apiGroupId: null, @@ -5607,7 +4345,6 @@ function getDownstreamStateResets(step, state = {}) { } if (step === 2) { return { - ...plusRuntimeResets, password: null, lastEmailTimestamp: null, signupVerificationRequestedAt: null, @@ -5621,7 +4358,6 @@ function getDownstreamStateResets(step, state = {}) { } if (step === 3 || step === 4) { return { - ...plusRuntimeResets, lastEmailTimestamp: null, signupVerificationRequestedAt: null, loginVerificationRequestedAt: null, @@ -5634,17 +4370,6 @@ function getDownstreamStateResets(step, state = {}) { } if (step === 5 || step === 6 || step === 7 || step === 8) { return { - ...(step <= 6 ? plusRuntimeResets : {}), - ...(step === 7 ? { - plusBillingCountryText: '', - plusBillingAddress: null, - plusPaypalApprovedAt: null, - plusReturnUrl: '', - } : {}), - ...(step === 8 ? { - plusPaypalApprovedAt: null, - plusReturnUrl: '', - } : {}), lastLoginCode: null, loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, @@ -5653,21 +4378,6 @@ function getDownstreamStateResets(step, state = {}) { }; } if (step === 9) { - return { - plusReturnUrl: '', - localhostUrl: null, - }; - } - if (stepKey === 'oauth-login' || stepKey === 'fetch-login-code') { - return { - lastLoginCode: null, - loginVerificationRequestedAt: null, - oauthFlowDeadlineAt: null, - oauthFlowDeadlineSourceUrl: null, - localhostUrl: null, - }; - } - if (stepKey === 'confirm-oauth') { return { localhostUrl: null, }; @@ -5681,15 +4391,7 @@ async function invalidateDownstreamAfterStepRestart(step, options = {}) { 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; - } + for (let downstream = step + 1; downstream <= LAST_STEP_ID; downstream++) { if (statuses[downstream] !== 'pending') { statuses[downstream] = 'pending'; changedSteps.push(downstream); @@ -5707,7 +4409,7 @@ async function invalidateDownstreamAfterStepRestart(step, options = {}) { await addLog(`${logLabel},已重置后续步骤状态:${changedSteps.join(', ')}`, 'warn'); } - const resets = getDownstreamStateResets(step, state); + const resets = getDownstreamStateResets(step); if (Object.keys(resets).length) { await setState(resets); broadcastDataUpdate(resets); @@ -5718,26 +4420,22 @@ 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') +function getRunningSteps(statuses = {}) { + if (typeof loggingStatus !== 'undefined' && loggingStatus?.getRunningSteps) { + return loggingStatus.getRunningSteps(statuses); + } + return Object.entries({ ...DEFAULT_STATE.stepStatuses, ...statuses }) + .filter(([, status]) => status === 'running') + .map(([step]) => Number(step)) .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 stepIds = Object.keys(statuses) + .map((step) => Number(step)) + .filter(Number.isFinite) + .sort((left, right) => left - right); const runningSteps = stepIds.filter((step) => statuses[step] === 'running'); if (runningSteps.length) { @@ -6238,13 +4936,8 @@ async function ensureManualInteractionAllowed(actionLabel) { 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)) { + if (!Number.isInteger(step) || !STEP_IDS.includes(step)) { throw new Error(`无效步骤:${step}`); } @@ -6257,12 +4950,10 @@ async function skipStep(step) { throw new Error(`步骤 ${step} 已完成,无需再跳过。`); } - const currentIndex = activeStepIds.indexOf(step); - if (currentIndex > 0) { - const prevStep = activeStepIds[currentIndex - 1]; - const prevStatus = statuses[prevStep]; + if (step > 1) { + const prevStatus = statuses[step - 1]; if (!isStepDoneStatus(prevStatus)) { - throw new Error(`请先完成步骤 ${prevStep},再跳过步骤 ${step}。`); + throw new Error(`请先完成步骤 ${step - 1},再跳过步骤 ${step}。`); } } @@ -6425,6 +5116,8 @@ async function handleStepData(step, payload) { 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 (Object.keys(updates).length) { @@ -6533,28 +5226,7 @@ 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', - 'platform-verify', -]); -const AUTO_RUN_PRE_EXECUTION_DELAYS_BY_STEP_KEY = new Map([ - ['plus-checkout-create', 20000], -]); +const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 10]); function waitForStepComplete(step, timeoutMs = 120000) { return new Promise((resolve, reject) => { @@ -6576,37 +5248,10 @@ function waitForStepComplete(step, timeoutMs = 120000) { }); } -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); - } +function doesStepUseCompletionSignal(step) { 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 notifyStepComplete(step, payload) { const waiter = stepWaiters.get(step); console.log(LOG_PREFIX, `[notifyStepComplete] step ${step}, hasWaiter=${Boolean(waiter)}`); @@ -6619,19 +5264,6 @@ function notifyStepError(step, 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'); @@ -6640,22 +5272,13 @@ async function completeStepFromBackground(step, payload = {}) { 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; + const completionState = step === LAST_STEP_ID ? await getState() : 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 handleStepData(step, payload); + if (step === LAST_STEP_ID) { + await appendAndBroadcastAccountRunRecord('success', completionState); } - - await runCompletedStepSideEffects(step, payload, completionState, lastStepId); notifyStepComplete(step, payload); } @@ -6732,7 +5355,7 @@ async function executeStepViaCompletionSignal(step, timeoutMs = AUTO_RUN_SIGNAL_ async function waitForRunningStepsToFinish(payload = {}) { let currentState = await getState(); - let runningSteps = getRunningSteps(currentState.stepStatuses, currentState); + let runningSteps = getRunningSteps(currentState.stepStatuses); if (!runningSteps.length) { return currentState; } @@ -6743,35 +5366,23 @@ async function waitForRunningStepsToFinish(payload = {}) { while (runningSteps.length) { await sleepWithStop(250); currentState = await getState(); - runningSteps = getRunningSteps(currentState.stepStatuses, currentState); + runningSteps = getRunningSteps(currentState.stepStatuses); } 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', -]); +const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]); 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); - } +function isAuthChainStep(step) { return AUTH_CHAIN_STEP_IDS.has(Number(step)); } -async function acquireTopLevelAuthChainExecution(step, state = {}) { +async function acquireTopLevelAuthChainExecution(step) { const normalizedStep = Number(step); - if (!isAuthChainStep(normalizedStep, state)) { + if (!isAuthChainStep(normalizedStep)) { return { joined: false, release() {}, @@ -6817,7 +5428,7 @@ async function acquireTopLevelAuthChainExecution(step, state = {}) { async function markRunningStepsStopped() { const state = await getState(); - const runningSteps = getRunningSteps(state.stepStatuses, state); + const runningSteps = getRunningSteps(state.stepStatuses); for (const step of runningSteps) { await setStepStatus(step, 'stopped'); @@ -6827,7 +5438,7 @@ async function markRunningStepsStopped() { async function requestStop(options = {}) { const { logMessage = '已收到停止请求,正在取消当前操作...' } = options; const state = await getState(); - const runningSteps = getRunningSteps(state.stepStatuses, state); + const runningSteps = getRunningSteps(state.stepStatuses); const inferredStopStep = inferStoppedRecordStep(state); const timerPlan = getPendingAutoRunTimerPlan(state); @@ -6870,7 +5481,6 @@ async function requestStop(options = {}) { stopRequested = true; clearCurrentAutoRunSessionId(); cancelPendingCommands(); - abortActiveIcloudRequests(); cleanupStep8NavigationListeners(); rejectPendingStep8(new Error(STOP_ERROR_MESSAGE)); @@ -6912,8 +5522,7 @@ async function requestStop(options = {}) { 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); + const authChainClaim = await acquireTopLevelAuthChainExecution(step); if (authChainClaim.joined) { return; } @@ -6925,29 +5534,21 @@ async function executeStep(step, options = {}) { await addLog(`步骤 ${step} 开始执行`); await humanStepDelay(); - state = await getState(); + const 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}`); - } - await activeStepRegistry.executeStep(step, { - ...state, - visibleStep: Number(step), - stepDefinition: getStepDefinitionForState(step, state), - }); + await stepRegistry.executeStep(step, state); } catch (err) { executionError = err; - const errorState = await getState(); + const state = await getState(); if (isStopError(err)) { await setStepStatus(step, 'stopped'); await addLog(`步骤 ${step} 已被用户停止`, 'warn'); - await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, errorState, getErrorMessage(err)); + await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, state, getErrorMessage(err)); throw err; } if (isTerminalSecurityBlockedError(err)) { @@ -6958,10 +5559,10 @@ async function executeStep(step, options = {}) { await handleBrowserSwitchRequired(err); throw new Error(STOP_ERROR_MESSAGE); } - if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step, errorState) && isRetryableContentScriptTransportError(err))) { + if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step) && isRetryableContentScriptTransportError(err))) { await setStepStatus(step, 'failed'); await addLog(`步骤 ${step} 失败:${err.message}`, 'error'); - await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, errorState, getErrorMessage(err)); + await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, state, getErrorMessage(err)); } else { console.warn( LOG_PREFIX, @@ -6991,23 +5592,12 @@ async function executeStepAndWait(step, delayAfter = 2000) { 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)) { + if (AUTO_RUN_BACKGROUND_COMPLETED_STEPS.has(step)) { await addLog(`自动运行:步骤 ${step} 由后台流程负责收尾,执行函数返回后将直接进入下一步。`, 'info'); await executeStep(step); const latestState = await getState(); await addLog(`自动运行:步骤 ${step} 已执行返回,当前状态为 ${latestState.stepStatuses?.[step] || 'pending'},准备继续后续步骤。`, 'info'); - } else if (doesStepUseCompletionSignal(step, executionState)) { + } else if (doesStepUseCompletionSignal(step)) { await addLog(`自动运行:步骤 ${step} 已发起,正在等待完成信号(超时 ${AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS / 1000} 秒)。`, 'info'); await executeStepViaCompletionSignal(step, AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS); await addLog(`自动运行:步骤 ${step} 已收到完成信号,准备继续后续步骤。`, 'info'); @@ -7362,42 +5952,6 @@ async function resumeAutoRunIfWaitingForEmail(options = {}) { 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)) { @@ -7483,9 +6037,7 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { 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'); @@ -7502,17 +6054,16 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { } 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)) { + if ( + (generator === 'cloudflare' && /域名/.test(String(err.message || ''))) + || (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR && /(服务地址|Admin Auth|域名)/.test(String(err.message || ''))) + ) { break; } } } - const totalAttempts = Math.max(1, attemptedFetches); - await addLog(`${generatorLabel}自动获取已连续失败 ${totalAttempts} 次:${lastError?.message || '未知错误'}`, 'error'); + await addLog(`${generatorLabel}自动获取已连续失败 ${EMAIL_FETCH_MAX_ATTEMPTS} 次:${lastError?.message || '未知错误'}`, 'error'); await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先自动获取邮箱或手动粘贴邮箱,然后继续 ===`, 'warn'); await broadcastAutoRunStatus('waiting_email', { currentRun: targetRun, @@ -7635,9 +6186,7 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { 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'); @@ -7654,17 +6203,16 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { } 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)) { + if ( + (generator === 'cloudflare' && /域名/.test(String(err.message || ''))) + || (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR && /(服务地址|Admin Auth|域名)/.test(String(err.message || ''))) + ) { break; } } } - const totalAttempts = Math.max(1, attemptedFetches); - await addLog(`${generatorLabel}自动获取已连续失败 ${totalAttempts} 次:${lastError?.message || '未知错误'}`, 'error'); + await addLog(`${generatorLabel}自动获取已连续失败 ${EMAIL_FETCH_MAX_ATTEMPTS} 次:${lastError?.message || '未知错误'}`, 'error'); await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先自动获取邮箱或手动粘贴邮箱,然后继续 ===`, 'warn'); await broadcastAutoRunStatus('waiting_email', { currentRun: targetRun, @@ -7730,14 +6278,8 @@ async function runAutoSequenceFromStep(startStep, context = {}) { 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))) { + while (step <= LAST_STEP_ID) { 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'); @@ -7787,11 +6329,6 @@ async function runAutoSequenceFromStep(startStep, context = {}) { 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 @@ -7800,22 +6337,19 @@ async function runAutoSequenceFromStep(startStep, context = {}) { ? `当前认证页:${authStateLabel}` : '未获取到认证页状态'; await addLog( - `步骤 ${step}:检测到报错且当前未进入 add-phone,正在回到步骤 ${restartStep} 重新开始授权流程(第 ${postStep7RestartCount} 次重开)。${authStateSuffix};原因:${restartDecision.errorMessage || '未知错误'}`, + `步骤 ${step}:检测到报错且当前未进入 add-phone,正在回到步骤 7 重新开始授权流程(第 ${postStep7RestartCount} 次重开)。${authStateSuffix};原因:${restartDecision.errorMessage || '未知错误'}`, 'warn' ); - await invalidateDownstreamAfterStepRestart(resetAfterStep, { - logLabel: `步骤 ${step} 报错后准备回到步骤 ${restartStep} 重试(第 ${postStep7RestartCount} 次重开)`, + await invalidateDownstreamAfterStepRestart(6, { + logLabel: `步骤 ${step} 报错后准备回到步骤 7 重试(第 ${postStep7RestartCount} 次重开)`, }); - step = restartStep; + step = 7; 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'); + await addLog(`步骤 ${step}:检测到认证流程进入 add-phone(${addPhoneUrl}),停止自动回到步骤 7 重开。`, 'warn'); } throw err; } @@ -8142,55 +6676,6 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS, throwIfStopped, }); -const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.createPlusCheckoutCreateExecutor({ - addLog, - chrome, - completeStepFromBackground, - ensureContentScriptReadyOnTabUntilStopped, - registerTab, - sendTabMessageUntilStopped, - setState, - sleepWithStop, - 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, - sendTabMessageUntilStopped, - setState, - sleepWithStop, - waitForTabCompleteUntilStopped, - waitForTabUrlMatchUntilStopped, -}); -const payPalApproveExecutor = self.MultiPageBackgroundPayPalApprove?.createPayPalApproveExecutor({ - addLog, - chrome, - completeStepFromBackground, - ensureContentScriptReadyOnTabUntilStopped, - getTabId, - isTabAlive, - sendTabMessageUntilStopped, - setState, - sleepWithStop, - waitForTabCompleteUntilStopped, - waitForTabUrlMatchUntilStopped, -}); -const plusReturnConfirmExecutor = self.MultiPageBackgroundPlusReturnConfirm?.createPlusReturnConfirmExecutor({ - addLog, - completeStepFromBackground, - getTabId, - isTabAlive, - setState, - sleepWithStop, - waitForTabUrlMatchUntilStopped, -}); const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({ addLog, chrome, @@ -8210,6 +6695,7 @@ const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({ shouldBypassStep9ForLocalCpa, SUB2API_STEP9_RESPONSE_TIMEOUT_MS, }); +const stepDefinitions = SHARED_STEP_DEFINITIONS; const stepExecutorsByKey = { 'open-chatgpt': () => step1Executor.executeStep1(), 'submit-signup-email': (state) => step2Executor.executeStep2(state), @@ -8217,10 +6703,6 @@ const stepExecutorsByKey = { '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) => 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), @@ -8271,9 +6753,6 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter getPendingAutoRunTimerPlan, getSourceLabel, getState, - getStepDefinitionForState, - getStepIdsForState, - getLastStepIdForState, getTabId, getStopRequested: () => stopRequested, handleCloudflareSecurityBlocked, @@ -8331,22 +6810,12 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter upsertHotmailAccount, verifyHotmailAccount, }); - -function buildStepRegistry(definitions = []) { - return self.MultiPageBackgroundStepRegistry?.createStepRegistry( - definitions.map((definition) => ({ - ...definition, - execute: stepExecutorsByKey[definition.key], - })) - ); -} - -const normalStepRegistry = buildStepRegistry(NORMAL_STEP_DEFINITIONS); -const plusStepRegistry = buildStepRegistry(PLUS_STEP_DEFINITIONS); - -function getStepRegistryForState(state = {}) { - return isPlusModeState(state) ? plusStepRegistry : normalStepRegistry; -} +const stepRegistry = self.MultiPageBackgroundStepRegistry?.createStepRegistry( + stepDefinitions.map((definition) => ({ + ...definition, + execute: stepExecutorsByKey[definition.key], + })) +); async function requestOAuthUrlFromPanel(state, options = {}) { return panelBridge.requestOAuthUrlFromPanel(state, options); @@ -8424,17 +6893,6 @@ function getMailConfig(state) { 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 { @@ -8684,13 +7142,12 @@ async function runPreStep6CookieCleanup() { // 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; +async function refreshOAuthUrlBeforeStep6(state) { if (state?.contributionModeExpected && !state?.contributionMode) { - throw new Error(`步骤 ${visibleStep}:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入贡献模式后再点击自动。`); + throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入贡献模式后再点击自动。'); } if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) { - await addLog(`步骤 ${visibleStep}:contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...`, 'info'); + await addLog('步骤 7:contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...', 'info'); const contributionState = await contributionOAuthManager.startContributionFlow({ nickname: state.contributionNickname || '', openAuthTab: false, @@ -8703,9 +7160,9 @@ async function refreshOAuthUrlBeforeStep6(state, options = {}) { await handleStepData(1, { oauthUrl }); return oauthUrl; } - await addLog(`步骤 ${visibleStep}:contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info'); + await addLog(`步骤 7: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}` }); + const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: '步骤 7' }); await handleStepData(1, refreshResult); if (!refreshResult?.oauthUrl) { @@ -8715,12 +7172,9 @@ async function refreshOAuthUrlBeforeStep6(state, options = {}) { return refreshResult.oauthUrl; } -function buildOAuthFlowTimeoutError(step, actionLabel = '后续授权流程', state = {}) { - const restartStep = typeof getAuthChainStartStepId === 'function' - ? getAuthChainStartStepId(state) - : FINAL_OAUTH_CHAIN_START_STEP; +function buildOAuthFlowTimeoutError(step, actionLabel = '后续授权流程') { return new Error( - `步骤 ${step}:从拿到 OAuth 登录地址开始,${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟内未完成${actionLabel},结束当前链路,准备从步骤 ${restartStep} 重新开始。` + `步骤 ${step}:从拿到 OAuth 登录地址开始,${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟内未完成${actionLabel},结束当前链路,准备从步骤 7 重新开始。` ); } @@ -8771,7 +7225,7 @@ async function getOAuthFlowRemainingMs(options = {}) { const remainingMs = deadlineAt - Date.now(); if (remainingMs <= 0) { - throw buildOAuthFlowTimeoutError(step, actionLabel, state); + throw buildOAuthFlowTimeoutError(step, actionLabel); } return remainingMs; @@ -8787,11 +7241,9 @@ async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) { const budgetMs = remainingMs - reserveMs; if (budgetMs <= 0) { - const stateForError = options.state || await getState(); throw buildOAuthFlowTimeoutError( Number(options.step) || 7, - String(options.actionLabel || '后续授权流程').trim() || '后续授权流程', - stateForError + String(options.actionLabel || '后续授权流程').trim() || '后续授权流程' ); } @@ -8822,19 +7274,11 @@ async function getPostStep6AutoRestartDecision(step, error) { 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); - if (!Number.isFinite(normalizedStep) || normalizedStep < authChainStartStep || normalizedStep > lastStepId) { + if (!Number.isFinite(normalizedStep) || normalizedStep < 7 || normalizedStep > LAST_STEP_ID) { return { shouldRestart: false, blockedByAddPhone: false, forcedByPhoneVerificationTimeout: false, - restartStep: authChainStartStep, errorMessage, authState: null, }; @@ -8845,7 +7289,6 @@ async function getPostStep6AutoRestartDecision(step, error) { shouldRestart: true, blockedByAddPhone: false, forcedByPhoneVerificationTimeout: true, - restartStep: authChainStartStep, errorMessage, authState: null, }; @@ -8856,7 +7299,6 @@ async function getPostStep6AutoRestartDecision(step, error) { shouldRestart: false, blockedByAddPhone: true, forcedByPhoneVerificationTimeout: false, - restartStep: authChainStartStep, errorMessage, authState: null, }; @@ -8865,7 +7307,7 @@ async function getPostStep6AutoRestartDecision(step, error) { let authState = null; try { authState = await getLoginAuthStateFromContent({ - logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 ${authChainStartStep} 重开...`, + logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 7 重开...`, }); } catch (inspectError) { console.warn(LOG_PREFIX, '[AutoRun] failed to inspect login auth state after post-step6 error', { @@ -8880,7 +7322,6 @@ async function getPostStep6AutoRestartDecision(step, error) { shouldRestart: false, blockedByAddPhone: true, forcedByPhoneVerificationTimeout: false, - restartStep: authChainStartStep, errorMessage, authState, }; @@ -8890,7 +7331,6 @@ async function getPostStep6AutoRestartDecision(step, error) { shouldRestart: true, blockedByAddPhone: false, forcedByPhoneVerificationTimeout: false, - restartStep: authChainStartStep, errorMessage, authState, }; @@ -8921,8 +7361,6 @@ async function getLoginAuthStateFromContent(options = {}) { } async function ensureStep8VerificationPageReady(options = {}) { - const visibleStep = Number(options.visibleStep) || 8; - const authLoginStep = Number(options.authLoginStep) || (visibleStep >= 11 ? 10 : 7); const pageState = await getLoginAuthStateFromContent(options); if (pageState.state === 'verification_page') { return pageState; @@ -8934,17 +7372,17 @@ async function ensureStep8VerificationPageReady(options = {}) { if (pageState.state === 'login_timeout_error_page') { const urlPart = pageState.url ? ` URL: ${pageState.url}` : ''; - throw new Error(`STEP8_RESTART_STEP7::步骤 ${visibleStep}:当前认证页进入登录超时报错页,请回到步骤 ${authLoginStep} 重新开始。${urlPart}`.trim()); + throw new Error(`STEP8_RESTART_STEP7::步骤 8:当前认证页进入登录超时报错页,请回到步骤 7 重新开始。${urlPart}`.trim()); } if (pageState.state === 'add_phone_page') { const urlPart = pageState.url ? ` URL: ${pageState.url}` : ''; - throw new Error(`步骤 ${visibleStep}:当前认证页进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim()); + throw new Error(`步骤 8:当前认证页进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim()); } const stateLabel = getLoginAuthStateLabel(pageState.state); const urlPart = pageState.url ? ` URL: ${pageState.url}` : ''; - throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 ${authLoginStep}。当前状态:${stateLabel}.${urlPart}`.trim()); + throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 7。当前状态:${stateLabel}.${urlPart}`.trim()); } async function rerunStep7ForStep8Recovery(options = {}) { @@ -8955,33 +7393,27 @@ async function rerunStep7ForStep8Recovery(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} 开始执行`); + await setStepStatus(7, 'running'); + await addLog('步骤 7 开始执行'); try { - await step7Executor.executeStep7({ - ...initialState, - visibleStep: authLoginStep, - }); + await step7Executor.executeStep7(initialState); } 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)); + await setStepStatus(7, 'stopped'); + await addLog('步骤 7 已被用户停止', 'warn'); + await appendManualAccountRunRecordIfNeeded('step7_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)); + await setStepStatus(7, 'failed'); + await addLog(`步骤 7 失败:${getErrorMessage(err)}`, 'error'); + await appendManualAccountRunRecordIfNeeded('step7_failed', latestState, getErrorMessage(err)); throw err; } @@ -9437,8 +7869,8 @@ async function executeContributionStep10(state) { 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'); + if (status === 'auto_approved' || status === 'manual_review_required') { + await addLog(`步骤 10:贡献流程已结束,最终状态:${latestState.contributionStatusMessage || status}`, status === 'auto_approved' ? 'ok' : 'warn'); await completeStepFromBackground(10, { contributionStatus: status, contributionStatusMessage: latestState.contributionStatusMessage || '', diff --git a/background/contribution-oauth.js b/background/contribution-oauth.js index 81a1131..e95cbc3 100644 --- a/background/contribution-oauth.js +++ b/background/contribution-oauth.js @@ -3,19 +3,13 @@ })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundContributionOAuthModule() { const API_BASE_URL = 'https://apikey.qzz.io/oauth/api'; const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']); - const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'expired', 'error']); + const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'manual_review_required', 'expired', 'error']); const CALLBACK_FINAL_STATUSES = new Set(['submitted']); const CALLBACK_WAITING_STATUSES = new Set(['idle', 'waiting', 'captured', 'failed', 'submitting']); - 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 RUNTIME_DEFAULTS = { contributionMode: false, contributionModeExpected: false, - contributionSource: CONTRIBUTION_SOURCE_SUB2API, - contributionTargetGroupName: CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME, contributionNickname: '', contributionQq: '', contributionSessionId: '', @@ -44,7 +38,6 @@ } = deps; let listenersBound = false; - const inFlightCapturedCallbackTasks = new Map(); function normalizeString(value = '') { return String(value || '').trim(); @@ -71,6 +64,9 @@ case 'auto_rejected': case 'rejected': return 'auto_rejected'; + case 'manual_review_required': + case 'manual_review': + return 'manual_review_required'; case 'expired': case 'timeout': return 'expired'; @@ -111,63 +107,6 @@ return FINAL_STATUSES.has(normalizeContributionStatus(status)); } - function runCapturedCallbackOnce(callbackUrl, executor) { - const normalizedUrl = normalizeString(callbackUrl); - if (!normalizedUrl) { - return Promise.resolve().then(executor); - } - - const existingTask = inFlightCapturedCallbackTasks.get(normalizedUrl); - if (existingTask) { - return existingTask; - } - - let task = null; - task = Promise.resolve() - .then(executor) - .finally(() => { - if (inFlightCapturedCallbackTasks.get(normalizedUrl) === task) { - inFlightCapturedCallbackTasks.delete(normalizedUrl); - } - }); - inFlightCapturedCallbackTasks.set(normalizedUrl, task); - return task; - } - - function normalizeContributionSource(value = '') { - const normalized = normalizeString(value).toLowerCase(); - return normalized === CONTRIBUTION_SOURCE_SUB2API - ? CONTRIBUTION_SOURCE_SUB2API - : CONTRIBUTION_SOURCE_CPA; - } - - function resolveContributionRouting(state = {}) { - const currentStatus = normalizeContributionStatus(state.contributionStatus); - const currentSource = normalizeContributionSource(state.contributionSource); - const hasActiveSession = Boolean( - normalizeString(state.contributionSessionId) - && currentStatus - && !FINAL_STATUSES.has(currentStatus) - ); - - if (hasActiveSession) { - return { - source: currentSource, - targetGroupName: currentSource === CONTRIBUTION_SOURCE_SUB2API - ? (normalizeString(state.contributionTargetGroupName) || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME) - : '', - }; - } - - const source = CONTRIBUTION_SOURCE_SUB2API; - return { - source, - targetGroupName: Boolean(state.plusModeEnabled) - ? CONTRIBUTION_SUB2API_PLUS_GROUP_NAME - : (normalizeString(state.contributionTargetGroupName) || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME), - }; - } - function getStatusLabel(status = '') { switch (normalizeContributionStatus(status)) { case 'started': @@ -175,11 +114,13 @@ case 'waiting': return '等待提交回调'; case 'processing': - return '已提交回调,等待服务端确认'; + return '已提交回调,等待 CPA 确认'; case 'auto_approved': - return '贡献成功,服务端已确认'; + return '贡献成功,CPA 已确认'; case 'auto_rejected': return '贡献未通过确认'; + case 'manual_review_required': + return '已提交,等待人工处理'; case 'expired': return '贡献会话已超时'; case 'error': @@ -537,44 +478,42 @@ } async function handleCapturedCallback(rawUrl, metadata = {}) { + const currentState = await getState(); + if (!normalizeString(currentState.contributionSessionId) || !currentState.contributionMode) { + return currentState; + } + if (!isContributionCallbackUrl(rawUrl, currentState)) { + return currentState; + } + const normalizedUrl = normalizeString(rawUrl); - return runCapturedCallbackOnce(normalizedUrl, async () => { - const currentState = await getState(); - if (!normalizeString(currentState.contributionSessionId) || !currentState.contributionMode) { - return currentState; - } - if (!isContributionCallbackUrl(normalizedUrl, currentState)) { - return currentState; - } + const currentCallbackStatus = normalizeContributionCallbackStatus(currentState.contributionCallbackStatus); + if ( + normalizedUrl + && normalizeString(currentState.contributionCallbackUrl) === normalizedUrl + && (CALLBACK_FINAL_STATUSES.has(currentCallbackStatus) || currentCallbackStatus === 'submitting') + ) { + return currentState; + } - const currentCallbackStatus = normalizeContributionCallbackStatus(currentState.contributionCallbackStatus); - if ( - normalizedUrl - && normalizeString(currentState.contributionCallbackUrl) === normalizedUrl - && (CALLBACK_FINAL_STATUSES.has(currentCallbackStatus) || currentCallbackStatus === 'submitting') - ) { - return currentState; - } - - await applyRuntimeUpdates({ - contributionCallbackUrl: normalizedUrl, - contributionCallbackStatus: 'captured', - contributionCallbackMessage: buildCallbackMessage('captured'), - }); - - if (typeof addLog === 'function') { - await addLog(`贡献模式:已捕获回调地址(${metadata.source || 'unknown'})。`, 'info'); - } - - try { - return await submitContributionCallback(normalizedUrl, { - reason: metadata.source || 'navigation', - stateOverride: await getState(), - }); - } catch { - return getState(); - } + await applyRuntimeUpdates({ + contributionCallbackUrl: normalizedUrl, + contributionCallbackStatus: 'captured', + contributionCallbackMessage: buildCallbackMessage('captured'), }); + + if (typeof addLog === 'function') { + await addLog(`贡献模式:已捕获回调地址(${metadata.source || 'unknown'})。`, 'info'); + } + + try { + return await submitContributionCallback(normalizedUrl, { + reason: metadata.source || 'navigation', + stateOverride: await getState(), + }); + } catch { + return getState(); + } } async function pollContributionStatus(options = {}) { @@ -597,16 +536,6 @@ const callbackState = deriveCallbackState(mergedPayload, currentState); const updates = { contributionLastPollAt: Date.now(), - contributionSource: normalizeContributionSource( - mergedPayload.source - || mergedPayload.source_kind - || currentState.contributionSource - ), - contributionTargetGroupName: normalizeString( - mergedPayload.target_group_name - || mergedPayload.group_name - || currentState.contributionTargetGroupName - ), contributionStatus: normalizedStatus, contributionStatusMessage: buildStatusMessage(normalizedStatus, mergedPayload), contributionCallbackUrl: callbackState.callbackUrl, @@ -648,7 +577,6 @@ async function startContributionFlow(options = {}) { const currentState = options.stateOverride || await getState(); const shouldOpenAuthTab = options.openAuthTab !== false; - const routing = resolveContributionRouting(currentState); if (!currentState.contributionMode) { throw new Error('请先进入贡献模式。'); } @@ -672,8 +600,7 @@ nickname: buildNickname(currentState, options.nickname), qq: buildContributionQq(currentState, options.qq), email: normalizeString(currentState.email), - source: routing.source, - target_group_name: routing.targetGroupName, + source: 'cpa', channel: 'codex-extension', }, }); @@ -686,12 +613,6 @@ } await applyRuntimeUpdates({ - contributionSource: normalizeContributionSource(payload.source || routing.source), - contributionTargetGroupName: normalizeString( - payload.target_group_name - || payload.group_name - || routing.targetGroupName - ), contributionSessionId: sessionId, contributionAuthUrl: authUrl, contributionAuthState: authState, @@ -722,7 +643,7 @@ } function onTabUpdated(tabId, changeInfo, tab) { - const candidateUrl = normalizeString(changeInfo?.url); + const candidateUrl = normalizeString(changeInfo?.url || tab?.url); if (!candidateUrl) { return; } diff --git a/background/message-router.js b/background/message-router.js index 56cfa36..42f3a61 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -40,9 +40,6 @@ getPendingAutoRunTimerPlan, getSourceLabel, getState, - getStepDefinitionForState, - getStepIdsForState, - getLastStepIdForState, getTabId, getStopRequested, handleAutoRunLoopUnhandledError, @@ -130,122 +127,7 @@ } } - function getStepKeyForState(step, state = {}) { - if (typeof getStepDefinitionForState === 'function') { - return String(getStepDefinitionForState(step, state)?.key || '').trim(); - } - return ''; - } - - function isStepProtectedFromAutoSkip(status) { - return status === 'running' - || status === 'completed' - || status === 'manual_completed' - || status === 'skipped'; - } - - function findStepByKeyAfter(currentStep, targetKey, state = {}) { - const activeStepIds = typeof getStepIdsForState === 'function' - ? getStepIdsForState(state) - : []; - const candidates = activeStepIds.length ? activeStepIds : [Number(currentStep) + 1, 8]; - return candidates.find((stepId) => { - const numericStep = Number(stepId); - if (!Number.isFinite(numericStep) || numericStep <= Number(currentStep)) { - return false; - } - const stepKey = getStepKeyForState(numericStep, state); - if (stepKey) { - return stepKey === targetKey; - } - return targetKey === 'fetch-login-code' && Number(currentStep) === 7 && numericStep === 8; - }) || null; - } - - async function handlePlatformVerifyStepData(payload) { - if (payload.localhostUrl) { - await closeLocalhostCallbackTabs(payload.localhostUrl); - } - const latestState = await getState(); - if (latestState.currentHotmailAccountId && isHotmailProvider(latestState)) { - await patchHotmailAccount(latestState.currentHotmailAccountId, { - used: true, - lastUsedAt: Date.now(), - }); - await addLog('当前 Hotmail 账号已自动标记为已用。', 'ok'); - } - if (String(latestState.mailProvider || '').trim().toLowerCase() === '2925' && latestState.currentMail2925AccountId) { - await patchMail2925Account(latestState.currentMail2925AccountId, { - lastUsedAt: Date.now(), - lastError: '', - }); - await addLog('当前 2925 账号已记录最近使用时间。', '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); - } - async function handleStepData(step, payload) { - const stateForStep = await getState(); - const stepKey = getStepKeyForState(step, stateForStep); - - if (stepKey === 'oauth-login') { - if (payload.skipLoginVerificationStep) { - await setState({ loginVerificationRequestedAt: null }); - const latestState = await getState(); - const loginCodeStep = findStepByKeyAfter(step, 'fetch-login-code', latestState); - if (loginCodeStep) { - const currentStatus = latestState.stepStatuses?.[loginCodeStep]; - if (!isStepProtectedFromAutoSkip(currentStatus)) { - await setStepStatus(loginCodeStep, 'skipped'); - await addLog(`步骤 ${step}:认证页已直接进入 OAuth 授权页,已自动跳过步骤 ${loginCodeStep} 的登录验证码。`, 'warn'); - } - } - } else if (payload.loginVerificationRequestedAt) { - await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt }); - } - return; - } - - if (stepKey === 'fetch-login-code') { - await setState({ - lastEmailTimestamp: payload.emailTimestamp || null, - loginVerificationRequestedAt: null, - }); - return; - } - - if (stepKey === 'confirm-oauth') { - if (payload.localhostUrl) { - if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) { - throw new Error(`步骤 ${step} 返回了无效的 localhost OAuth 回调地址。`); - } - await setState({ localhostUrl: payload.localhostUrl }); - broadcastDataUpdate({ localhostUrl: payload.localhostUrl }); - } - return; - } - - if (stepKey === 'platform-verify') { - await handlePlatformVerifyStepData(payload); - return; - } - switch (step) { case 1: { const updates = {}; @@ -258,6 +140,8 @@ 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 (Object.keys(updates).length) { @@ -299,6 +183,11 @@ 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, @@ -313,6 +202,59 @@ } } 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 }); + broadcastDataUpdate({ localhostUrl: payload.localhostUrl }); + } + break; + case 10: { + if (payload.localhostUrl) { + await closeLocalhostCallbackTabs(payload.localhostUrl); + } + const latestState = await getState(); + if (latestState.currentHotmailAccountId && isHotmailProvider(latestState)) { + await patchHotmailAccount(latestState.currentHotmailAccountId, { + used: true, + lastUsedAt: Date.now(), + }); + await addLog('当前 Hotmail 账号已自动标记为已用。', 'ok'); + } + if (String(latestState.mailProvider || '').trim().toLowerCase() === '2925' && latestState.currentMail2925AccountId) { + await patchMail2925Account(latestState.currentMail2925AccountId, { + lastUsedAt: Date.now(), + lastError: '', + }); + await addLog('当前 2925 账号已记录最近使用时间。', '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); + break; + } default: break; } @@ -363,15 +305,11 @@ return { ok: true, error: errorMessage }; } - const completionStateCandidate = await getState(); - const lastStepId = typeof getLastStepIdForState === 'function' - ? getLastStepIdForState(completionStateCandidate) - : 10; - const completionState = message.step === lastStepId ? completionStateCandidate : null; + const completionState = message.step === 10 ? await getState() : null; await setStepStatus(message.step, 'completed'); await addLog(`步骤 ${message.step} 已完成`, 'ok'); await handleStepData(message.step, message.payload); - if (message.step === lastStepId && typeof appendAccountRunRecord === 'function') { + if (message.step === 10 && typeof appendAccountRunRecord === 'function') { await appendAccountRunRecord('success', completionState); } notifyStepComplete(message.step, message.payload); @@ -520,8 +458,7 @@ await setPersistentSettings({ emailPrefix: message.payload.emailPrefix }); await setState({ emailPrefix: message.payload.emailPrefix }); } - const executionState = await getState(); - if (doesStepUseCompletionSignal(step, executionState)) { + if (doesStepUseCompletionSignal(step)) { await executeStepViaCompletionSignal(step); } else { await executeStep(step); @@ -626,35 +563,13 @@ } case 'SAVE_SETTING': { - const currentState = await getState(); const updates = buildPersistentSettingsPayload(message.payload || {}); const sessionUpdates = buildLuckmailSessionSettingsPayload(message.payload || {}); - const modeChanged = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled') - && Boolean(currentState?.plusModeEnabled) !== Boolean(updates.plusModeEnabled); await setPersistentSettings(updates); - const stateUpdates = { + await setState({ ...updates, ...sessionUpdates, - }; - if (modeChanged && typeof getStepIdsForState === 'function') { - const nextStateForSteps = { ...currentState, ...stateUpdates }; - stateUpdates.stepStatuses = Object.fromEntries( - getStepIdsForState(nextStateForSteps).map((stepId) => [stepId, 'pending']) - ); - stateUpdates.currentStep = 0; - } - await setState(stateUpdates); - if (Boolean(currentState?.contributionMode) && typeof setContributionMode === 'function') { - await setContributionMode(true); - } - if (modeChanged) { - await addLog( - Boolean(updates.plusModeEnabled) - ? 'Plus 模式已开启,已切换为 Plus Checkout + PayPal 步骤。' - : 'Plus 模式已关闭,已恢复普通注册授权步骤。', - 'info' - ); - } + }); return { ok: true, state: await getState() }; } diff --git a/background/panel-bridge.js b/background/panel-bridge.js index a667960..2b0f077 100644 --- a/background/panel-bridge.js +++ b/background/panel-bridge.js @@ -43,6 +43,78 @@ return message || `Codex2API 请求失败(HTTP ${responseStatus})。`; } + function deriveCpaManagementOrigin(vpsUrl) { + const normalizedUrl = String(vpsUrl || '').trim(); + if (!normalizedUrl) { + throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。'); + } + let parsed; + try { + parsed = new URL(normalizedUrl); + } catch { + throw new Error('CPA 地址格式无效,请先在侧边栏检查。'); + } + return parsed.origin; + } + + function getCpaApiErrorMessage(payload, responseStatus = 500) { + const candidates = [ + payload?.error, + payload?.message, + payload?.detail, + payload?.reason, + ]; + const message = candidates + .map((value) => String(value || '').trim()) + .find(Boolean); + return message || `CPA 管理接口请求失败(HTTP ${responseStatus})。`; + } + + async function fetchCpaManagementJson(origin, path, options = {}) { + const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000)); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + try { + const managementKey = String(options.managementKey || '').trim(); + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + if (managementKey) { + headers.Authorization = `Bearer ${managementKey}`; + headers['X-Management-Key'] = managementKey; + } + + const response = await fetch(`${origin}${path}`, { + method: options.method || 'POST', + headers, + body: options.body === undefined ? undefined : JSON.stringify(options.body), + signal: controller.signal, + }); + + let payload = {}; + try { + payload = await response.json(); + } catch { + payload = {}; + } + + if (!response.ok) { + throw new Error(getCpaApiErrorMessage(payload, response.status)); + } + + return payload; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error('CPA 管理接口请求超时,请稍后重试。'); + } + throw error; + } finally { + clearTimeout(timer); + } + } + async function fetchCodex2ApiJson(origin, path, options = {}) { const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000)); const controller = new AbortController(); @@ -97,49 +169,48 @@ if (!state.vpsUrl) { throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。'); } - - await addLog(`${logLabel}:正在打开 CPA 面板...`); - - const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js']; - await closeConflictingTabsForSource('vps-panel', state.vpsUrl); - - const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true }); - const tabId = tab.id; - await rememberSourceLastUrl('vps-panel', state.vpsUrl); - - await addLog(`${logLabel}:CPA 面板已打开,正在等待页面进入目标地址...`); - const matchedTab = await waitForTabUrlFamily('vps-panel', tabId, state.vpsUrl, { - timeoutMs: 15000, - retryDelayMs: 400, - }); - if (!matchedTab) { - await addLog(`${logLabel}:CPA 页面尚未完全进入目标地址,继续尝试连接内容脚本...`, 'warn'); + const managementKey = String(state.vpsPassword || '').trim(); + if (!managementKey) { + throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。'); } - await ensureContentScriptReadyOnTab('vps-panel', tabId, { - inject: injectFiles, - timeoutMs: 45000, - retryDelayMs: 900, - logMessage: `${logLabel}:CPA 面板仍在加载,正在重试连接内容脚本...`, + const origin = deriveCpaManagementOrigin(state.vpsUrl); + + await addLog(`${logLabel}:正在通过 CPA 管理接口获取 OAuth 授权链接...`); + const result = await fetchCpaManagementJson(origin, '/v0/management/codex-auth-url', { + method: 'GET', + managementKey, }); - const result = await sendToContentScriptResilient('vps-panel', { - type: 'REQUEST_OAUTH_URL', - source: 'background', - payload: { - vpsPassword: state.vpsPassword, - logStep: 7, - }, - }, { - timeoutMs: 30000, - retryDelayMs: 700, - logMessage: `${logLabel}:CPA 面板通信未就绪,正在等待页面恢复...`, - }); + const oauthUrl = String( + result?.url + || result?.auth_url + || result?.authUrl + || result?.data?.url + || result?.data?.auth_url + || result?.data?.authUrl + || '' + ).trim(); + const oauthState = String( + result?.state + || result?.auth_state + || result?.authState + || result?.data?.state + || result?.data?.auth_state + || result?.data?.authState + || '' + ).trim() + || extractStateFromAuthUrl(oauthUrl); - if (result?.error) { - throw new Error(result.error); + if (!oauthUrl || !oauthUrl.startsWith('http')) { + throw new Error('CPA 管理接口未返回有效的 auth_url。'); } - return result || {}; + + return { + oauthUrl, + cpaOAuthState: oauthState || null, + cpaManagementOrigin: origin, + }; } async function requestCodex2ApiOAuthUrl(state, options = {}) { diff --git a/background/steps/confirm-oauth.js b/background/steps/confirm-oauth.js index 88607c9..59deab9 100644 --- a/background/steps/confirm-oauth.js +++ b/background/steps/confirm-oauth.js @@ -33,27 +33,16 @@ setStep8TabUpdatedListener, } = deps; - function getVisibleStep(state, fallback = 9) { - const visibleStep = Math.floor(Number(state?.visibleStep) || 0); - return visibleStep > 0 ? visibleStep : fallback; - } - - function getAuthLoginStepForVisibleStep(visibleStep) { - return visibleStep >= 12 ? 10 : 7; - } - async function executeStep9(state) { - const visibleStep = getVisibleStep(state, 9); if (!state.oauthUrl) { - const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep); - throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${authLoginStep}。`); + throw new Error('缺少登录用 OAuth 链接,请先完成步骤 7。'); } - await addLog(`步骤 ${visibleStep}:正在监听 localhost 回调地址...`); + await addLog('步骤 9:正在监听 localhost 回调地址...'); const callbackTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(240000, { - step: visibleStep, + step: 9, actionLabel: 'OAuth localhost 回调', }) : 240000; @@ -82,8 +71,8 @@ cleanupListener(); clearTimeout(timeout); - addLog(`步骤 ${visibleStep}:已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => { - return completeStepFromBackground(visibleStep, { localhostUrl: callbackUrl }); + addLog(`步骤 9:已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => { + return completeStepFromBackground(9, { localhostUrl: callbackUrl }); }).then(() => { resolve(); }).catch((err) => { @@ -92,7 +81,7 @@ }; const timeout = setTimeout(() => { - rejectStep9(new Error(`${Math.round(callbackTimeoutMs / 1000)} 秒内未捕获到 localhost 回调跳转,步骤 ${visibleStep} 的点击可能被拦截了。`)); + rejectStep9(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 9 的点击可能被拦截了。')); }, callbackTimeoutMs); setStep8PendingReject((error) => { @@ -122,10 +111,10 @@ if (signupTabId && await isTabAlive('signup-page')) { await chrome.tabs.update(signupTabId, { active: true }); - await addLog(`步骤 ${visibleStep}:已切回认证页,正在准备调试器点击...`); + await addLog('步骤 9:已切回认证页,正在准备调试器点击...'); } else { signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl); - await addLog(`步骤 ${visibleStep}:已重新打开认证页,正在准备调试器点击...`); + await addLog('步骤 9:已重新打开认证页,正在准备调试器点击...'); } throwIfStep8SettledOrStopped(resolved); @@ -135,11 +124,11 @@ await ensureStep8SignupPageReady(signupTabId, { timeoutMs: typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(15000, { - step: visibleStep, + step: 9, actionLabel: '等待 OAuth 同意页内容脚本就绪', }) : 15000, - logMessage: `步骤 ${visibleStep}:认证页内容脚本尚未就绪,正在等待页面恢复...`, + logMessage: '步骤 9:认证页内容脚本尚未就绪,正在等待页面恢复...', }); for (let round = 1; round <= STEP8_MAX_ROUNDS && !resolved; round++) { @@ -148,7 +137,7 @@ signupTabId, typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(STEP8_READY_WAIT_TIMEOUT_MS, { - step: visibleStep, + step: 9, actionLabel: '等待 OAuth 同意页出现', }) : STEP8_READY_WAIT_TIMEOUT_MS @@ -160,12 +149,12 @@ const strategy = STEP8_STRATEGIES[Math.min(round - 1, STEP8_STRATEGIES.length - 1)]; - await addLog(`步骤 ${visibleStep}:第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label})...`); + await addLog(`步骤 9:第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label})...`); if (strategy.mode === 'debugger') { const clickActionTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(15000, { - step: visibleStep, + step: 9, actionLabel: '定位 OAuth 同意页继续按钮', }) : 15000; @@ -178,7 +167,7 @@ } else { const clickActionTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(15000, { - step: visibleStep, + step: 9, actionLabel: '点击 OAuth 同意页继续按钮', }) : 15000; @@ -197,7 +186,7 @@ pageState.url, typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(15000, { - step: visibleStep, + step: 9, actionLabel: '等待 OAuth 同意页点击生效', }) : 15000 @@ -207,20 +196,20 @@ } if (effect.progressed) { - await addLog(`步骤 ${visibleStep}:检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info'); + await addLog(`步骤 9:检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info'); break; } if (round >= STEP8_MAX_ROUNDS) { - throw new Error(`步骤 ${visibleStep}:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`); + throw new Error(`步骤 9:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`); } - await addLog(`步骤 ${visibleStep}:${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS})...`, 'warn'); + await addLog(`步骤 9:${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS})...`, 'warn'); await reloadStep8ConsentPage( signupTabId, typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(30000, { - step: visibleStep, + step: 9, actionLabel: '刷新 OAuth 同意页', }) : 30000 diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index d12ca49..f3209ea 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -40,13 +40,7 @@ return /缺少|未配置|请输入|无效|错误|失败|401|认证失败|未授权|unauthorized|invalid/i.test(message); } - function getVisibleStep(state, fallback = 7) { - const visibleStep = Math.floor(Number(state?.visibleStep) || 0); - return visibleStep > 0 ? visibleStep : fallback; - } - async function executeStep7(state) { - const visibleStep = getVisibleStep(state, 7); if (!state.email) { throw new Error('缺少邮箱地址,请先完成步骤 3。'); } @@ -60,22 +54,22 @@ try { const currentState = attempt === 1 ? state : await getState(); const password = currentState.password || currentState.customPassword || ''; - const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState, { visibleStep }); + const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState); if (typeof startOAuthFlowTimeoutWindow === 'function') { - await startOAuthFlowTimeoutWindow({ step: visibleStep, oauthUrl }); + await startOAuthFlowTimeoutWindow({ step: 7, oauthUrl }); } const loginTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(180000, { - step: visibleStep, + step: 7, actionLabel: 'OAuth 登录并进入验证码页', oauthUrl, }) : 180000; if (attempt === 1) { - await addLog(`步骤 ${visibleStep}:正在打开最新 OAuth 链接并登录...`); + await addLog('步骤 7:正在打开最新 OAuth 链接并登录...'); } else { - await addLog(`步骤 ${visibleStep}:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn'); + await addLog(`步骤 7:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn'); } await reuseOrCreateTab('signup-page', oauthUrl); @@ -89,14 +83,13 @@ payload: { email: currentState.email, password, - visibleStep, }, }, { timeoutMs: loginTimeoutMs, responseTimeoutMs: loginTimeoutMs, retryDelayMs: 700, - logMessage: `步骤 ${visibleStep}:认证页正在切换,等待页面重新就绪后继续登录...`, + logMessage: '步骤 7:认证页正在切换,等待页面重新就绪后继续登录...', } ); @@ -105,16 +98,9 @@ } if (isStep6SuccessResult(result)) { - const completionPayload = { + await completeStepFromBackground(7, { loginVerificationRequestedAt: result.loginVerificationRequestedAt || null, - }; - if (result.skipLoginVerificationStep) { - completionPayload.skipLoginVerificationStep = true; - } - if (result.directOAuthConsentPage) { - completionPayload.directOAuthConsentPage = true; - } - await completeStepFromBackground(visibleStep, completionPayload); + }); return; } @@ -132,7 +118,7 @@ } if (isManagementSecretConfigError(err)) { await addLog( - `步骤 ${visibleStep}:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`, + `步骤 7:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`, 'error' ); throw err; @@ -142,11 +128,11 @@ break; } - await addLog(`步骤 ${visibleStep}:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn'); + await addLog(`步骤 7:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn'); } } - throw new Error(`步骤 ${visibleStep}:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`); + throw new Error(`步骤 7:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`); } return { executeStep7 }; diff --git a/background/steps/platform-verify.js b/background/steps/platform-verify.js index b88f295..bb391eb 100644 --- a/background/steps/platform-verify.js +++ b/background/steps/platform-verify.js @@ -26,31 +26,18 @@ return String(value || '').trim(); } - function getVisibleStep(state, fallback = 10) { - const visibleStep = Math.floor(Number(state?.visibleStep) || 0); - return visibleStep > 0 ? visibleStep : fallback; - } - - function getConfirmStepForVisibleStep(visibleStep) { - return visibleStep >= 13 ? 12 : 9; - } - - function getAuthLoginStepForVisibleStep(visibleStep) { - return visibleStep >= 13 ? 10 : 7; - } - - function parseLocalhostCallback(rawUrl, visibleStep = 10, confirmStep = 9) { + function parseLocalhostCallback(rawUrl) { let parsed; try { parsed = new URL(rawUrl); } catch { - throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 ${confirmStep}。`); + throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 9。'); } const code = normalizeString(parsed.searchParams.get('code')); const state = normalizeString(parsed.searchParams.get('state')); if (!code || !state) { - throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 ${confirmStep}。`); + throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 9。'); } return { @@ -72,6 +59,77 @@ return details || `Codex2API 请求失败(HTTP ${responseStatus})。`; } + function deriveCpaManagementOrigin(vpsUrl) { + const normalizedUrl = normalizeString(vpsUrl); + if (!normalizedUrl) { + throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。'); + } + let parsed; + try { + parsed = new URL(normalizedUrl); + } catch { + throw new Error('CPA 地址格式无效,请先在侧边栏检查。'); + } + return parsed.origin; + } + + function getCpaApiErrorMessage(payload, responseStatus = 500) { + const details = [ + payload?.error, + payload?.message, + payload?.detail, + payload?.reason, + ] + .map((value) => normalizeString(value)) + .find(Boolean); + return details || `CPA 管理接口请求失败(HTTP ${responseStatus})。`; + } + + async function fetchCpaManagementJson(origin, path, options = {}) { + const controller = new AbortController(); + const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000)); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + try { + const managementKey = normalizeString(options.managementKey); + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + if (managementKey) { + headers.Authorization = `Bearer ${managementKey}`; + headers['X-Management-Key'] = managementKey; + } + + const response = await fetch(`${origin}${path}`, { + method: options.method || 'POST', + headers, + body: options.body === undefined ? undefined : JSON.stringify(options.body), + signal: controller.signal, + }); + + let payload = {}; + try { + payload = await response.json(); + } catch { + payload = {}; + } + + if (!response.ok) { + throw new Error(getCpaApiErrorMessage(payload, response.status)); + } + + return payload; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error('CPA 管理接口请求超时,请稍后重试。'); + } + throw error; + } finally { + clearTimeout(timer); + } + } + async function fetchCodex2ApiJson(origin, path, options = {}) { const controller = new AbortController(); const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000)); @@ -122,95 +180,86 @@ } async function executeCpaStep10(state) { - const visibleStep = getVisibleStep(state, 10); - const confirmStep = getConfirmStepForVisibleStep(visibleStep); if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { - throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`); + throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。'); } if (!state.localhostUrl) { - throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`); + throw new Error('缺少 localhost 回调地址,请先完成步骤 9。'); } if (!state.vpsUrl) { throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。'); } if (shouldBypassStep9ForLocalCpa(state)) { - await addLog(`步骤 ${visibleStep}:检测到本地 CPA,且当前策略为“跳过第10步”,本轮不再重复提交回调地址。`, 'info'); - await completeStepFromBackground(visibleStep, { + await addLog('步骤 10:检测到本地 CPA,且当前策略为“跳过第10步”,本轮不再重复提交回调地址。', 'info'); + await completeStepFromBackground(10, { localhostUrl: state.localhostUrl, verifiedStatus: 'local-auto', }); return; } - await addLog(`步骤 ${visibleStep}:正在打开 CPA 面板...`); - - const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js']; - let tabId = await getTabId('vps-panel'); - const alive = tabId && await isTabAlive('vps-panel'); - - if (!alive) { - tabId = await reuseOrCreateTab('vps-panel', state.vpsUrl, { - inject: injectFiles, - reloadIfSameUrl: true, - }); - } else { - await closeConflictingTabsForSource('vps-panel', state.vpsUrl, { excludeTabIds: [tabId] }); - await chrome.tabs.update(tabId, { active: true }); - await rememberSourceLastUrl('vps-panel', state.vpsUrl); + const callback = parseLocalhostCallback(state.localhostUrl); + const expectedState = normalizeString(state.cpaOAuthState); + if (expectedState && expectedState !== callback.state) { + throw new Error('CPA 回调 state 与当前授权会话不匹配,请重新执行步骤 7。'); + } + const managementKey = normalizeString(state.vpsPassword); + if (!managementKey) { + throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。'); } - await ensureContentScriptReadyOnTab('vps-panel', tabId, { - inject: injectFiles, - timeoutMs: 45000, - retryDelayMs: 900, - logMessage: `步骤 ${visibleStep}:CPA 面板仍在加载,正在重试连接...`, - }); + await addLog('步骤 10:正在通过 CPA 管理接口提交回调地址...'); + try { + const origin = normalizeString(state.cpaManagementOrigin) || deriveCpaManagementOrigin(state.vpsUrl); + const result = await fetchCpaManagementJson(origin, '/v0/management/oauth-callback', { + method: 'POST', + managementKey, + body: { + provider: 'codex', + redirect_url: callback.url, + }, + }); - await addLog(`步骤 ${visibleStep}:正在填写回调地址...`); - const result = await sendToContentScriptResilient('vps-panel', { - type: 'EXECUTE_STEP', - step: visibleStep, - source: 'background', - payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword, visibleStep }, - }, { - timeoutMs: 125000, - responseTimeoutMs: 125000, - retryDelayMs: 700, - logMessage: `步骤 ${visibleStep}:CPA 面板通信未就绪,正在等待页面恢复...`, - }); - - if (result?.error) { - throw new Error(result.error); + const verifiedStatus = normalizeString(result?.message) + || normalizeString(result?.status_message) + || 'CPA 已通过接口提交回调'; + await addLog(`步骤 10:${verifiedStatus}`, 'ok'); + await completeStepFromBackground(10, { + localhostUrl: callback.url, + verifiedStatus, + }); + } catch (error) { + const reason = normalizeString(error?.message) || 'unknown error'; + await addLog(`步骤 10:CPA 接口提交失败:${reason}`, 'error'); + throw error; } } async function executeCodex2ApiStep10(state) { - const visibleStep = getVisibleStep(state, 10); - const confirmStep = getConfirmStepForVisibleStep(visibleStep); if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { - throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`); + throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。'); } if (!state.localhostUrl) { - throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`); + throw new Error('缺少 localhost 回调地址,请先完成步骤 9。'); } if (!state.codex2apiSessionId) { - throw new Error(`缺少 Codex2API 会话信息,请重新执行步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`); + throw new Error('缺少 Codex2API 会话信息,请重新执行步骤 7。'); } if (!normalizeString(state.codex2apiAdminKey)) { throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。'); } - const callback = parseLocalhostCallback(state.localhostUrl, visibleStep, confirmStep); + const callback = parseLocalhostCallback(state.localhostUrl); const expectedState = normalizeString(state.codex2apiOAuthState); if (expectedState && expectedState !== callback.state) { - throw new Error(`Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`); + throw new Error('Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 7。'); } const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl); const origin = new URL(codex2apiUrl).origin; - await addLog(`步骤 ${visibleStep}:正在向 Codex2API 提交回调并创建账号...`); + await addLog('步骤 10:正在向 Codex2API 提交回调并创建账号...'); const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/exchange-code', { adminKey: state.codex2apiAdminKey, method: 'POST', @@ -222,21 +271,19 @@ }); const verifiedStatus = normalizeString(result?.message) || 'Codex2API OAuth 账号添加成功'; - await addLog(`步骤 ${visibleStep}:${verifiedStatus}`, 'ok'); - await completeStepFromBackground(visibleStep, { + await addLog(`步骤 10:${verifiedStatus}`, 'ok'); + await completeStepFromBackground(10, { localhostUrl: callback.url, verifiedStatus, }); } async function executeSub2ApiStep10(state) { - const visibleStep = getVisibleStep(state, 10); - const confirmStep = getConfirmStepForVisibleStep(visibleStep); if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { - throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`); + throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。'); } if (!state.localhostUrl) { - throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`); + throw new Error('缺少 localhost 回调地址,请先完成步骤 9。'); } if (!state.sub2apiSessionId) { throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。'); @@ -251,7 +298,7 @@ const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl); const injectFiles = ['content/utils.js', 'content/sub2api-panel.js']; - await addLog(`步骤 ${visibleStep}:正在打开 SUB2API 后台...`); + await addLog('步骤 10:正在打开 SUB2API 后台...'); let tabId = await getTabId('sub2api-panel'); const alive = tabId && await isTabAlive('sub2api-panel'); @@ -273,13 +320,12 @@ injectSource: 'sub2api-panel', }); - await addLog(`步骤 ${visibleStep}:正在向 SUB2API 提交回调并创建账号...`); + await addLog('步骤 10:正在向 SUB2API 提交回调并创建账号...'); const result = await sendToContentScript('sub2api-panel', { type: 'EXECUTE_STEP', - step: visibleStep, + step: 10, source: 'background', payload: { - visibleStep, localhostUrl: state.localhostUrl, sub2apiUrl, sub2apiEmail: state.sub2apiEmail, diff --git a/tests/background-cpa-state-plumbing.test.js b/tests/background-cpa-state-plumbing.test.js new file mode 100644 index 0000000..c894bd5 --- /dev/null +++ b/tests/background-cpa-state-plumbing.test.js @@ -0,0 +1,39 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +test('background step-1 state plumbing persists and resets cpa oauth runtime keys', () => { + const source = fs.readFileSync('background.js', 'utf8'); + + assert.match(source, /cpaOAuthState:\s*null/); + assert.match(source, /cpaManagementOrigin:\s*null/); + assert.match(source, /payload\.cpaOAuthState[^\n]*updates\.cpaOAuthState/); + assert.match(source, /payload\.cpaManagementOrigin[^\n]*updates\.cpaManagementOrigin/); + assert.match(source, /if \(step <= 1\) \{[\s\S]*cpaOAuthState:\s*null,[\s\S]*cpaManagementOrigin:\s*null,/); +}); + +test('message router step-1 handler stores cpa oauth runtime keys', async () => { + const source = fs.readFileSync('background/message-router.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope); + + const updates = []; + const router = api.createMessageRouter({ + broadcastDataUpdate: () => {}, + setState: async (payload) => { + updates.push(payload); + }, + }); + + await router.handleStepData(1, { + cpaOAuthState: 'oauth-state-1', + cpaManagementOrigin: 'http://localhost:8317', + }); + + assert.deepStrictEqual(updates, [ + { + cpaOAuthState: 'oauth-state-1', + cpaManagementOrigin: 'http://localhost:8317', + }, + ]); +}); diff --git a/tests/background-panel-bridge-module.test.js b/tests/background-panel-bridge-module.test.js index 353c990..3e655af 100644 --- a/tests/background-panel-bridge-module.test.js +++ b/tests/background-panel-bridge-module.test.js @@ -71,3 +71,48 @@ test('panel bridge can request codex2api oauth url via protocol', async () => { globalThis.fetch = originalFetch; } }); + +test('panel bridge can request cpa oauth url via management api', async () => { + const source = fs.readFileSync('background/panel-bridge.js', 'utf8'); + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, options = {}) => { + assert.equal(url, 'http://localhost:8317/v0/management/codex-auth-url'); + assert.equal(options.method, 'GET'); + assert.equal(options.headers.Authorization, 'Bearer cpa-key'); + assert.equal(options.headers['X-Management-Key'], 'cpa-key'); + return { + ok: true, + json: async () => ({ + status: 'ok', + url: 'https://auth.openai.com/authorize?state=cpa-oauth-state', + state: 'cpa-oauth-state', + }), + }; + }; + + try { + const api = new Function('self', `${source}; return self.MultiPageBackgroundPanelBridge;`)({}); + const bridge = api.createPanelBridge({ + addLog: async () => {}, + getPanelMode: () => 'cpa', + normalizeCodex2ApiUrl: (value) => value, + normalizeSub2ApiUrl: (value) => value, + DEFAULT_SUB2API_GROUP_NAME: 'codex', + SUB2API_STEP1_RESPONSE_TIMEOUT_MS: 90000, + }); + + const result = await bridge.requestOAuthUrlFromPanel({ + panelMode: 'cpa', + vpsUrl: 'http://localhost:8317/admin/oauth', + vpsPassword: 'cpa-key', + }, { logLabel: '步骤 7' }); + + assert.deepStrictEqual(result, { + oauthUrl: 'https://auth.openai.com/authorize?state=cpa-oauth-state', + cpaOAuthState: 'cpa-oauth-state', + cpaManagementOrigin: 'http://localhost:8317', + }); + } finally { + globalThis.fetch = originalFetch; + } +}); diff --git a/tests/background-platform-verify-cpa-api.test.js b/tests/background-platform-verify-cpa-api.test.js new file mode 100644 index 0000000..0fd9002 --- /dev/null +++ b/tests/background-platform-verify-cpa-api.test.js @@ -0,0 +1,239 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const test = require('node:test'); + +function createDeps(overrides = {}) { + const logs = []; + const completed = []; + const uiCalls = []; + + const deps = { + addLog: async (message, level = 'info') => { + logs.push({ message, level }); + }, + chrome: { + tabs: { + update: async () => {}, + }, + }, + closeConflictingTabsForSource: async () => {}, + completeStepFromBackground: async (step, payload) => { + completed.push({ step, payload }); + }, + ensureContentScriptReadyOnTab: async () => {}, + getPanelMode: () => 'cpa', + getTabId: async () => 0, + isLocalhostOAuthCallbackUrl: (value) => String(value || '').includes('/auth/callback?code='), + isTabAlive: async () => false, + normalizeCodex2ApiUrl: (value) => value, + normalizeSub2ApiUrl: (value) => value, + rememberSourceLastUrl: async () => {}, + reuseOrCreateTab: async () => 91, + sendToContentScript: async () => ({}), + sendToContentScriptResilient: async (source, message, options) => { + uiCalls.push({ source, message, options }); + return {}; + }, + shouldBypassStep9ForLocalCpa: () => false, + SUB2API_STEP9_RESPONSE_TIMEOUT_MS: 120000, + ...overrides, + }; + + return { deps, logs, completed, uiCalls }; +} + +test('platform verify module submits CPA callback via management API first', async () => { + const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8'); + const originalFetch = globalThis.fetch; + let uiCalled = false; + globalThis.fetch = async (url, options = {}) => { + assert.equal(url, 'http://localhost:8317/v0/management/oauth-callback'); + assert.equal(options.method, 'POST'); + assert.equal(options.headers.Authorization, 'Bearer cpa-key'); + assert.equal(options.headers['X-Management-Key'], 'cpa-key'); + assert.deepStrictEqual(JSON.parse(options.body), { + provider: 'codex', + redirect_url: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state', + }); + return { + ok: true, + json: async () => ({ + message: 'CPA API 回调提交成功', + }), + }; + }; + + try { + const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({}); + const { deps, logs, completed } = createDeps({ + sendToContentScriptResilient: async () => { + uiCalled = true; + return {}; + }, + }); + const executor = api.createStep10Executor(deps); + + await executor.executeStep10({ + panelMode: 'cpa', + localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state', + vpsUrl: 'http://localhost:8317/admin/oauth', + vpsPassword: 'cpa-key', + }); + + assert.equal(uiCalled, false); + assert.deepStrictEqual(completed, [ + { + step: 10, + payload: { + localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state', + verifiedStatus: 'CPA API 回调提交成功', + }, + }, + ]); + assert.deepStrictEqual(logs, [ + { message: '步骤 10:正在通过 CPA 管理接口提交回调地址...', level: 'info' }, + { message: '步骤 10:CPA API 回调提交成功', level: 'ok' }, + ]); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test('platform verify module prefers cpaManagementOrigin when provided', async () => { + const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8'); + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, options = {}) => { + assert.equal(url, 'http://localhost:9999/v0/management/oauth-callback'); + assert.equal(options.method, 'POST'); + assert.equal(options.headers.Authorization, 'Bearer cpa-key'); + assert.equal(options.headers['X-Management-Key'], 'cpa-key'); + return { + ok: true, + json: async () => ({ + message: 'CPA API 回调提交成功', + }), + }; + }; + + try { + const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({}); + const { deps, completed } = createDeps(); + const executor = api.createStep10Executor(deps); + + await executor.executeStep10({ + panelMode: 'cpa', + localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state', + cpaManagementOrigin: 'http://localhost:9999', + vpsUrl: 'http://localhost:8317/admin/oauth', + vpsPassword: 'cpa-key', + }); + + assert.equal(completed.length, 1); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test('platform verify module fails fast when CPA API submit fails', async () => { + const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8'); + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => ({ + ok: false, + status: 500, + json: async () => ({ message: 'failed to persist oauth callback' }), + }); + + try { + const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({}); + const { deps, logs, completed, uiCalls } = createDeps(); + const executor = api.createStep10Executor(deps); + + await assert.rejects( + () => executor.executeStep10({ + panelMode: 'cpa', + localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state', + vpsUrl: 'http://localhost:8317/admin/oauth', + vpsPassword: 'cpa-key', + }), + /failed to persist oauth callback/ + ); + + assert.equal(uiCalls.length, 0); + assert.equal(completed.length, 0); + assert.equal(logs[0].message, '步骤 10:正在通过 CPA 管理接口提交回调地址...'); + assert.match(logs[1].message, /步骤 10:CPA 接口提交失败:failed to persist oauth callback/); + assert.equal(logs[1].level, 'error'); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test('platform verify module requires management key for CPA API-only flow', async () => { + const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8'); + const originalFetch = globalThis.fetch; + let fetchCalled = false; + globalThis.fetch = async () => { + fetchCalled = true; + return { + ok: true, + json: async () => ({}), + }; + }; + + try { + const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({}); + const { deps, logs, completed, uiCalls } = createDeps(); + const executor = api.createStep10Executor(deps); + + await assert.rejects( + () => executor.executeStep10({ + panelMode: 'cpa', + localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state', + vpsUrl: 'http://localhost:8317/admin/oauth', + vpsPassword: ' ', + }), + /尚未配置 CPA 管理密钥/ + ); + + assert.equal(fetchCalled, false); + assert.equal(uiCalls.length, 0); + assert.equal(completed.length, 0); + assert.equal(logs.length, 0); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test('platform verify module rejects callback when cpa oauth state mismatches', async () => { + const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8'); + const originalFetch = globalThis.fetch; + let fetchCalled = false; + globalThis.fetch = async () => { + fetchCalled = true; + return { + ok: true, + json: async () => ({ message: 'should not happen' }), + }; + }; + + try { + const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({}); + const { deps } = createDeps(); + const executor = api.createStep10Executor(deps); + + await assert.rejects( + () => executor.executeStep10({ + panelMode: 'cpa', + localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=callback-state', + cpaOAuthState: 'expected-state', + vpsUrl: 'http://localhost:8317/admin/oauth', + vpsPassword: 'cpa-key', + }), + /CPA 回调 state 与当前授权会话不匹配/ + ); + + assert.equal(fetchCalled, false); + } finally { + globalThis.fetch = originalFetch; + } +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 371bc0d..39d7153 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -23,7 +23,6 @@ - 轮询登录验证码 - 自动确认 OAuth 同意页 - 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API -- 可选开启 Plus 模式:先完成 Plus Checkout、账单地址、PayPal 登录授权与订阅回跳确认,再复用 OAuth 后半段链路 ## 2. 核心运行参与者 @@ -43,9 +42,8 @@ - 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `apikey.qzz.io` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现 - 在 sidepanel 初始化和点击“自动”按钮前刷新一次贡献站公开内容摘要;如果刷新失败,不阻塞主自动流程 - 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计与分页列表 -- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Ultra`、历史 `Pro` 与 legacy `v` 三个版本族,排序时固定以 `Ultra` 为最高正式系列,同时会在读取缓存后重新排序,避免历史 `Pro` 或 `v` 版本误显示为比 `Ultra` 更新 +- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Pro` 与 legacy `v` 两个版本族,排序时优先保持版本族语义一致,同时会在读取缓存后重新排序,避免旧缓存把 `v` 版本误显示为比 `Pro` 更新 - 展示一个单独的“接码”开关;开启后才展示 HeroSMS 的接码国家与 API Key 设置,用于 OAuth 登录链路命中手机号验证页时直接续跑手机验证 -- 展示 `Plus 模式` 开关与 PayPal 账号/密码配置;开启后步骤列表切换为 Plus 模式 13 步定义,普通模式的 Cookie 清理步骤不再显示或执行,登录验证码步骤会移动到 Plus 可见第 11 步 - 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作 ### 2.2 Background Service Worker @@ -118,7 +116,6 @@ - 步骤顺序靠 `order` - 步骤文件名靠语义 - 新增步骤时不需要重命名后续文件 -- 普通模式使用 10 步定义;Plus 模式使用 13 步定义,其中 Plus 可见步骤 10/11/12/13 复用原 OAuth 登录、登录验证码、OAuth 同意页与平台回调验证执行器,但按 Plus 可见步骤编号记录状态 ## 4. 状态与存储链路 @@ -141,7 +138,6 @@ - 第 8 步固定的验证码页显示邮箱 `step8VerificationTargetEmail` - 当前手机号验证激活记录 `currentPhoneActivation` - 可复用的手机号验证激活记录 `reusablePhoneActivation` -- Plus checkout / PayPal 运行态:`plusCheckoutTabId`、`plusCheckoutUrl`、`plusCheckoutCountry`、`plusCheckoutCurrency`、`plusBillingCountryText`、`plusBillingAddress`、`plusPaypalApprovedAt`、`plusReturnUrl` - localhost 回调地址 - 自动运行轮次信息 - 当前自动运行 session 标识 `autoRunSessionId` @@ -160,8 +156,6 @@ - CPA / SUB2API 配置 - Codex2API 配置 -- Plus 模式开关 `plusModeEnabled` -- PayPal 登录配置 `paypalEmail / paypalPassword` - 邮箱 provider 配置 - Hotmail 账号池 - 2925 账号池 @@ -169,8 +163,7 @@ - 2925 当前选中的号池账号 ID `currentMail2925AccountId` - Cloudflare / Temp Email 设置 - 接码开关,以及 HeroSMS 的 API Key 与默认国家设置 -- iCloud 相关偏好:Host、获取策略、成功后自动删除、目标邮箱类型与转发收码邮箱 provider -- iCloud Hide My Email 别名缓存:`icloudAliasCache` / `icloudAliasCacheAt`,用于在 iCloud 会话或网络上下文短暂波动时回退展示最近可用列表 +- iCloud 相关偏好 - LuckMail API 配置 - 自动运行默认配置 - 账号运行历史 `accountRunHistory`(以邮箱为主键,保存该邮箱最近一次状态:成功/失败/停止) @@ -453,7 +446,6 @@ 补充: - HeroSMS 号码当前最多复用 3 次成功注册;超过上限后会清空可复用激活记录,下次重新申请新号码。 -- HeroSMS 新号码申请会先查询当前国家与 OpenAI 服务的最低价并携带固定价格参数;如果 `getNumber` 返回 `NO_NUMBERS`,会回退到 `getNumberV2`,后续用 `getStatusV2` 轮询验证码。 - 如果同一个号码在重发短信后 60 秒仍收不到验证码,后台会抛出“回到步骤 7 重新拿新号码”的恢复错误,而不是把当前号码无限重试下去。 ### Step 10 @@ -487,35 +479,6 @@ Codex2API 补充: - 步骤 10 会先尝试提交已捕获的 callback URL,随后轮询公开贡献状态,直到进入最终态 - `auto_approved` 与 `manual_review_required` 视为主流程完成;`auto_rejected / expired / error` 视为当前轮失败 -## 6.1 Plus 模式链路 - -Plus 模式通过 `plusModeEnabled` 开启,目标是在普通注册资料完成后,不执行原 Step 6 Cookie 清理,也不执行原 Step 8 登录验证码步骤,而是先完成 Plus Checkout 与 PayPal 授权,再复用现有 OAuth 后半段。 - -Plus 模式可见步骤: - -1. 第 1~5 步:沿用普通注册入口、邮箱、密码、注册验证码、资料填写链路。 -2. 第 6 步 `创建 Plus Checkout`:打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session,并打开 `https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`。 -3. 第 7 步 `填写账单并提交订阅`:选择 PayPal,生成账单全名,从 `data/address-sources.js` 读取同国家 seed query,触发 checkout 内置 Google 地址推荐,选择推荐项并校验地址第 1 行、城市、州/省、邮编等结构化字段,再点击“订阅”。运行时会按 Stripe iframe 拆分执行:付款方式在 `elements-inner-payment` frame,账单地址在 `elements-inner-address` frame,Google 推荐在 `elements-inner-autocompl` frame 时单独点击推荐项。 -4. 第 8 步 `PayPal 登录与授权`:在 PayPal 页面填写 `paypalEmail / paypalPassword`,登录前固定等待 1 秒,关闭可见通行密钥提示,点击“同意并继续”。 -5. 第 9 步 `订阅回跳确认`:等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。 -6. 第 10 步:复用原 Step 7 OAuth 登录执行器,但状态和日志按 Plus 可见第 10 步记录。 -7. 第 11 步:复用原 Step 8 登录验证码执行器,但状态和日志按 Plus 可见第 11 步记录。 -8. 第 12 步:复用原 Step 9 OAuth 同意页点击和 localhost callback 捕获执行器,但状态和日志按 Plus 可见第 12 步记录。 -9. 第 13 步:复用原 Step 10 CPA / SUB2API / Codex2API 平台回调验证执行器,但状态和日志按 Plus 可见第 13 步记录。 - -隐藏与跳过规则: - -- 原 Step 6 `清理登录 Cookies`:Plus 模式下隐藏且不执行,因为 Plus Checkout 创建依赖当前 ChatGPT 登录态。 -- 原 Step 8 `获取登录验证码`:Plus 模式下移动到可见第 11 步,继续负责登录验证码页的收码与提交。 -- 原 Step 9 不能跳过;它负责点击 OAuth 同意页并捕获 `localhostUrl`。 -- 原 Step 10 不能跳过;它依赖 `localhostUrl` 完成平台侧账号创建或回调验证。 - -等待模型: - -- Plus Checkout 和 PayPal 专用步骤使用“无限等待但可停止”的后台等待 helper。 -- 每次页面加载完成后固定等待 1 秒,再继续下一次输入、点击或状态判断。 -- 第一版不实时抓取外部地址网站;地址自动化只依赖本地 seed query 和 checkout 页面内置 Google 地址推荐。 - ## 7. 邮箱与 provider 链路 ### 7.1 2026-04-17 补充:Gmail / 2925 统一别名邮箱链路 @@ -642,7 +605,6 @@ Plus 模式可见步骤: - `163`、`163 VIP`、`126` 都走同一条“网易网页邮箱”验证码链路。 - sidepanel 只负责切换 provider 与展示登录入口;后台根据 provider 选择对应网页邮箱首页。 - 内容脚本来源统一归类到 `mail-163`,这样 Step 4 / Step 8 继续复用同一套验证码读取与邮件清理逻辑。 -- `mail-provider-utils.js` 同时承接 iCloud 转发收码可选 provider 的归一化与入口配置,避免 background / sidepanel 重新复制 QQ、网易和 Gmail 的收码地址、label 与注入脚本。 - `manifest.json` 需要同时覆盖: - `https://mail.163.com/*` - `https://webmail.vip.163.com/*` @@ -737,33 +699,8 @@ Plus 模式可见步骤: 组成: - [icloud-utils.js](c:/Users/projectf/Downloads/codex注册扩展/icloud-utils.js) -- [mail-provider-utils.js](c:/Users/projectf/Downloads/codex注册扩展/mail-provider-utils.js) - [content/icloud-mail.js](c:/Users/projectf/Downloads/codex注册扩展/content/icloud-mail.js) -配置: - -- `icloudHostPreference` 只决定 iCloud 登录、别名管理和 iCloud Mail 收件箱 Host。 -- `icloudFetchMode` 决定生成注册邮箱时复用已有 Hide My Email 别名,还是始终创建新别名。 -- `icloudTargetMailboxType = icloud-inbox` 时,Step 4 / Step 8 直接打开 iCloud Mail 收件箱轮询验证码。 -- `icloudTargetMailboxType = forward-mailbox` 时,注册邮箱仍由 iCloud Hide My Email 生成,但 Step 4 / Step 8 改为打开 `icloudForwardMailProvider` 指定的转发目标邮箱收码;当前支持 `qq / 163 / 163-vip / 126 / gmail`。 - -Hide My Email 获取与管理链路: - -1. 后台优先通过 `setup.icloud.com*/setup/ws/1/validate` 解析 `premiummailsettings.url`,并记录最近可用服务节点。 -2. 如果后台请求受 401 / 403 / 409 / 421 / 429 / 5xx、CORS、超时或地址空间限制影响,会优先尝试在已打开或自动创建的 `www.icloud.com*/mail/` 页面主上下文中重放请求。 -3. `maildomainws` 请求会补充 `clientBuildNumber / clientMasteringNumber / clientId / dsid` 查询参数,写请求使用 `text/plain;charset=UTF-8`,以兼容 iCloud 网页端接口。 -4. 别名列表加载成功后会缓存到 `icloudAliasCache`;遇到短暂上下文波动时,列表展示会按“最近缓存 -> 历史缓存 -> 本地已用/保留记录”回退。 -5. 生成新别名后若 `reserve` 返回鉴权或网络异常,会先回查列表确认候选别名是否已经创建;必要时刷新服务节点重试一次,最后才进入可复用别名回退或暂停等待用户处理。 -6. 自动运行获取 iCloud 别名时,检测到会话或网络上下文异常后会停止重复消耗获取重试次数,转入等待邮箱状态,避免连续请求放大 iCloud 风控或上下文抖动。 - -维护约定: - -- iCloud 转发目标邮箱 provider 的 label、URL、source、inject 配置统一由 `mail-provider-utils.js` 输出。 -- `background.js` 只根据 `icloudTargetMailboxType` 选择 iCloud Mail 收件箱或共享转发邮箱配置,不再在主文件内硬编码各 provider 地址。 -- `sidepanel/sidepanel.js` 只负责展示、保存和回显目标邮箱类型 / 转发邮箱 provider,不重复定义 provider 业务清单。 -- iCloud 登录态提示必须区分“真实未登录”和“已登录但请求上下文波动”;不能把所有 401 / 403 / 421 都直接提示为未登录。 -- iCloud 别名缓存只用于短暂失败回退,不应改变最终的已用 / 保留状态来源;状态仍以 `manualAliasUsage`、`preservedAliases` 与最新线上列表合并后的结果为准。 - ## 8. 自动运行完整链路 文件: @@ -779,11 +716,9 @@ Hide My Email 获取与管理链路: - 如果当前 `Mail = 自定义邮箱` 且配置了 `customMailProviderPool`,会先按当前目标轮次把号池中的对应邮箱写回运行态 - 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态 5. 执行 `runAutoSequenceFromStep` - - 自动运行会按当前 `plusModeEnabled` 选择普通 10 步或 Plus 13 步可见步骤;Plus 模式下第 6~9 步走 checkout / PayPal,第 10~13 步复用 OAuth 后半段 - 步骤 7 内部仍保留登录态恢复的有限重试,但 `add-phone / 手机号页` 属于立即跳出的不可重试错误 - 步骤 8 若在验证码提交后进入 `add-phone / 手机号页`,会直接抛出 fatal 错误,不再先标记步骤成功 - - 普通模式一旦进入步骤 7~10,遇到普通报错且认证流程未进入 `add-phone`,则自动回到步骤 7 无限重开 - - Plus 模式在 checkout / PayPal 结束后,如果 OAuth 后半段遇到普通报错且认证流程未进入 `add-phone`,则自动回到 Plus 可见步骤 10 重新开始授权链路 + - 一旦进入步骤 7~10,遇到普通报错且认证流程未进入 `add-phone`,则自动回到步骤 7 无限重开 - 如果命中 `add-phone / 手机号页` 这类 fatal 错误,则不会再做当前轮的内部重试;当开启自动重试/跳过失败时,会直接结束当前轮并继续下一轮,而不是把整条自动流程暂停 - 如果是手动停止,则立即退出自动流程,不会再触发“回到步骤 7 重开” 6. 如果失败,根据设置决定:
+ + QQ交流群,便于大家交流 + 最新版本运行日志