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. 如果失败,根据设置决定: