10711 lines
374 KiB
JavaScript
10711 lines
374 KiB
JavaScript
// background.js — Service Worker: orchestration, state, tab management, message routing
|
||
|
||
importScripts(
|
||
'managed-alias-utils.js',
|
||
'mail2925-utils.js',
|
||
'paypal-utils.js',
|
||
'background/phone-verification-flow.js',
|
||
'background/account-run-history.js',
|
||
'background/contribution-oauth.js',
|
||
'background/mail-2925-session.js',
|
||
'background/paypal-account-store.js',
|
||
'background/ip-proxy-provider-711proxy.js',
|
||
'background/ip-proxy-core.js',
|
||
'background/panel-bridge.js',
|
||
'background/generated-email-helpers.js',
|
||
'background/signup-flow-helpers.js',
|
||
'background/message-router.js',
|
||
'background/verification-flow.js',
|
||
'background/auto-run-controller.js',
|
||
'background/tab-runtime.js',
|
||
'background/navigation-utils.js',
|
||
'background/logging-status.js',
|
||
'background/steps/registry.js',
|
||
'data/step-definitions.js',
|
||
'data/address-sources.js',
|
||
'background/steps/open-chatgpt.js',
|
||
'background/steps/submit-signup-email.js',
|
||
'background/steps/fill-password.js',
|
||
'background/steps/fetch-signup-code.js',
|
||
'background/steps/fill-profile.js',
|
||
'background/steps/clear-login-cookies.js',
|
||
'background/steps/create-plus-checkout.js',
|
||
'background/steps/fill-plus-checkout.js',
|
||
'background/steps/gopay-manual-confirm.js',
|
||
'background/steps/paypal-approve.js',
|
||
'background/steps/plus-return-confirm.js',
|
||
'background/steps/oauth-login.js',
|
||
'background/steps/fetch-login-code.js',
|
||
'background/steps/confirm-oauth.js',
|
||
'background/steps/platform-verify.js',
|
||
'data/names.js',
|
||
'hotmail-utils.js',
|
||
'microsoft-email.js',
|
||
'luckmail-utils.js',
|
||
'cloudflare-temp-email-utils.js',
|
||
'icloud-utils.js',
|
||
'mail-provider-utils.js',
|
||
'content/activation-utils.js'
|
||
);
|
||
|
||
const NORMAL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled: false }) || [];
|
||
const PLUS_PAYPAL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||
plusModeEnabled: true,
|
||
plusPaymentMethod: 'paypal',
|
||
}) || NORMAL_STEP_DEFINITIONS;
|
||
const PLUS_GOPAY_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||
plusModeEnabled: true,
|
||
plusPaymentMethod: 'gopay',
|
||
}) || PLUS_PAYPAL_STEP_DEFINITIONS;
|
||
const PLUS_STEP_DEFINITIONS = PLUS_PAYPAL_STEP_DEFINITIONS;
|
||
const ALL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getAllSteps?.() || [
|
||
...NORMAL_STEP_DEFINITIONS,
|
||
...PLUS_PAYPAL_STEP_DEFINITIONS,
|
||
...PLUS_GOPAY_STEP_DEFINITIONS,
|
||
];
|
||
const STEP_IDS = Array.from(new Set(ALL_STEP_DEFINITIONS
|
||
.map((definition) => Number(definition?.id))
|
||
.filter(Number.isFinite)))
|
||
.sort((left, right) => left - right);
|
||
const NORMAL_STEP_IDS = NORMAL_STEP_DEFINITIONS
|
||
.map((definition) => Number(definition?.id))
|
||
.filter(Number.isFinite)
|
||
.sort((left, right) => left - right);
|
||
const PLUS_PAYPAL_STEP_IDS = PLUS_PAYPAL_STEP_DEFINITIONS
|
||
.map((definition) => Number(definition?.id))
|
||
.filter(Number.isFinite)
|
||
.sort((left, right) => left - right);
|
||
const PLUS_GOPAY_STEP_IDS = PLUS_GOPAY_STEP_DEFINITIONS
|
||
.map((definition) => Number(definition?.id))
|
||
.filter(Number.isFinite)
|
||
.sort((left, right) => left - right);
|
||
const LAST_STEP_ID = Math.max(
|
||
NORMAL_STEP_IDS[NORMAL_STEP_IDS.length - 1] || 10,
|
||
PLUS_PAYPAL_STEP_IDS[PLUS_PAYPAL_STEP_IDS.length - 1] || 10,
|
||
PLUS_GOPAY_STEP_IDS[PLUS_GOPAY_STEP_IDS.length - 1] || 10
|
||
);
|
||
const FINAL_OAUTH_CHAIN_START_STEP = 7;
|
||
|
||
const {
|
||
extractVerificationCodeFromMessage,
|
||
filterHotmailAccountsByUsage,
|
||
getLatestHotmailMessage,
|
||
getHotmailMailApiRequestConfig,
|
||
getHotmailVerificationPollConfig,
|
||
getHotmailVerificationRequestTimestamp,
|
||
normalizeHotmailServiceMode,
|
||
normalizeHotmailMailApiMessages,
|
||
pickHotmailAccountForRun,
|
||
pickVerificationMessage,
|
||
pickVerificationMessageWithFallback,
|
||
pickVerificationMessageWithTimeFallback,
|
||
shouldClearHotmailCurrentSelection,
|
||
} = self.HotmailUtils;
|
||
const {
|
||
MAIL2925_LIMIT_COOLDOWN_MS,
|
||
findMail2925Account,
|
||
getMail2925AccountStatus,
|
||
normalizeMail2925Account,
|
||
normalizeMail2925Accounts,
|
||
parseMail2925ImportText,
|
||
pickMail2925AccountForRun,
|
||
upsertMail2925AccountInList,
|
||
} = self.Mail2925Utils;
|
||
const {
|
||
fetchMicrosoftMailboxMessages,
|
||
} = self.MultiPageMicrosoftEmail;
|
||
const {
|
||
DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
|
||
DEFAULT_LUCKMAIL_BASE_URL,
|
||
DEFAULT_LUCKMAIL_EMAIL_TYPE,
|
||
buildLuckmailBaselineCursor,
|
||
buildLuckmailMailCursor,
|
||
filterReusableLuckmailPurchases,
|
||
isLuckmailMailNewerThanCursor,
|
||
isLuckmailPurchaseReusable,
|
||
isLuckmailPurchaseForProject,
|
||
isLuckmailPurchasePreserved,
|
||
normalizeLuckmailBaseUrl,
|
||
normalizeLuckmailEmailType,
|
||
normalizeLuckmailMailCursor,
|
||
normalizeLuckmailProjectName,
|
||
normalizeLuckmailPurchase,
|
||
normalizeLuckmailPurchaseId,
|
||
normalizeLuckmailPurchaseListPage,
|
||
normalizeLuckmailPurchases,
|
||
normalizeLuckmailTags,
|
||
normalizeLuckmailTokenCode,
|
||
normalizeLuckmailTokenMail,
|
||
normalizeLuckmailTokenMails,
|
||
normalizeLuckmailUsedPurchases,
|
||
normalizeTimestamp: normalizeLuckmailTimestamp,
|
||
pickLuckmailVerificationMail,
|
||
} = self.LuckMailUtils;
|
||
const {
|
||
DEFAULT_MAIL_PAGE_SIZE: CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE,
|
||
buildCloudflareTempEmailHeaders,
|
||
getCloudflareTempEmailAddressFromResponse,
|
||
joinCloudflareTempEmailUrl,
|
||
normalizeCloudflareTempEmailAddress,
|
||
normalizeCloudflareTempEmailBaseUrl,
|
||
normalizeCloudflareTempEmailDomain,
|
||
normalizeCloudflareTempEmailDomains,
|
||
normalizeCloudflareTempEmailMailApiMessages,
|
||
} = self.CloudflareTempEmailUtils;
|
||
const {
|
||
findIcloudAliasByEmail,
|
||
getConfiguredIcloudHostPreference,
|
||
getIcloudHostHintFromMessage,
|
||
getIcloudLoginUrlForHost,
|
||
getIcloudMailUrlForHost,
|
||
getIcloudSetupUrlForHost,
|
||
normalizeBooleanMap,
|
||
normalizeIcloudAliasList,
|
||
normalizeIcloudAliasRecord,
|
||
normalizeIcloudHost,
|
||
pickReusableIcloudAlias,
|
||
toNormalizedEmailSet,
|
||
} = self.IcloudUtils;
|
||
const {
|
||
getIcloudForwardMailConfig: getSharedIcloudForwardMailConfig,
|
||
normalizeIcloudForwardMailProvider,
|
||
normalizeIcloudTargetMailboxType,
|
||
} = self.MailProviderUtils;
|
||
const {
|
||
isRecoverableStep9AuthFailure,
|
||
} = self.MultiPageActivationUtils;
|
||
|
||
const LOG_PREFIX = '[MultiPage:bg]';
|
||
const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill';
|
||
const ICLOUD_SETUP_URLS = [
|
||
'https://setup.icloud.com.cn/setup/ws/1',
|
||
'https://setup.icloud.com/setup/ws/1',
|
||
];
|
||
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';
|
||
const HOTMAIL_PROVIDER = 'hotmail-api';
|
||
const LUCKMAIL_PROVIDER = 'luckmail-api';
|
||
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
|
||
const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
|
||
const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool';
|
||
const HOTMAIL_MAILBOXES = ['INBOX', 'Junk'];
|
||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||
const CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX = 'CF_SECURITY_BLOCKED::';
|
||
const CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE = '您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器';
|
||
const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::';
|
||
const HUMAN_STEP_DELAY_MIN = 700;
|
||
const HUMAN_STEP_DELAY_MAX = 2200;
|
||
const STEP6_MAX_ATTEMPTS = 3;
|
||
const STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS = 8;
|
||
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
|
||
const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000;
|
||
const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000;
|
||
const DEFAULT_SUB2API_URL = 'https://sub2api.hisence.fun/admin/accounts';
|
||
const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
|
||
const DEFAULT_SUB2API_GROUP_NAME = 'codex';
|
||
const DEFAULT_SUB2API_PROXY_NAME = '';
|
||
const CONTRIBUTION_SOURCE_CPA = 'cpa';
|
||
const CONTRIBUTION_SOURCE_SUB2API = 'sub2api';
|
||
const CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME = 'codex号池';
|
||
const CONTRIBUTION_SUB2API_PLUS_GROUP_NAME = 'openai-plus';
|
||
const DEFAULT_SUB2API_REDIRECT_URI = 'http://localhost:1455/auth/callback';
|
||
const DEFAULT_IP_PROXY_SERVICE = '711proxy';
|
||
const IP_PROXY_SERVICE_VALUES = ['711proxy', 'lumiproxy', 'iproyal', 'omegaproxy'];
|
||
const IP_PROXY_ENABLED_SERVICE_VALUES = ['711proxy'];
|
||
const DEFAULT_IP_PROXY_MODE = 'account';
|
||
const IP_PROXY_MODE_VALUES = ['api', 'account'];
|
||
const DEFAULT_IP_PROXY_PROTOCOL = 'http';
|
||
const IP_PROXY_PROTOCOL_VALUES = ['http', 'https', 'socks4', 'socks5'];
|
||
const IP_PROXY_FETCH_TIMEOUT_MS = 20000;
|
||
const IP_PROXY_SETTINGS_SCOPE = 'regular';
|
||
const IP_PROXY_BYPASS_LIST = ['<local>', 'localhost', '127.0.0.1'];
|
||
const IP_PROXY_ROUTE_ALL_TRAFFIC = true;
|
||
const IP_PROXY_ACCOUNT_LIST_ENABLED = false;
|
||
const IP_PROXY_INIT_ENABLE_EXIT_PROBE = false;
|
||
const IP_PROXY_INIT_SUPPRESS_AUTH_REBIND = true;
|
||
const IP_PROXY_INIT_AUTO_APPLY = false;
|
||
const IP_PROXY_TARGET_HOST_PATTERNS = [
|
||
'openai.com',
|
||
'*.openai.com',
|
||
'chatgpt.com',
|
||
'*.chatgpt.com',
|
||
'ipwho.is',
|
||
'*.ipwho.is',
|
||
'ipapi.co',
|
||
'*.ipapi.co',
|
||
'ipinfo.io',
|
||
'*.ipinfo.io',
|
||
'api.ipify.org',
|
||
'api64.ipify.org',
|
||
'api.ip.cc',
|
||
'ifconfig.me',
|
||
'checkip.amazonaws.com',
|
||
'ipv4.icanhazip.com',
|
||
'ident.me',
|
||
'httpbin.org',
|
||
'ip-api.com',
|
||
'myip.ipip.net',
|
||
];
|
||
const AUTO_RUN_TIMER_ALARM_NAME = 'auto-run-timer';
|
||
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
|
||
const AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS = 'between_rounds';
|
||
const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry';
|
||
const AUTO_RUN_DELAY_MIN_MINUTES = 1;
|
||
const AUTO_RUN_DELAY_MAX_MINUTES = 1440;
|
||
const AUTO_RUN_RETRY_DELAY_MS = 3000;
|
||
const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
|
||
const AUTO_STEP_DELAY_MIN_ALLOWED_SECONDS = 0;
|
||
const AUTO_STEP_DELAY_MAX_ALLOWED_SECONDS = 600;
|
||
const VERIFICATION_RESEND_COUNT_MIN = 0;
|
||
const VERIFICATION_RESEND_COUNT_MAX = 20;
|
||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
|
||
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
|
||
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
|
||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
|
||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
|
||
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
|
||
const PHONE_CODE_POLL_ROUNDS_MIN = 1;
|
||
const PHONE_CODE_POLL_ROUNDS_MAX = 120;
|
||
const DEFAULT_PHONE_CODE_POLL_ROUNDS = 4;
|
||
const LEGACY_AUTO_STEP_DELAY_KEYS = ['autoStepRandomDelayMinSeconds', 'autoStepRandomDelayMaxSeconds'];
|
||
const LEGACY_VERIFICATION_RESEND_COUNT_KEYS = ['signupVerificationResendCount', 'loginVerificationResendCount'];
|
||
const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
|
||
const MAIL_2925_MODE_PROVIDE = 'provide';
|
||
const MAIL_2925_MODE_RECEIVE = 'receive';
|
||
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
|
||
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
|
||
const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
|
||
const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
|
||
const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373';
|
||
const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = DEFAULT_HOTMAIL_LOCAL_BASE_URL;
|
||
const HOTMAIL_LOCAL_HELPER_TIMEOUT_MS = 45000;
|
||
const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
|
||
const DEFAULT_HERO_SMS_BASE_URL = 'https://hero-sms.com/stubs/handler_api.php';
|
||
const HERO_SMS_SERVICE_CODE = 'dr';
|
||
const HERO_SMS_SERVICE_LABEL = 'OpenAI';
|
||
const HERO_SMS_COUNTRY_ID = 52;
|
||
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||
const DISPLAY_TIMEZONE = 'Asia/Shanghai';
|
||
const MICROSOFT_TOKEN_DNR_RULE_ID = 1001;
|
||
const PERSISTENT_ALIAS_STATE_KEYS = [
|
||
'manualAliasUsage',
|
||
'preservedAliases',
|
||
'icloudAliasCache',
|
||
'icloudAliasCacheAt',
|
||
];
|
||
const ACCOUNT_RUN_HISTORY_STORAGE_KEY = 'accountRunHistory';
|
||
const CONTRIBUTION_RUNTIME_DEFAULTS = self.MultiPageBackgroundContributionOAuth?.RUNTIME_DEFAULTS || {
|
||
contributionMode: false,
|
||
contributionModeExpected: false,
|
||
contributionSource: CONTRIBUTION_SOURCE_SUB2API,
|
||
contributionTargetGroupName: CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME,
|
||
contributionNickname: '',
|
||
contributionQq: '',
|
||
contributionSessionId: '',
|
||
contributionAuthUrl: '',
|
||
contributionAuthState: '',
|
||
contributionCallbackUrl: '',
|
||
contributionStatus: '',
|
||
contributionStatusMessage: '',
|
||
contributionLastPollAt: 0,
|
||
contributionCallbackStatus: 'idle',
|
||
contributionCallbackMessage: '',
|
||
contributionAuthOpenedAt: 0,
|
||
contributionAuthTabId: 0,
|
||
};
|
||
const CONTRIBUTION_RUNTIME_KEYS = self.MultiPageBackgroundContributionOAuth?.RUNTIME_KEYS
|
||
|| Object.keys(CONTRIBUTION_RUNTIME_DEFAULTS);
|
||
|
||
function isPlusModeState(state = {}) {
|
||
return Boolean(state?.plusModeEnabled);
|
||
}
|
||
|
||
function normalizePlusPaymentMethod(value = '') {
|
||
return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal';
|
||
}
|
||
|
||
function normalizeContributionModeSource(value = '') {
|
||
const normalized = String(value || '').trim().toLowerCase();
|
||
return normalized === CONTRIBUTION_SOURCE_SUB2API
|
||
? CONTRIBUTION_SOURCE_SUB2API
|
||
: CONTRIBUTION_SOURCE_CPA;
|
||
}
|
||
|
||
function resolveContributionModeRoutingState(state = {}) {
|
||
const currentStatus = String(state?.contributionStatus || '').trim().toLowerCase();
|
||
const currentSource = normalizeContributionModeSource(state?.contributionSource);
|
||
const hasActiveSession = Boolean(
|
||
String(state?.contributionSessionId || '').trim()
|
||
&& currentStatus
|
||
&& !['auto_approved', 'auto_rejected', 'expired', 'error'].includes(currentStatus)
|
||
);
|
||
|
||
if (hasActiveSession) {
|
||
return {
|
||
source: currentSource,
|
||
targetGroupName: currentSource === CONTRIBUTION_SOURCE_SUB2API
|
||
? (String(state?.contributionTargetGroupName || '').trim() || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME)
|
||
: '',
|
||
};
|
||
}
|
||
|
||
const source = CONTRIBUTION_SOURCE_SUB2API;
|
||
return {
|
||
source,
|
||
targetGroupName: isPlusModeState(state)
|
||
? CONTRIBUTION_SUB2API_PLUS_GROUP_NAME
|
||
: (String(state?.contributionTargetGroupName || '').trim() || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME),
|
||
};
|
||
}
|
||
|
||
function getStepDefinitionsForState(state = {}) {
|
||
if (!isPlusModeState(state)) {
|
||
return NORMAL_STEP_DEFINITIONS;
|
||
}
|
||
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay'
|
||
? PLUS_GOPAY_STEP_DEFINITIONS
|
||
: PLUS_PAYPAL_STEP_DEFINITIONS;
|
||
}
|
||
|
||
function getStepIdsForState(state = {}) {
|
||
if (!isPlusModeState(state)) {
|
||
return NORMAL_STEP_IDS;
|
||
}
|
||
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay'
|
||
? PLUS_GOPAY_STEP_IDS
|
||
: PLUS_PAYPAL_STEP_IDS;
|
||
}
|
||
|
||
function getLastStepIdForState(state = {}) {
|
||
const ids = getStepIdsForState(state);
|
||
return ids[ids.length - 1] || 10;
|
||
}
|
||
|
||
function getAuthChainStartStepId(state = {}) {
|
||
return isPlusModeState(state) ? 10 : FINAL_OAUTH_CHAIN_START_STEP;
|
||
}
|
||
|
||
function getStepDefinitionForState(step, state = {}) {
|
||
const numericStep = Number(step);
|
||
return getStepDefinitionsForState(state).find((definition) => Number(definition.id) === numericStep) || null;
|
||
}
|
||
|
||
initializeSessionStorageAccess();
|
||
setupDeclarativeNetRequestRules();
|
||
|
||
function setupDeclarativeNetRequestRules() {
|
||
if (!chrome.declarativeNetRequest?.updateDynamicRules) {
|
||
return;
|
||
}
|
||
|
||
chrome.declarativeNetRequest.updateDynamicRules({
|
||
removeRuleIds: [MICROSOFT_TOKEN_DNR_RULE_ID],
|
||
addRules: [{
|
||
id: MICROSOFT_TOKEN_DNR_RULE_ID,
|
||
priority: 1,
|
||
action: {
|
||
type: 'modifyHeaders',
|
||
requestHeaders: [
|
||
{ header: 'Origin', operation: 'remove' },
|
||
],
|
||
},
|
||
condition: {
|
||
urlFilter: 'login.microsoftonline.com/*/oauth2/v2.0/token',
|
||
resourceTypes: ['xmlhttprequest'],
|
||
},
|
||
}],
|
||
}).catch((error) => {
|
||
console.warn(LOG_PREFIX, 'Failed to setup declarativeNetRequest rules:', error?.message || error);
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// 状态管理(chrome.storage.session + chrome.storage.local)
|
||
// ============================================================
|
||
|
||
const PERSISTED_SETTING_DEFAULTS = {
|
||
panelMode: 'cpa',
|
||
vpsUrl: '',
|
||
vpsPassword: '',
|
||
localCpaStep9Mode: DEFAULT_LOCAL_CPA_STEP9_MODE,
|
||
sub2apiUrl: DEFAULT_SUB2API_URL,
|
||
sub2apiEmail: '',
|
||
sub2apiPassword: '',
|
||
sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME,
|
||
sub2apiDefaultProxyName: DEFAULT_SUB2API_PROXY_NAME,
|
||
ipProxyEnabled: false,
|
||
ipProxyService: DEFAULT_IP_PROXY_SERVICE,
|
||
ipProxyMode: DEFAULT_IP_PROXY_MODE,
|
||
ipProxyApiUrl: '',
|
||
ipProxyServiceProfiles: {},
|
||
ipProxyAccountList: '',
|
||
ipProxyAccountSessionPrefix: '',
|
||
ipProxyAccountLifeMinutes: '',
|
||
ipProxyPoolTargetCount: '20',
|
||
ipProxyHost: '',
|
||
ipProxyPort: '',
|
||
ipProxyProtocol: DEFAULT_IP_PROXY_PROTOCOL,
|
||
ipProxyUsername: '',
|
||
ipProxyPassword: '',
|
||
ipProxyRegion: '',
|
||
codex2apiUrl: DEFAULT_CODEX2API_URL,
|
||
codex2apiAdminKey: '',
|
||
customPassword: '',
|
||
plusModeEnabled: false,
|
||
plusPaymentMethod: 'paypal',
|
||
paypalEmail: '',
|
||
paypalPassword: '',
|
||
currentPayPalAccountId: '',
|
||
autoRunSkipFailures: false,
|
||
autoRunFallbackThreadIntervalMinutes: 0,
|
||
autoRunDelayEnabled: false,
|
||
autoRunDelayMinutes: 30,
|
||
autoStepDelaySeconds: null,
|
||
phoneVerificationEnabled: false,
|
||
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
|
||
phoneVerificationReplacementLimit: DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT,
|
||
phoneCodeWaitSeconds: DEFAULT_PHONE_CODE_WAIT_SECONDS,
|
||
phoneCodeTimeoutWindows: DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS,
|
||
phoneCodePollIntervalSeconds: DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS,
|
||
phoneCodePollMaxRounds: DEFAULT_PHONE_CODE_POLL_ROUNDS,
|
||
mailProvider: '163',
|
||
mail2925Mode: DEFAULT_MAIL_2925_MODE,
|
||
mail2925UseAccountPool: false,
|
||
emailGenerator: 'duck',
|
||
customMailProviderPool: [],
|
||
customEmailPool: [],
|
||
autoDeleteUsedIcloudAlias: false,
|
||
icloudHostPreference: 'auto',
|
||
icloudTargetMailboxType: 'icloud-inbox',
|
||
icloudForwardMailProvider: 'qq',
|
||
icloudFetchMode: 'reuse_existing',
|
||
accountRunHistoryTextEnabled: true,
|
||
accountRunHistoryHelperBaseUrl: DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL,
|
||
gmailBaseEmail: '',
|
||
mail2925BaseEmail: '',
|
||
currentMail2925AccountId: '',
|
||
emailPrefix: '',
|
||
inbucketHost: '',
|
||
inbucketMailbox: '',
|
||
hotmailServiceMode: HOTMAIL_SERVICE_MODE_LOCAL,
|
||
hotmailRemoteBaseUrl: DEFAULT_HOTMAIL_REMOTE_BASE_URL,
|
||
hotmailLocalBaseUrl: DEFAULT_HOTMAIL_LOCAL_BASE_URL,
|
||
luckmailApiKey: '',
|
||
luckmailBaseUrl: DEFAULT_LUCKMAIL_BASE_URL,
|
||
luckmailEmailType: DEFAULT_LUCKMAIL_EMAIL_TYPE,
|
||
luckmailDomain: '',
|
||
cloudflareDomain: '',
|
||
cloudflareDomains: [],
|
||
cloudflareTempEmailBaseUrl: '',
|
||
cloudflareTempEmailAdminAuth: '',
|
||
cloudflareTempEmailCustomAuth: '',
|
||
cloudflareTempEmailReceiveMailbox: '',
|
||
cloudflareTempEmailUseRandomSubdomain: false,
|
||
cloudflareTempEmailDomain: '',
|
||
cloudflareTempEmailDomains: [],
|
||
hotmailAccounts: [],
|
||
mail2925Accounts: [],
|
||
paypalAccounts: [],
|
||
heroSmsApiKey: '',
|
||
heroSmsReuseEnabled: DEFAULT_HERO_SMS_REUSE_ENABLED,
|
||
heroSmsAcquirePriority: DEFAULT_HERO_SMS_ACQUIRE_PRIORITY,
|
||
heroSmsMaxPrice: '',
|
||
heroSmsCountryId: HERO_SMS_COUNTRY_ID,
|
||
heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL,
|
||
heroSmsCountryFallback: [],
|
||
};
|
||
|
||
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
||
const SETTINGS_EXPORT_SCHEMA_VERSION = 1;
|
||
const SETTINGS_EXPORT_FILENAME_PREFIX = 'multipage-settings';
|
||
const STEP6_PRE_LOGIN_COOKIE_CLEAR_DELAY_MS = 25000;
|
||
const PRE_LOGIN_COOKIE_CLEAR_DOMAINS = [
|
||
'chatgpt.com',
|
||
'chat.openai.com',
|
||
'openai.com',
|
||
'auth.openai.com',
|
||
'auth0.openai.com',
|
||
'accounts.openai.com',
|
||
];
|
||
const PRE_LOGIN_COOKIE_CLEAR_ORIGINS = [
|
||
'https://chatgpt.com',
|
||
'https://chat.openai.com',
|
||
'https://auth.openai.com',
|
||
'https://auth0.openai.com',
|
||
'https://accounts.openai.com',
|
||
'https://openai.com',
|
||
];
|
||
|
||
const DEFAULT_STATE = {
|
||
currentStep: 0, // 当前流程执行到的步骤编号。
|
||
stepStatuses: Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending'])),
|
||
...CONTRIBUTION_RUNTIME_DEFAULTS,
|
||
oauthUrl: null, // 运行时抓取到的 OAuth 地址,不要手动预填。
|
||
email: null, // 运行时邮箱,由程序自动获取并写入,不能手动预填。
|
||
password: null, // 运行时实际密码,由 customPassword 或程序自动生成后写入。
|
||
accounts: [], // 已生成账号记录:{ email, password, createdAt }。
|
||
accountRunHistory: [], // 账号运行历史快照,实际持久化在 chrome.storage.local。
|
||
manualAliasUsage: {},
|
||
preservedAliases: {},
|
||
icloudAliasCache: [],
|
||
icloudAliasCacheAt: 0,
|
||
lastEmailTimestamp: null, // 最近一次获取到邮箱数据的运行时时间戳。
|
||
lastSignupCode: null, // 注册验证码,运行时由程序自动读取并写入。
|
||
lastLoginCode: null, // 登录验证码,运行时由程序自动读取并写入。
|
||
localhostUrl: null, // 运行时捕获到的 localhost 回调地址,不要手动预填。
|
||
cpaOAuthState: null, // CPA OAuth state。
|
||
cpaManagementOrigin: null, // CPA 管理接口 origin。
|
||
sub2apiSessionId: null, // SUB2API OpenAI Auth 会话 ID。
|
||
sub2apiOAuthState: null, // SUB2API OpenAI Auth state。
|
||
sub2apiGroupId: null, // SUB2API 目标分组 ID。
|
||
sub2apiDraftName: null, // SUB2API 本轮预生成的账号名称。
|
||
sub2apiProxyId: null, // SUB2API 本轮使用的代理 ID。
|
||
codex2apiSessionId: null, // Codex2API OAuth 会话 ID。
|
||
codex2apiOAuthState: null, // Codex2API OAuth state。
|
||
plusCheckoutTabId: null, // Plus checkout / PayPal 标签页 ID。
|
||
plusCheckoutUrl: null, // Plus checkout 运行时短链,不写入持久配置。
|
||
plusCheckoutCountry: 'DE',
|
||
plusCheckoutCurrency: 'EUR',
|
||
plusBillingCountryText: '',
|
||
plusBillingAddress: null,
|
||
plusPaypalApprovedAt: null,
|
||
plusReturnUrl: '',
|
||
plusManualConfirmationPending: false,
|
||
plusManualConfirmationRequestId: '',
|
||
plusManualConfirmationStep: 0,
|
||
plusManualConfirmationMethod: '',
|
||
plusManualConfirmationTitle: '',
|
||
plusManualConfirmationMessage: '',
|
||
flowStartTime: null, // 当前流程开始时间。
|
||
tabRegistry: {}, // 程序维护的标签页注册表。
|
||
sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。
|
||
logs: [], // 侧边栏展示的运行日志。
|
||
...PERSISTED_SETTING_DEFAULTS, // 合并 chrome.storage.local 中持久化保存的用户配置。
|
||
ipProxyApiPool: [],
|
||
ipProxyApiCurrentIndex: 0,
|
||
ipProxyApiCurrent: null,
|
||
ipProxyAccountPool: [],
|
||
ipProxyAccountCurrentIndex: 0,
|
||
ipProxyAccountCurrent: null,
|
||
ipProxyPool: [],
|
||
ipProxyCurrentIndex: 0,
|
||
ipProxyCurrent: null,
|
||
luckmailApiKey: '',
|
||
luckmailBaseUrl: DEFAULT_LUCKMAIL_BASE_URL,
|
||
luckmailEmailType: DEFAULT_LUCKMAIL_EMAIL_TYPE,
|
||
luckmailDomain: '',
|
||
luckmailUsedPurchases: {},
|
||
luckmailPreserveTagId: 0,
|
||
luckmailPreserveTagName: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
|
||
currentLuckmailPurchase: null,
|
||
currentLuckmailMailCursor: null,
|
||
currentPhoneActivation: null,
|
||
currentPhoneVerificationCode: '',
|
||
reusablePhoneActivation: null,
|
||
heroSmsLastPriceTiers: [],
|
||
heroSmsLastPriceCountryId: 0,
|
||
heroSmsLastPriceCountryLabel: '',
|
||
heroSmsLastPriceUserLimit: '',
|
||
heroSmsLastPriceAt: 0,
|
||
pendingPhoneActivationConfirmation: null,
|
||
autoRunning: false, // 当前是否处于自动运行中。
|
||
autoRunPhase: 'idle', // 当前自动运行阶段。
|
||
autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。
|
||
autoRunTotalRuns: 1, // 自动运行计划总轮数。
|
||
autoRunAttemptRun: 0, // 当前轮次的重试序号。
|
||
autoRunSessionId: 0,
|
||
autoRunRoundSummaries: [], // 自动运行轮次摘要。
|
||
scheduledAutoRunAt: null, // 自动运行计划启动时间戳。
|
||
autoRunTimerPlan: null, // 自动运行可恢复计时计划快照。
|
||
autoRunCountdownAt: null,
|
||
autoRunCountdownTitle: '',
|
||
autoRunCountdownNote: '',
|
||
signupVerificationRequestedAt: null,
|
||
loginVerificationRequestedAt: null,
|
||
oauthFlowDeadlineAt: null,
|
||
oauthFlowDeadlineSourceUrl: null,
|
||
currentPayPalAccountId: null,
|
||
currentHotmailAccountId: null,
|
||
currentMail2925AccountId: null,
|
||
preferredIcloudHost: '',
|
||
ipProxyApplied: false,
|
||
ipProxyAppliedReason: 'disabled',
|
||
ipProxyAppliedAt: 0,
|
||
ipProxyAppliedHost: '',
|
||
ipProxyAppliedPort: 0,
|
||
ipProxyAppliedRegion: '',
|
||
ipProxyAppliedHasAuth: false,
|
||
ipProxyAppliedProvider: DEFAULT_IP_PROXY_SERVICE,
|
||
ipProxyAppliedError: '',
|
||
ipProxyAppliedWarning: '',
|
||
ipProxyAppliedExitIp: '',
|
||
ipProxyAppliedExitRegion: '',
|
||
ipProxyAppliedExitDetecting: false,
|
||
ipProxyAppliedExitError: '',
|
||
ipProxyAppliedExitSource: '',
|
||
};
|
||
|
||
function normalizeAutoRunDelayMinutes(value) {
|
||
const numeric = Number(value);
|
||
if (!Number.isFinite(numeric)) {
|
||
return PERSISTED_SETTING_DEFAULTS.autoRunDelayMinutes;
|
||
}
|
||
return Math.min(
|
||
AUTO_RUN_DELAY_MAX_MINUTES,
|
||
Math.max(AUTO_RUN_DELAY_MIN_MINUTES, Math.floor(numeric))
|
||
);
|
||
}
|
||
|
||
function normalizeAutoRunFallbackThreadIntervalMinutes(value) {
|
||
const rawValue = String(value ?? '').trim();
|
||
if (!rawValue) {
|
||
return 0;
|
||
}
|
||
|
||
const numeric = Number(rawValue);
|
||
if (!Number.isFinite(numeric)) {
|
||
return 0;
|
||
}
|
||
|
||
return Math.min(
|
||
AUTO_RUN_DELAY_MAX_MINUTES,
|
||
Math.max(0, Math.floor(numeric))
|
||
);
|
||
}
|
||
|
||
function normalizeAutoStepDelaySeconds(value, fallback = null) {
|
||
const rawValue = String(value ?? '').trim();
|
||
if (!rawValue) {
|
||
return fallback;
|
||
}
|
||
|
||
const numeric = Number(rawValue);
|
||
if (!Number.isFinite(numeric)) {
|
||
return fallback;
|
||
}
|
||
|
||
return Math.min(
|
||
AUTO_STEP_DELAY_MAX_ALLOWED_SECONDS,
|
||
Math.max(AUTO_STEP_DELAY_MIN_ALLOWED_SECONDS, Math.floor(numeric))
|
||
);
|
||
}
|
||
|
||
function normalizeVerificationResendCount(value, fallback) {
|
||
const rawValue = String(value ?? '').trim();
|
||
if (!rawValue) {
|
||
return fallback;
|
||
}
|
||
|
||
const numeric = Number(rawValue);
|
||
if (!Number.isFinite(numeric)) {
|
||
return fallback;
|
||
}
|
||
|
||
return Math.min(
|
||
VERIFICATION_RESEND_COUNT_MAX,
|
||
Math.max(VERIFICATION_RESEND_COUNT_MIN, Math.floor(numeric))
|
||
);
|
||
}
|
||
|
||
function normalizePhoneVerificationReplacementLimit(value, fallback = DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT) {
|
||
const rawValue = String(value ?? '').trim();
|
||
const numeric = Number(rawValue);
|
||
if (!rawValue || !Number.isFinite(numeric)) {
|
||
return Math.min(
|
||
PHONE_REPLACEMENT_LIMIT_MAX,
|
||
Math.max(PHONE_REPLACEMENT_LIMIT_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT))
|
||
);
|
||
}
|
||
return Math.min(
|
||
PHONE_REPLACEMENT_LIMIT_MAX,
|
||
Math.max(PHONE_REPLACEMENT_LIMIT_MIN, Math.floor(numeric))
|
||
);
|
||
}
|
||
|
||
function normalizePhoneCodeWaitSeconds(value, fallback = DEFAULT_PHONE_CODE_WAIT_SECONDS) {
|
||
const rawValue = String(value ?? '').trim();
|
||
const numeric = Number(rawValue);
|
||
if (!rawValue || !Number.isFinite(numeric)) {
|
||
return Math.min(
|
||
PHONE_CODE_WAIT_SECONDS_MAX,
|
||
Math.max(PHONE_CODE_WAIT_SECONDS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_WAIT_SECONDS))
|
||
);
|
||
}
|
||
return Math.min(
|
||
PHONE_CODE_WAIT_SECONDS_MAX,
|
||
Math.max(PHONE_CODE_WAIT_SECONDS_MIN, Math.floor(numeric))
|
||
);
|
||
}
|
||
|
||
function normalizePhoneCodeTimeoutWindows(value, fallback = DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS) {
|
||
const rawValue = String(value ?? '').trim();
|
||
const numeric = Number(rawValue);
|
||
if (!rawValue || !Number.isFinite(numeric)) {
|
||
return Math.min(
|
||
PHONE_CODE_TIMEOUT_WINDOWS_MAX,
|
||
Math.max(PHONE_CODE_TIMEOUT_WINDOWS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS))
|
||
);
|
||
}
|
||
return Math.min(
|
||
PHONE_CODE_TIMEOUT_WINDOWS_MAX,
|
||
Math.max(PHONE_CODE_TIMEOUT_WINDOWS_MIN, Math.floor(numeric))
|
||
);
|
||
}
|
||
|
||
function normalizePhoneCodePollIntervalSeconds(value, fallback = DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS) {
|
||
const rawValue = String(value ?? '').trim();
|
||
const numeric = Number(rawValue);
|
||
if (!rawValue || !Number.isFinite(numeric)) {
|
||
return Math.min(
|
||
PHONE_CODE_POLL_INTERVAL_SECONDS_MAX,
|
||
Math.max(PHONE_CODE_POLL_INTERVAL_SECONDS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS))
|
||
);
|
||
}
|
||
return Math.min(
|
||
PHONE_CODE_POLL_INTERVAL_SECONDS_MAX,
|
||
Math.max(PHONE_CODE_POLL_INTERVAL_SECONDS_MIN, Math.floor(numeric))
|
||
);
|
||
}
|
||
|
||
function normalizePhoneCodePollMaxRounds(value, fallback = DEFAULT_PHONE_CODE_POLL_ROUNDS) {
|
||
const rawValue = String(value ?? '').trim();
|
||
const numeric = Number(rawValue);
|
||
if (!rawValue || !Number.isFinite(numeric)) {
|
||
return Math.min(
|
||
PHONE_CODE_POLL_ROUNDS_MAX,
|
||
Math.max(PHONE_CODE_POLL_ROUNDS_MIN, Math.floor(Number(fallback) || DEFAULT_PHONE_CODE_POLL_ROUNDS))
|
||
);
|
||
}
|
||
return Math.min(
|
||
PHONE_CODE_POLL_ROUNDS_MAX,
|
||
Math.max(PHONE_CODE_POLL_ROUNDS_MIN, Math.floor(numeric))
|
||
);
|
||
}
|
||
|
||
function normalizeHeroSmsMaxPrice(value = '') {
|
||
const rawValue = String(value ?? '').trim();
|
||
if (!rawValue) {
|
||
return '';
|
||
}
|
||
const numeric = Number(rawValue);
|
||
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||
return '';
|
||
}
|
||
return String(Math.round(numeric * 10000) / 10000);
|
||
}
|
||
|
||
function normalizeHeroSmsAcquirePriority(value = '') {
|
||
return String(value || '').trim().toLowerCase() === HERO_SMS_ACQUIRE_PRIORITY_PRICE
|
||
? HERO_SMS_ACQUIRE_PRIORITY_PRICE
|
||
: HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||
}
|
||
|
||
function normalizeHeroSmsCountryFallback(value = []) {
|
||
const source = Array.isArray(value)
|
||
? value
|
||
: String(value || '')
|
||
.split(/[\r\n,,;;]+/)
|
||
.map((entry) => String(entry || '').trim())
|
||
.filter(Boolean);
|
||
const seenIds = new Set();
|
||
const normalized = [];
|
||
|
||
for (const entry of source) {
|
||
let countryId = 0;
|
||
let countryLabel = '';
|
||
|
||
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
||
countryId = Math.floor(Number(entry.countryId ?? entry.id) || 0);
|
||
countryLabel = String((entry.countryLabel ?? entry.label) || '').trim();
|
||
} else {
|
||
const text = String(entry || '').trim();
|
||
const structuredMatch = text.match(/^(\d+)\s*(?:[:|/-]\s*(.+))?$/);
|
||
if (structuredMatch) {
|
||
countryId = Math.floor(Number(structuredMatch[1]) || 0);
|
||
countryLabel = String(structuredMatch[2] || '').trim();
|
||
} else {
|
||
countryId = Math.floor(Number(text) || 0);
|
||
}
|
||
}
|
||
|
||
if (!Number.isFinite(countryId) || countryId <= 0 || seenIds.has(countryId)) {
|
||
continue;
|
||
}
|
||
seenIds.add(countryId);
|
||
normalized.push({
|
||
id: countryId,
|
||
label: countryLabel || `Country #${countryId}`,
|
||
});
|
||
if (normalized.length >= 20) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
return normalized;
|
||
}
|
||
|
||
function resolveLegacyAutoStepDelaySeconds(input = {}) {
|
||
const hasLegacyMin = input.autoStepRandomDelayMinSeconds !== undefined;
|
||
const hasLegacyMax = input.autoStepRandomDelayMaxSeconds !== undefined;
|
||
if (!hasLegacyMin && !hasLegacyMax) {
|
||
return undefined;
|
||
}
|
||
|
||
const minSeconds = normalizeAutoStepDelaySeconds(input.autoStepRandomDelayMinSeconds, null);
|
||
const maxSeconds = normalizeAutoStepDelaySeconds(input.autoStepRandomDelayMaxSeconds, null);
|
||
if (minSeconds === null && maxSeconds === null) {
|
||
return null;
|
||
}
|
||
if (minSeconds === null) {
|
||
return maxSeconds;
|
||
}
|
||
if (maxSeconds === null) {
|
||
return minSeconds;
|
||
}
|
||
return Math.round((minSeconds + maxSeconds) / 2);
|
||
}
|
||
|
||
function normalizeRunCount(value) {
|
||
const numeric = Number(value);
|
||
if (!Number.isFinite(numeric)) {
|
||
return 1;
|
||
}
|
||
return Math.max(1, Math.floor(numeric));
|
||
}
|
||
|
||
function normalizeAutoRunTimerKind(value = '') {
|
||
const normalized = String(value || '').trim().toLowerCase();
|
||
if (normalized === AUTO_RUN_TIMER_KIND_SCHEDULED_START) {
|
||
return AUTO_RUN_TIMER_KIND_SCHEDULED_START;
|
||
}
|
||
if (normalized === AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS) {
|
||
return AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS;
|
||
}
|
||
if (normalized === AUTO_RUN_TIMER_KIND_BEFORE_RETRY) {
|
||
return AUTO_RUN_TIMER_KIND_BEFORE_RETRY;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function normalizeAutoRunSessionId(value) {
|
||
const numeric = Math.floor(Number(value) || 0);
|
||
return numeric > 0 ? numeric : 0;
|
||
}
|
||
|
||
function createAutoRunSessionId() {
|
||
autoRunSessionSeed = Math.max(autoRunSessionSeed + 1, Date.now());
|
||
autoRunSessionId = autoRunSessionSeed;
|
||
return autoRunSessionId;
|
||
}
|
||
|
||
function setCurrentAutoRunSessionId(value) {
|
||
autoRunSessionId = normalizeAutoRunSessionId(value);
|
||
return autoRunSessionId;
|
||
}
|
||
|
||
function clearCurrentAutoRunSessionId(expectedSessionId = null) {
|
||
if (expectedSessionId === null) {
|
||
autoRunSessionId = 0;
|
||
return autoRunSessionId;
|
||
}
|
||
|
||
const normalizedExpected = normalizeAutoRunSessionId(expectedSessionId);
|
||
if (!normalizedExpected || normalizedExpected === autoRunSessionId) {
|
||
autoRunSessionId = 0;
|
||
}
|
||
return autoRunSessionId;
|
||
}
|
||
|
||
function isCurrentAutoRunSessionId(value) {
|
||
const normalized = normalizeAutoRunSessionId(value);
|
||
return normalized > 0 && normalized === autoRunSessionId;
|
||
}
|
||
|
||
function throwIfAutoRunSessionStopped(sessionId) {
|
||
const normalizedSessionId = normalizeAutoRunSessionId(sessionId);
|
||
if (normalizedSessionId && !isCurrentAutoRunSessionId(normalizedSessionId)) {
|
||
throw new Error(STOP_ERROR_MESSAGE);
|
||
}
|
||
throwIfStopped();
|
||
}
|
||
|
||
function normalizeAutoRunTimerPlan(plan) {
|
||
if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
|
||
return null;
|
||
}
|
||
|
||
const kind = normalizeAutoRunTimerKind(plan.kind);
|
||
if (!kind) {
|
||
return null;
|
||
}
|
||
|
||
const fireAt = Number(plan.fireAt);
|
||
if (!Number.isFinite(fireAt)) {
|
||
return null;
|
||
}
|
||
|
||
const totalRuns = normalizeRunCount(plan.totalRuns);
|
||
const autoRunSkipFailures = Boolean(plan.autoRunSkipFailures);
|
||
const mode = plan.mode === 'continue' ? 'continue' : 'restart';
|
||
const currentRun = Math.max(0, Math.min(totalRuns, Math.floor(Number(plan.currentRun) || 0)));
|
||
const attemptRun = Math.max(
|
||
0,
|
||
Math.min(AUTO_RUN_MAX_RETRIES_PER_ROUND + 1, Math.floor(Number(plan.attemptRun) || 0))
|
||
);
|
||
const autoRunSessionId = normalizeAutoRunSessionId(plan.autoRunSessionId ?? plan.sessionId);
|
||
const roundSummaries = serializeAutoRunRoundSummaries(totalRuns, plan.roundSummaries);
|
||
const countdownTitle = String(plan.countdownTitle || '').trim();
|
||
const countdownNote = String(plan.countdownNote || '').trim();
|
||
|
||
if (kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START) {
|
||
return {
|
||
kind,
|
||
fireAt,
|
||
totalRuns,
|
||
autoRunSkipFailures,
|
||
mode,
|
||
currentRun: 0,
|
||
attemptRun: 0,
|
||
autoRunSessionId,
|
||
roundSummaries: [],
|
||
countdownTitle: countdownTitle || '已计划自动运行',
|
||
countdownNote: countdownNote || `计划于 ${formatAutoRunScheduleTime(fireAt)} 开始`,
|
||
};
|
||
}
|
||
|
||
if (kind === AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS) {
|
||
const normalizedCurrentRun = Math.max(1, Math.min(totalRuns, currentRun));
|
||
const normalizedAttemptRun = Math.max(1, attemptRun);
|
||
return {
|
||
kind,
|
||
fireAt,
|
||
totalRuns,
|
||
autoRunSkipFailures,
|
||
mode: 'restart',
|
||
currentRun: normalizedCurrentRun,
|
||
attemptRun: normalizedAttemptRun,
|
||
autoRunSessionId,
|
||
roundSummaries,
|
||
countdownTitle: countdownTitle || '线程间隔中',
|
||
countdownNote: countdownNote || `第 ${Math.min(normalizedCurrentRun + 1, totalRuns)}/${totalRuns} 轮即将开始`,
|
||
};
|
||
}
|
||
|
||
const normalizedCurrentRun = Math.max(1, Math.min(totalRuns, currentRun));
|
||
const normalizedAttemptRun = Math.max(1, attemptRun);
|
||
return {
|
||
kind,
|
||
fireAt,
|
||
totalRuns,
|
||
autoRunSkipFailures,
|
||
mode: 'restart',
|
||
currentRun: normalizedCurrentRun,
|
||
attemptRun: normalizedAttemptRun,
|
||
autoRunSessionId,
|
||
roundSummaries,
|
||
countdownTitle: countdownTitle || '线程间隔中',
|
||
countdownNote: countdownNote || `第 ${normalizedCurrentRun}/${totalRuns} 轮第 ${normalizedAttemptRun} 次尝试即将开始`,
|
||
};
|
||
}
|
||
|
||
function normalizeAutoRunTimerPlanFromState(state = {}) {
|
||
const directPlan = normalizeAutoRunTimerPlan(state.autoRunTimerPlan);
|
||
if (directPlan) {
|
||
return directPlan;
|
||
}
|
||
|
||
if (state.autoRunPhase !== 'scheduled') {
|
||
return null;
|
||
}
|
||
|
||
const legacyScheduledAt = Number(state.scheduledAutoRunAt);
|
||
if (!Number.isFinite(legacyScheduledAt)) {
|
||
return null;
|
||
}
|
||
|
||
return normalizeAutoRunTimerPlan({
|
||
kind: AUTO_RUN_TIMER_KIND_SCHEDULED_START,
|
||
fireAt: legacyScheduledAt,
|
||
totalRuns: state.scheduledAutoRunPlan?.totalRuns ?? state.autoRunTotalRuns,
|
||
autoRunSkipFailures: state.scheduledAutoRunPlan?.autoRunSkipFailures ?? state.autoRunSkipFailures,
|
||
autoRunSessionId: state.autoRunSessionId,
|
||
mode: state.scheduledAutoRunPlan?.mode,
|
||
});
|
||
}
|
||
|
||
function getAutoRunTimerPlanPhase(kind = '') {
|
||
return kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START ? 'scheduled' : 'waiting_interval';
|
||
}
|
||
|
||
function getAutoRunTimerStatusPayload(plan) {
|
||
const normalizedPlan = normalizeAutoRunTimerPlan(plan);
|
||
if (!normalizedPlan) {
|
||
return null;
|
||
}
|
||
|
||
const phase = getAutoRunTimerPlanPhase(normalizedPlan.kind);
|
||
return {
|
||
phase,
|
||
currentRun: normalizedPlan.currentRun,
|
||
totalRuns: normalizedPlan.totalRuns,
|
||
attemptRun: normalizedPlan.attemptRun,
|
||
sessionId: normalizedPlan.autoRunSessionId,
|
||
scheduledAt: phase === 'scheduled' ? normalizedPlan.fireAt : null,
|
||
countdownAt: normalizedPlan.fireAt,
|
||
countdownTitle: normalizedPlan.countdownTitle,
|
||
countdownNote: normalizedPlan.countdownNote,
|
||
};
|
||
}
|
||
|
||
function normalizeEmailGenerator(value = '') {
|
||
const normalized = String(value || '').trim().toLowerCase();
|
||
const customEmailPoolGenerator = typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
|
||
? CUSTOM_EMAIL_POOL_GENERATOR
|
||
: 'custom-pool';
|
||
const gmailAliasGenerator = typeof GMAIL_ALIAS_GENERATOR === 'string'
|
||
? GMAIL_ALIAS_GENERATOR
|
||
: 'gmail-alias';
|
||
if (normalized === 'custom' || normalized === 'manual') {
|
||
return 'custom';
|
||
}
|
||
if (normalized === gmailAliasGenerator) {
|
||
return gmailAliasGenerator;
|
||
}
|
||
if (normalized === customEmailPoolGenerator) {
|
||
return customEmailPoolGenerator;
|
||
}
|
||
if (normalized === 'icloud') {
|
||
return 'icloud';
|
||
}
|
||
if (normalized === 'cloudflare') return 'cloudflare';
|
||
if (normalized === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return CLOUDFLARE_TEMP_EMAIL_GENERATOR;
|
||
return 'duck';
|
||
}
|
||
|
||
function normalizeIcloudFetchMode(value = '') {
|
||
const normalized = String(value || '').trim().toLowerCase();
|
||
return normalized === 'always_new' ? 'always_new' : 'reuse_existing';
|
||
}
|
||
|
||
function normalizeCustomEmailPool(value = []) {
|
||
const source = Array.isArray(value)
|
||
? value
|
||
: String(value || '').split(/[\r\n,,;;]+/);
|
||
|
||
return source
|
||
.map((item) => String(item || '').trim().toLowerCase())
|
||
.filter((item) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(item));
|
||
}
|
||
|
||
function isCustomEmailPoolGenerator(stateOrValue = {}) {
|
||
const generator = typeof stateOrValue === 'string'
|
||
? stateOrValue
|
||
: stateOrValue?.emailGenerator;
|
||
const customEmailPoolGenerator = typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
|
||
? CUSTOM_EMAIL_POOL_GENERATOR
|
||
: 'custom-pool';
|
||
return normalizeEmailGenerator(generator) === customEmailPoolGenerator;
|
||
}
|
||
|
||
function getCustomEmailPool(state = {}) {
|
||
return normalizeCustomEmailPool(state?.customEmailPool);
|
||
}
|
||
|
||
function getCustomEmailPoolEmailForRun(state = {}, targetRun = 1) {
|
||
const entries = getCustomEmailPool(state);
|
||
const numericRun = Math.max(1, Math.floor(Number(targetRun) || 1));
|
||
return entries[numericRun - 1] || '';
|
||
}
|
||
|
||
function getCustomMailProviderPool(state = {}) {
|
||
return normalizeCustomEmailPool(state?.customMailProviderPool);
|
||
}
|
||
|
||
function getCustomMailProviderPoolEmailForRun(state = {}, targetRun = 1) {
|
||
const entries = getCustomMailProviderPool(state);
|
||
const numericRun = Math.max(1, Math.floor(Number(targetRun) || 1));
|
||
return entries[numericRun - 1] || '';
|
||
}
|
||
|
||
function normalizePanelMode(value = '') {
|
||
const normalized = String(value || '').trim().toLowerCase();
|
||
if (normalized === 'sub2api') {
|
||
return 'sub2api';
|
||
}
|
||
if (normalized === 'codex2api') {
|
||
return 'codex2api';
|
||
}
|
||
return 'cpa';
|
||
}
|
||
|
||
function normalizeMailProvider(value = '') {
|
||
const normalized = String(value || '').trim().toLowerCase();
|
||
switch (normalized) {
|
||
case 'custom':
|
||
case ICLOUD_PROVIDER:
|
||
case GMAIL_PROVIDER:
|
||
case HOTMAIL_PROVIDER:
|
||
case LUCKMAIL_PROVIDER:
|
||
case CLOUDFLARE_TEMP_EMAIL_PROVIDER:
|
||
case '163':
|
||
case '163-vip':
|
||
case '126':
|
||
case 'qq':
|
||
case 'inbucket':
|
||
case '2925':
|
||
return normalized;
|
||
default:
|
||
return PERSISTED_SETTING_DEFAULTS.mailProvider;
|
||
}
|
||
}
|
||
|
||
function buildLuckmailSessionSettingsPayload(input = {}) {
|
||
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
||
return {};
|
||
}
|
||
|
||
const payload = {};
|
||
|
||
if (input.luckmailApiKey !== undefined) {
|
||
payload.luckmailApiKey = String(input.luckmailApiKey || '');
|
||
}
|
||
if (input.luckmailBaseUrl !== undefined) {
|
||
payload.luckmailBaseUrl = normalizeLuckmailBaseUrl(input.luckmailBaseUrl);
|
||
}
|
||
if (input.luckmailEmailType !== undefined) {
|
||
payload.luckmailEmailType = normalizeLuckmailEmailType(input.luckmailEmailType);
|
||
}
|
||
if (input.luckmailDomain !== undefined) {
|
||
payload.luckmailDomain = String(input.luckmailDomain || '').trim();
|
||
}
|
||
if (input.luckmailUsedPurchases !== undefined) {
|
||
payload.luckmailUsedPurchases = normalizeLuckmailUsedPurchases(input.luckmailUsedPurchases);
|
||
}
|
||
if (input.luckmailPreserveTagId !== undefined) {
|
||
payload.luckmailPreserveTagId = Number(input.luckmailPreserveTagId) || 0;
|
||
}
|
||
if (input.luckmailPreserveTagName !== undefined) {
|
||
payload.luckmailPreserveTagName = String(input.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME;
|
||
}
|
||
if (input.currentLuckmailPurchase !== undefined) {
|
||
payload.currentLuckmailPurchase = input.currentLuckmailPurchase
|
||
? normalizeLuckmailPurchase(input.currentLuckmailPurchase)
|
||
: null;
|
||
}
|
||
if (input.currentLuckmailMailCursor !== undefined) {
|
||
payload.currentLuckmailMailCursor = input.currentLuckmailMailCursor
|
||
? normalizeLuckmailMailCursor(input.currentLuckmailMailCursor)
|
||
: null;
|
||
}
|
||
|
||
return payload;
|
||
}
|
||
|
||
function normalizeMail2925Mode(value = '') {
|
||
return String(value || '').trim().toLowerCase() === MAIL_2925_MODE_RECEIVE
|
||
? MAIL_2925_MODE_RECEIVE
|
||
: DEFAULT_MAIL_2925_MODE;
|
||
}
|
||
|
||
function normalizeLocalCpaStep9Mode(value = '') {
|
||
return String(value || '').trim().toLowerCase() === 'bypass'
|
||
? 'bypass'
|
||
: DEFAULT_LOCAL_CPA_STEP9_MODE;
|
||
}
|
||
|
||
function normalizeCloudflareDomain(rawValue = '') {
|
||
let value = String(rawValue || '').trim().toLowerCase();
|
||
if (!value) return '';
|
||
value = value.replace(/^@+/, '');
|
||
value = value.replace(/^https?:\/\//, '');
|
||
value = value.replace(/\/.*$/, '');
|
||
if (!/^[a-z0-9.-]+\.[a-z]{2,}$/.test(value)) return '';
|
||
return value;
|
||
}
|
||
|
||
function normalizeCloudflareDomains(values) {
|
||
const normalizedDomains = [];
|
||
const seen = new Set();
|
||
|
||
for (const value of Array.isArray(values) ? values : []) {
|
||
const normalized = normalizeCloudflareDomain(value);
|
||
if (!normalized || seen.has(normalized)) continue;
|
||
seen.add(normalized);
|
||
normalizedDomains.push(normalized);
|
||
}
|
||
|
||
return normalizedDomains;
|
||
}
|
||
|
||
function normalizeHotmailRemoteBaseUrl(rawValue = '') {
|
||
const value = String(rawValue || '').trim();
|
||
if (!value) return DEFAULT_HOTMAIL_REMOTE_BASE_URL;
|
||
|
||
try {
|
||
const parsed = new URL(value);
|
||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||
return DEFAULT_HOTMAIL_REMOTE_BASE_URL;
|
||
}
|
||
|
||
if (parsed.pathname.endsWith('/api/mail-new') || parsed.pathname.endsWith('/api/mail-all') || parsed.pathname === '/api.html') {
|
||
parsed.pathname = '';
|
||
parsed.search = '';
|
||
parsed.hash = '';
|
||
}
|
||
|
||
return parsed.toString().replace(/\/$/, '');
|
||
} catch {
|
||
return DEFAULT_HOTMAIL_REMOTE_BASE_URL;
|
||
}
|
||
}
|
||
|
||
function normalizeHotmailLocalBaseUrl(rawValue = '') {
|
||
const value = String(rawValue || '').trim();
|
||
if (!value) return DEFAULT_HOTMAIL_LOCAL_BASE_URL;
|
||
|
||
try {
|
||
const parsed = new URL(value);
|
||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||
return DEFAULT_HOTMAIL_LOCAL_BASE_URL;
|
||
}
|
||
|
||
if (['/messages', '/code', '/clear', '/token'].includes(parsed.pathname)) {
|
||
parsed.pathname = '';
|
||
parsed.search = '';
|
||
parsed.hash = '';
|
||
}
|
||
|
||
return parsed.toString().replace(/\/$/, '');
|
||
} catch {
|
||
return DEFAULT_HOTMAIL_LOCAL_BASE_URL;
|
||
}
|
||
}
|
||
|
||
function normalizeAccountRunHistoryHelperBaseUrl(rawValue = '') {
|
||
const value = String(rawValue || '').trim();
|
||
if (!value) return DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL;
|
||
|
||
try {
|
||
const parsed = new URL(value);
|
||
if (parsed.pathname === '/append-account-log' || parsed.pathname === '/sync-account-run-records') {
|
||
parsed.pathname = '';
|
||
parsed.search = '';
|
||
parsed.hash = '';
|
||
}
|
||
return normalizeHotmailLocalBaseUrl(parsed.toString());
|
||
} catch {
|
||
return normalizeHotmailLocalBaseUrl(value);
|
||
}
|
||
}
|
||
|
||
function getHotmailServiceSettings(state = {}) {
|
||
return {
|
||
mode: normalizeHotmailServiceMode(state.hotmailServiceMode),
|
||
remoteBaseUrl: normalizeHotmailRemoteBaseUrl(state.hotmailRemoteBaseUrl),
|
||
localBaseUrl: normalizeHotmailLocalBaseUrl(state.hotmailLocalBaseUrl),
|
||
};
|
||
}
|
||
|
||
function getCloudflareTempEmailConfig(state = {}) {
|
||
return {
|
||
baseUrl: normalizeCloudflareTempEmailBaseUrl(state.cloudflareTempEmailBaseUrl),
|
||
adminAuth: String(state.cloudflareTempEmailAdminAuth || ''),
|
||
customAuth: String(state.cloudflareTempEmailCustomAuth || ''),
|
||
receiveMailbox: normalizeCloudflareTempEmailReceiveMailbox(state.cloudflareTempEmailReceiveMailbox),
|
||
useRandomSubdomain: Boolean(state.cloudflareTempEmailUseRandomSubdomain),
|
||
domain: normalizeCloudflareTempEmailDomain(state.cloudflareTempEmailDomain),
|
||
domains: normalizeCloudflareTempEmailDomains(state.cloudflareTempEmailDomains),
|
||
};
|
||
}
|
||
|
||
function normalizeCloudflareTempEmailReceiveMailbox(value = '') {
|
||
const normalized = normalizeCloudflareTempEmailAddress(value);
|
||
if (!normalized) return '';
|
||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized) ? normalized : '';
|
||
}
|
||
|
||
function resolveCloudflareTempEmailPollTargetEmail(state = {}, pollPayload = {}, config = getCloudflareTempEmailConfig(state)) {
|
||
const configuredReceiveMailbox = normalizeCloudflareTempEmailReceiveMailbox(config.receiveMailbox);
|
||
const mailProvider = String(state?.mailProvider || '').trim().toLowerCase();
|
||
const emailGenerator = String(state?.emailGenerator || '').trim().toLowerCase();
|
||
const shouldPreferConfiguredReceiveMailbox = mailProvider === 'cloudflare-temp-email'
|
||
&& emailGenerator !== 'cloudflare-temp-email';
|
||
if (shouldPreferConfiguredReceiveMailbox && configuredReceiveMailbox) {
|
||
return configuredReceiveMailbox;
|
||
}
|
||
|
||
const requestedTarget = normalizeCloudflareTempEmailReceiveMailbox(pollPayload.targetEmail);
|
||
if (requestedTarget) {
|
||
return requestedTarget;
|
||
}
|
||
|
||
return normalizeCloudflareTempEmailReceiveMailbox(state.email);
|
||
}
|
||
|
||
function normalizePersistentSettingValue(key, value) {
|
||
switch (key) {
|
||
case 'panelMode':
|
||
return normalizePanelMode(value);
|
||
case 'vpsUrl':
|
||
return String(value || '').trim();
|
||
case 'vpsPassword':
|
||
return String(value || '');
|
||
case 'localCpaStep9Mode':
|
||
return normalizeLocalCpaStep9Mode(value);
|
||
case 'sub2apiUrl':
|
||
return String(value || '').trim();
|
||
case 'sub2apiEmail':
|
||
return String(value || '').trim();
|
||
case 'sub2apiPassword':
|
||
return String(value || '');
|
||
case 'sub2apiGroupName':
|
||
return String(value || '').trim();
|
||
case 'sub2apiDefaultProxyName':
|
||
return String(value || '').trim();
|
||
case 'ipProxyEnabled':
|
||
return Boolean(value);
|
||
case 'ipProxyService':
|
||
return normalizeIpProxyProviderValue(value);
|
||
case 'ipProxyMode':
|
||
return normalizeIpProxyMode(value);
|
||
case 'ipProxyApiUrl':
|
||
return String(value || '').trim();
|
||
case 'ipProxyServiceProfiles':
|
||
return normalizeIpProxyServiceProfiles(value || {}, PERSISTED_SETTING_DEFAULTS);
|
||
case 'ipProxyAccountList':
|
||
return normalizeIpProxyAccountList(value || '');
|
||
case 'ipProxyAccountSessionPrefix':
|
||
return normalizeIpProxyAccountSessionPrefix(value || '');
|
||
case 'ipProxyAccountLifeMinutes':
|
||
return normalizeIpProxyAccountLifeMinutes(value || '');
|
||
case 'ipProxyPoolTargetCount':
|
||
return normalizeIpProxyPoolTargetCount(value || '', 20);
|
||
case 'ipProxyHost':
|
||
return String(value || '').trim();
|
||
case 'ipProxyPort':
|
||
return String(normalizeIpProxyPort(value || '') || '');
|
||
case 'ipProxyProtocol':
|
||
return normalizeIpProxyProtocol(value);
|
||
case 'ipProxyUsername':
|
||
return String(value || '').trim();
|
||
case 'ipProxyPassword':
|
||
return String(value || '');
|
||
case 'ipProxyRegion':
|
||
return String(value || '').trim();
|
||
case 'ipProxyApiPool':
|
||
return normalizeProxyPoolEntries(
|
||
value,
|
||
normalizeIpProxyProviderValue(value?.provider || DEFAULT_IP_PROXY_SERVICE)
|
||
);
|
||
case 'ipProxyApiCurrentIndex':
|
||
return normalizeIpProxyCurrentIndex(value, 0);
|
||
case 'ipProxyApiCurrent':
|
||
return normalizeProxyPoolEntries(value ? [value] : [], DEFAULT_IP_PROXY_SERVICE)[0] || null;
|
||
case 'ipProxyAccountPool':
|
||
return normalizeProxyPoolEntries(
|
||
value,
|
||
normalizeIpProxyProviderValue(value?.provider || DEFAULT_IP_PROXY_SERVICE)
|
||
);
|
||
case 'ipProxyAccountCurrentIndex':
|
||
return normalizeIpProxyCurrentIndex(value, 0);
|
||
case 'ipProxyAccountCurrent':
|
||
return normalizeProxyPoolEntries(value ? [value] : [], DEFAULT_IP_PROXY_SERVICE)[0] || null;
|
||
case 'ipProxyPool':
|
||
return normalizeProxyPoolEntries(
|
||
value,
|
||
normalizeIpProxyProviderValue(value?.provider || DEFAULT_IP_PROXY_SERVICE)
|
||
);
|
||
case 'ipProxyCurrentIndex':
|
||
return normalizeIpProxyCurrentIndex(value, 0);
|
||
case 'ipProxyCurrent':
|
||
return normalizeProxyPoolEntries(value ? [value] : [], DEFAULT_IP_PROXY_SERVICE)[0] || null;
|
||
case 'codex2apiUrl':
|
||
return normalizeCodex2ApiUrl(value);
|
||
case 'codex2apiAdminKey':
|
||
return String(value || '').trim();
|
||
case 'customPassword':
|
||
return String(value || '');
|
||
case 'paypalEmail':
|
||
return String(value || '').trim();
|
||
case 'paypalPassword':
|
||
return String(value || '');
|
||
case 'currentPayPalAccountId':
|
||
return String(value || '').trim();
|
||
case 'plusPaymentMethod':
|
||
return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal';
|
||
case 'autoRunSkipFailures':
|
||
case 'autoRunDelayEnabled':
|
||
case 'phoneVerificationEnabled':
|
||
case 'plusModeEnabled':
|
||
return Boolean(value);
|
||
case 'autoRunFallbackThreadIntervalMinutes':
|
||
return normalizeAutoRunFallbackThreadIntervalMinutes(value);
|
||
case 'autoRunDelayMinutes':
|
||
return normalizeAutoRunDelayMinutes(value);
|
||
case 'autoStepDelaySeconds':
|
||
return normalizeAutoStepDelaySeconds(value, PERSISTED_SETTING_DEFAULTS.autoStepDelaySeconds);
|
||
case 'verificationResendCount':
|
||
return normalizeVerificationResendCount(value, DEFAULT_VERIFICATION_RESEND_COUNT);
|
||
case 'phoneVerificationReplacementLimit':
|
||
return normalizePhoneVerificationReplacementLimit(value, DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT);
|
||
case 'phoneCodeWaitSeconds':
|
||
return normalizePhoneCodeWaitSeconds(value, DEFAULT_PHONE_CODE_WAIT_SECONDS);
|
||
case 'phoneCodeTimeoutWindows':
|
||
return normalizePhoneCodeTimeoutWindows(value, DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS);
|
||
case 'phoneCodePollIntervalSeconds':
|
||
return normalizePhoneCodePollIntervalSeconds(value, DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS);
|
||
case 'phoneCodePollMaxRounds':
|
||
return normalizePhoneCodePollMaxRounds(value, DEFAULT_PHONE_CODE_POLL_ROUNDS);
|
||
case 'mailProvider':
|
||
return normalizeMailProvider(value);
|
||
case 'mail2925Mode':
|
||
return normalizeMail2925Mode(value);
|
||
case 'mail2925UseAccountPool':
|
||
return Boolean(value);
|
||
case 'emailGenerator':
|
||
return normalizeEmailGenerator(value);
|
||
case 'customMailProviderPool':
|
||
case 'customEmailPool':
|
||
return normalizeCustomEmailPool(value);
|
||
case 'autoDeleteUsedIcloudAlias':
|
||
case 'accountRunHistoryTextEnabled':
|
||
case 'cloudflareTempEmailUseRandomSubdomain':
|
||
return Boolean(value);
|
||
case 'icloudHostPreference':
|
||
return normalizeIcloudHost(value) || 'auto';
|
||
case 'icloudTargetMailboxType':
|
||
return normalizeIcloudTargetMailboxType(value);
|
||
case 'icloudForwardMailProvider':
|
||
return normalizeIcloudForwardMailProvider(value);
|
||
case 'icloudFetchMode':
|
||
return normalizeIcloudFetchMode(value);
|
||
case 'accountRunHistoryHelperBaseUrl':
|
||
return normalizeAccountRunHistoryHelperBaseUrl(value);
|
||
case 'gmailBaseEmail':
|
||
case 'mail2925BaseEmail':
|
||
case 'currentMail2925AccountId':
|
||
case 'emailPrefix':
|
||
return String(value || '').trim();
|
||
case 'inbucketHost':
|
||
return String(value || '').trim();
|
||
case 'inbucketMailbox':
|
||
return String(value || '').trim();
|
||
case 'hotmailServiceMode':
|
||
return normalizeHotmailServiceMode(value);
|
||
case 'hotmailRemoteBaseUrl':
|
||
return normalizeHotmailRemoteBaseUrl(value);
|
||
case 'hotmailLocalBaseUrl':
|
||
return normalizeHotmailLocalBaseUrl(value);
|
||
case 'luckmailApiKey':
|
||
return String(value || '');
|
||
case 'luckmailBaseUrl':
|
||
return normalizeLuckmailBaseUrl(value);
|
||
case 'luckmailEmailType':
|
||
return normalizeLuckmailEmailType(value);
|
||
case 'luckmailDomain':
|
||
return String(value || '').trim();
|
||
case 'cloudflareDomain':
|
||
return normalizeCloudflareDomain(value);
|
||
case 'cloudflareDomains':
|
||
return normalizeCloudflareDomains(value);
|
||
case 'cloudflareTempEmailBaseUrl':
|
||
return normalizeCloudflareTempEmailBaseUrl(value);
|
||
case 'cloudflareTempEmailAdminAuth':
|
||
case 'cloudflareTempEmailCustomAuth':
|
||
return String(value || '');
|
||
case 'cloudflareTempEmailReceiveMailbox':
|
||
return normalizeCloudflareTempEmailReceiveMailbox(value);
|
||
case 'cloudflareTempEmailDomain':
|
||
return normalizeCloudflareTempEmailDomain(value);
|
||
case 'cloudflareTempEmailDomains':
|
||
return normalizeCloudflareTempEmailDomains(value);
|
||
case 'hotmailAccounts':
|
||
return normalizeHotmailAccounts(value);
|
||
case 'mail2925Accounts':
|
||
return normalizeMail2925Accounts(value);
|
||
case 'paypalAccounts':
|
||
return normalizePayPalAccounts(value);
|
||
case 'heroSmsApiKey':
|
||
return String(value || '');
|
||
case 'heroSmsReuseEnabled':
|
||
return Boolean(value);
|
||
case 'heroSmsAcquirePriority':
|
||
return normalizeHeroSmsAcquirePriority(value);
|
||
case 'heroSmsMaxPrice':
|
||
return normalizeHeroSmsMaxPrice(value);
|
||
case 'heroSmsCountryId':
|
||
return Math.max(1, Math.floor(Number(value) || HERO_SMS_COUNTRY_ID));
|
||
case 'heroSmsCountryLabel':
|
||
return String(value || HERO_SMS_COUNTRY_LABEL).trim() || HERO_SMS_COUNTRY_LABEL;
|
||
case 'heroSmsCountryFallback':
|
||
return normalizeHeroSmsCountryFallback(value);
|
||
default:
|
||
return value;
|
||
}
|
||
}
|
||
|
||
function buildPersistentSettingsPayload(input = {}, options = {}) {
|
||
const { fillDefaults = false, requireKnownKeys = false } = options;
|
||
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
||
throw new Error('\u914d\u7f6e\u5185\u5bb9\u683c\u5f0f\u65e0\u6548\u3002');
|
||
}
|
||
|
||
const normalizedInput = { ...input };
|
||
if (normalizedInput.autoStepDelaySeconds === undefined) {
|
||
const legacyAutoStepDelaySeconds = resolveLegacyAutoStepDelaySeconds(normalizedInput);
|
||
if (legacyAutoStepDelaySeconds !== undefined) {
|
||
normalizedInput.autoStepDelaySeconds = legacyAutoStepDelaySeconds;
|
||
}
|
||
}
|
||
if (normalizedInput.verificationResendCount === undefined) {
|
||
const legacyVerificationResendCount = normalizedInput.signupVerificationResendCount !== undefined
|
||
? normalizedInput.signupVerificationResendCount
|
||
: normalizedInput.loginVerificationResendCount;
|
||
if (legacyVerificationResendCount !== undefined) {
|
||
normalizedInput.verificationResendCount = legacyVerificationResendCount;
|
||
}
|
||
}
|
||
|
||
const payload = {};
|
||
let matchedKeyCount = 0;
|
||
for (const key of PERSISTED_SETTING_KEYS) {
|
||
if (normalizedInput[key] !== undefined) {
|
||
payload[key] = normalizePersistentSettingValue(key, normalizedInput[key]);
|
||
matchedKeyCount += 1;
|
||
} else if (fillDefaults) {
|
||
payload[key] = normalizePersistentSettingValue(key, PERSISTED_SETTING_DEFAULTS[key]);
|
||
}
|
||
}
|
||
|
||
if (requireKnownKeys && matchedKeyCount === 0) {
|
||
throw new Error('\u914d\u7f6e\u6587\u4ef6\u4e2d\u6ca1\u6709\u53ef\u8bc6\u522b\u7684\u914d\u7f6e\u5185\u5bb9\u3002');
|
||
}
|
||
|
||
if (payload.cloudflareDomains) {
|
||
const domains = normalizeCloudflareDomains(payload.cloudflareDomains);
|
||
if (payload.cloudflareDomain && !domains.includes(payload.cloudflareDomain)) {
|
||
domains.unshift(payload.cloudflareDomain);
|
||
}
|
||
payload.cloudflareDomains = domains;
|
||
}
|
||
if (payload.cloudflareTempEmailDomains) {
|
||
const domains = normalizeCloudflareTempEmailDomains(payload.cloudflareTempEmailDomains);
|
||
if (payload.cloudflareTempEmailDomain && !domains.includes(payload.cloudflareTempEmailDomain)) {
|
||
domains.unshift(payload.cloudflareTempEmailDomain);
|
||
}
|
||
payload.cloudflareTempEmailDomains = domains;
|
||
}
|
||
if (payload.ipProxyServiceProfiles) {
|
||
const selectedService = normalizeIpProxyProviderValue(
|
||
payload.ipProxyService || PERSISTED_SETTING_DEFAULTS.ipProxyService
|
||
);
|
||
const normalizedProfiles = normalizeIpProxyServiceProfiles(payload.ipProxyServiceProfiles, {
|
||
...PERSISTED_SETTING_DEFAULTS,
|
||
...payload,
|
||
});
|
||
payload.ipProxyServiceProfiles = normalizedProfiles;
|
||
const activeProfile = normalizedProfiles[selectedService]
|
||
|| buildIpProxyServiceProfileFromState({
|
||
...PERSISTED_SETTING_DEFAULTS,
|
||
...payload,
|
||
});
|
||
payload.ipProxyService = selectedService;
|
||
payload.ipProxyMode = normalizeIpProxyMode(activeProfile?.mode || payload.ipProxyMode);
|
||
payload.ipProxyApiUrl = String(activeProfile?.apiUrl || payload.ipProxyApiUrl || '').trim();
|
||
payload.ipProxyAccountList = normalizeIpProxyAccountList(activeProfile?.accountList || payload.ipProxyAccountList || '');
|
||
payload.ipProxyAccountSessionPrefix = normalizeIpProxyAccountSessionPrefix(activeProfile?.accountSessionPrefix || payload.ipProxyAccountSessionPrefix || '');
|
||
payload.ipProxyAccountLifeMinutes = normalizeIpProxyAccountLifeMinutes(activeProfile?.accountLifeMinutes || payload.ipProxyAccountLifeMinutes || '');
|
||
payload.ipProxyPoolTargetCount = normalizeIpProxyPoolTargetCount(activeProfile?.poolTargetCount || payload.ipProxyPoolTargetCount || '', 20);
|
||
payload.ipProxyHost = String(activeProfile?.host || payload.ipProxyHost || '').trim();
|
||
payload.ipProxyPort = String(normalizeIpProxyPort(activeProfile?.port || payload.ipProxyPort || '') || '');
|
||
payload.ipProxyProtocol = normalizeIpProxyProtocol(activeProfile?.protocol || payload.ipProxyProtocol);
|
||
payload.ipProxyUsername = String(activeProfile?.username || payload.ipProxyUsername || '').trim();
|
||
payload.ipProxyPassword = String(activeProfile?.password || payload.ipProxyPassword || '');
|
||
payload.ipProxyRegion = String(activeProfile?.region || payload.ipProxyRegion || '').trim();
|
||
}
|
||
|
||
return payload;
|
||
}
|
||
|
||
async function getPersistedSettings() {
|
||
const stored = await chrome.storage.local.get([
|
||
...PERSISTED_SETTING_KEYS,
|
||
...LEGACY_AUTO_STEP_DELAY_KEYS,
|
||
...LEGACY_VERIFICATION_RESEND_COUNT_KEYS,
|
||
]);
|
||
return buildPersistentSettingsPayload(stored, { fillDefaults: true });
|
||
}
|
||
|
||
async function getPersistedAliasState() {
|
||
try {
|
||
const stored = await chrome.storage.local.get(PERSISTENT_ALIAS_STATE_KEYS);
|
||
const manualAliasUsage = normalizeBooleanMap(stored.manualAliasUsage);
|
||
const preservedAliases = normalizeBooleanMap(stored.preservedAliases);
|
||
return {
|
||
manualAliasUsage,
|
||
preservedAliases,
|
||
icloudAliasCache: normalizeIcloudAliasCacheList(stored.icloudAliasCache, {
|
||
usedEmails: toNormalizedEmailSet(manualAliasUsage),
|
||
preservedEmails: toNormalizedEmailSet(preservedAliases),
|
||
}),
|
||
icloudAliasCacheAt: Math.max(0, Number(stored.icloudAliasCacheAt) || 0),
|
||
};
|
||
} catch (err) {
|
||
console.warn(LOG_PREFIX, 'Failed to read persisted iCloud alias state:', err?.message || err);
|
||
return {
|
||
manualAliasUsage: {},
|
||
preservedAliases: {},
|
||
icloudAliasCache: [],
|
||
icloudAliasCacheAt: 0,
|
||
};
|
||
}
|
||
}
|
||
|
||
async function getState() {
|
||
const [state, persistedSettings, persistedAliasState, accountRunHistory] = await Promise.all([
|
||
chrome.storage.session.get(null),
|
||
getPersistedSettings(),
|
||
getPersistedAliasState(),
|
||
accountRunHistoryHelpers?.getPersistedAccountRunHistory?.() || [],
|
||
]);
|
||
return { ...DEFAULT_STATE, ...persistedSettings, ...persistedAliasState, ...state, accountRunHistory };
|
||
}
|
||
|
||
async function initializeSessionStorageAccess() {
|
||
try {
|
||
if (chrome.storage?.session?.setAccessLevel) {
|
||
await chrome.storage.session.setAccessLevel({
|
||
accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS',
|
||
});
|
||
console.log(LOG_PREFIX, 'Enabled storage.session for content scripts');
|
||
}
|
||
} catch (err) {
|
||
console.warn(LOG_PREFIX, 'Failed to enable storage.session for content scripts:', err?.message || err);
|
||
}
|
||
}
|
||
|
||
async function setState(updates) {
|
||
console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
|
||
if (Object.keys(updates || {}).length > 0) {
|
||
await chrome.storage.session.set(updates);
|
||
const persistentAliasUpdates = {};
|
||
if (Object.prototype.hasOwnProperty.call(updates, 'manualAliasUsage')) {
|
||
persistentAliasUpdates.manualAliasUsage = normalizeBooleanMap(updates.manualAliasUsage);
|
||
}
|
||
if (Object.prototype.hasOwnProperty.call(updates, 'preservedAliases')) {
|
||
persistentAliasUpdates.preservedAliases = normalizeBooleanMap(updates.preservedAliases);
|
||
}
|
||
if (Object.prototype.hasOwnProperty.call(updates, 'icloudAliasCache')) {
|
||
persistentAliasUpdates.icloudAliasCache = normalizeIcloudAliasCacheList(updates.icloudAliasCache);
|
||
}
|
||
if (Object.prototype.hasOwnProperty.call(updates, 'icloudAliasCacheAt')) {
|
||
persistentAliasUpdates.icloudAliasCacheAt = Math.max(0, Number(updates.icloudAliasCacheAt) || 0);
|
||
}
|
||
if (Object.keys(persistentAliasUpdates).length > 0) {
|
||
await chrome.storage.local.set(persistentAliasUpdates);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function setPersistentSettings(updates) {
|
||
const persistedUpdates = buildPersistentSettingsPayload(updates);
|
||
|
||
if (Object.keys(persistedUpdates).length > 0) {
|
||
await chrome.storage.local.set(persistedUpdates);
|
||
}
|
||
}
|
||
|
||
function buildSettingsExportFilename(date = new Date()) {
|
||
const pad = (value) => String(value).padStart(2, '0');
|
||
return `${SETTINGS_EXPORT_FILENAME_PREFIX}-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}.json`;
|
||
}
|
||
|
||
async function exportSettingsBundle() {
|
||
const settings = await getPersistedSettings();
|
||
const bundle = {
|
||
schemaVersion: SETTINGS_EXPORT_SCHEMA_VERSION,
|
||
exportedAt: new Date().toISOString(),
|
||
extensionVersion: chrome.runtime.getManifest().version,
|
||
settings,
|
||
};
|
||
|
||
return {
|
||
fileName: buildSettingsExportFilename(),
|
||
fileContent: JSON.stringify(bundle, null, 2),
|
||
};
|
||
}
|
||
|
||
async function importSettingsBundle(configBundle) {
|
||
const state = await ensureManualInteractionAllowed('\u5bfc\u5165\u914d\u7f6e');
|
||
if (Object.values(state.stepStatuses || {}).some((status) => status === 'running')) {
|
||
throw new Error('\u5f53\u524d\u6709\u6b65\u9aa4\u6b63\u5728\u6267\u884c\uff0c\u65e0\u6cd5\u5bfc\u5165\u914d\u7f6e\u3002');
|
||
}
|
||
if (!configBundle || typeof configBundle !== 'object' || Array.isArray(configBundle)) {
|
||
throw new Error('\u914d\u7f6e\u6587\u4ef6\u5185\u5bb9\u65e0\u6548\u3002');
|
||
}
|
||
|
||
const schemaVersion = Number(configBundle.schemaVersion);
|
||
if (schemaVersion !== SETTINGS_EXPORT_SCHEMA_VERSION) {
|
||
throw new Error(`\u4ec5\u652f\u6301\u5bfc\u5165 schemaVersion=${SETTINGS_EXPORT_SCHEMA_VERSION} \u7684\u914d\u7f6e\u6587\u4ef6\u3002`);
|
||
}
|
||
if (!configBundle.settings || typeof configBundle.settings !== 'object' || Array.isArray(configBundle.settings)) {
|
||
throw new Error('\u914d\u7f6e\u6587\u4ef6\u7f3a\u5c11 settings \u914d\u7f6e\u6bb5\u3002');
|
||
}
|
||
|
||
const importedSettings = buildPersistentSettingsPayload(configBundle.settings, {
|
||
fillDefaults: true,
|
||
requireKnownKeys: true,
|
||
});
|
||
|
||
await setPersistentSettings(importedSettings);
|
||
|
||
const sessionUpdates = {
|
||
...importedSettings,
|
||
currentHotmailAccountId: null,
|
||
email: null,
|
||
};
|
||
|
||
await setState(sessionUpdates);
|
||
broadcastDataUpdate({
|
||
...importedSettings,
|
||
currentHotmailAccountId: null,
|
||
...(sessionUpdates.email !== undefined ? { email: sessionUpdates.email } : {}),
|
||
});
|
||
|
||
return getState();
|
||
}
|
||
|
||
function broadcastDataUpdate(payload) {
|
||
chrome.runtime.sendMessage({
|
||
type: 'DATA_UPDATED',
|
||
payload,
|
||
}).catch(() => { });
|
||
}
|
||
|
||
function broadcastIcloudAliasesChanged(payload = {}) {
|
||
chrome.runtime.sendMessage({
|
||
type: 'ICLOUD_ALIASES_CHANGED',
|
||
payload,
|
||
}).catch(() => { });
|
||
}
|
||
|
||
async function setEmailStateSilently(email) {
|
||
await setState({ email });
|
||
broadcastDataUpdate({ email });
|
||
}
|
||
|
||
async function setEmailState(email) {
|
||
await setEmailStateSilently(email);
|
||
if (email) {
|
||
await appendManualAccountRunRecordIfNeeded('step2_stopped', null, '步骤 2 已使用邮箱,流程尚未完成。');
|
||
await resumeAutoRunIfWaitingForEmail();
|
||
}
|
||
}
|
||
|
||
async function setPasswordState(password) {
|
||
await setState({ password });
|
||
broadcastDataUpdate({ password });
|
||
}
|
||
|
||
function buildContributionModeState(enabled, persistedSettings = {}, currentState = {}) {
|
||
const currentContributionState = {};
|
||
for (const key of CONTRIBUTION_RUNTIME_KEYS) {
|
||
currentContributionState[key] = currentState[key] !== undefined
|
||
? currentState[key]
|
||
: CONTRIBUTION_RUNTIME_DEFAULTS[key];
|
||
}
|
||
|
||
if (enabled) {
|
||
const routing = resolveContributionModeRoutingState({
|
||
...persistedSettings,
|
||
...currentState,
|
||
...currentContributionState,
|
||
});
|
||
return {
|
||
...currentContributionState,
|
||
contributionMode: true,
|
||
contributionModeExpected: true,
|
||
contributionSource: routing.source,
|
||
contributionTargetGroupName: routing.targetGroupName,
|
||
panelMode: routing.source,
|
||
customPassword: '',
|
||
accountRunHistoryTextEnabled: false,
|
||
};
|
||
}
|
||
|
||
return {
|
||
...CONTRIBUTION_RUNTIME_DEFAULTS,
|
||
contributionMode: false,
|
||
contributionModeExpected: false,
|
||
panelMode: persistedSettings.panelMode || DEFAULT_STATE.panelMode,
|
||
customPassword: persistedSettings.customPassword || '',
|
||
accountRunHistoryTextEnabled: Boolean(persistedSettings.accountRunHistoryTextEnabled),
|
||
};
|
||
}
|
||
|
||
async function setContributionMode(enabled) {
|
||
const normalizedEnabled = Boolean(enabled);
|
||
const [persistedSettings, currentState] = await Promise.all([
|
||
getPersistedSettings(),
|
||
getState(),
|
||
]);
|
||
|
||
const updates = buildContributionModeState(normalizedEnabled, persistedSettings, currentState);
|
||
|
||
await setState(updates);
|
||
const nextState = await getState();
|
||
const contributionBroadcast = {};
|
||
for (const key of CONTRIBUTION_RUNTIME_KEYS) {
|
||
contributionBroadcast[key] = nextState[key];
|
||
}
|
||
broadcastDataUpdate({
|
||
...contributionBroadcast,
|
||
panelMode: nextState.panelMode,
|
||
customPassword: nextState.customPassword,
|
||
accountRunHistoryTextEnabled: nextState.accountRunHistoryTextEnabled,
|
||
accountRunHistoryHelperBaseUrl: nextState.accountRunHistoryHelperBaseUrl,
|
||
});
|
||
return nextState;
|
||
}
|
||
|
||
function getLuckmailUsedPurchases(state = {}) {
|
||
return normalizeLuckmailUsedPurchases(state?.luckmailUsedPurchases);
|
||
}
|
||
|
||
function getLuckmailPreserveTagInfo(state = {}) {
|
||
return {
|
||
id: Number(state?.luckmailPreserveTagId) || 0,
|
||
name: String(state?.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
|
||
};
|
||
}
|
||
|
||
async function setLuckmailUsedPurchasesState(usedPurchases) {
|
||
const normalizedUsedPurchases = normalizeLuckmailUsedPurchases(usedPurchases);
|
||
await setState({ luckmailUsedPurchases: normalizedUsedPurchases });
|
||
broadcastDataUpdate({ luckmailUsedPurchases: normalizedUsedPurchases });
|
||
return normalizedUsedPurchases;
|
||
}
|
||
|
||
async function setLuckmailPurchaseUsedState(purchaseId, used) {
|
||
const normalizedPurchaseId = normalizeLuckmailPurchaseId(purchaseId);
|
||
if (!normalizedPurchaseId) {
|
||
throw new Error('LuckMail 邮箱 ID 无效。');
|
||
}
|
||
|
||
const state = await getState();
|
||
const usedPurchases = getLuckmailUsedPurchases(state);
|
||
if (used) {
|
||
usedPurchases[normalizedPurchaseId] = true;
|
||
} else {
|
||
delete usedPurchases[normalizedPurchaseId];
|
||
}
|
||
|
||
await setLuckmailUsedPurchasesState(usedPurchases);
|
||
return {
|
||
purchaseId: Number(normalizedPurchaseId),
|
||
used: Boolean(used),
|
||
};
|
||
}
|
||
|
||
async function setLuckmailPreserveTagInfo(tag) {
|
||
const normalizedTags = normalizeLuckmailTags([tag]);
|
||
const normalizedTag = normalizedTags[0] || {
|
||
id: 0,
|
||
name: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
|
||
};
|
||
const updates = {
|
||
luckmailPreserveTagId: Number(normalizedTag.id) || 0,
|
||
luckmailPreserveTagName: String(normalizedTag.name || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
|
||
};
|
||
await setState(updates);
|
||
broadcastDataUpdate(updates);
|
||
return updates;
|
||
}
|
||
|
||
async function setLuckmailPurchaseState(purchase) {
|
||
const normalizedPurchase = purchase ? normalizeLuckmailPurchase(purchase) : null;
|
||
await setState({ currentLuckmailPurchase: normalizedPurchase });
|
||
broadcastDataUpdate({ currentLuckmailPurchase: normalizedPurchase });
|
||
return normalizedPurchase;
|
||
}
|
||
|
||
async function setLuckmailMailCursorState(cursor) {
|
||
const normalizedCursor = cursor ? normalizeLuckmailMailCursor(cursor) : null;
|
||
await setState({ currentLuckmailMailCursor: normalizedCursor });
|
||
return normalizedCursor;
|
||
}
|
||
|
||
async function clearLuckmailRuntimeState(options = {}) {
|
||
const { clearEmail = false } = options;
|
||
const updates = {
|
||
currentLuckmailPurchase: null,
|
||
currentLuckmailMailCursor: null,
|
||
};
|
||
if (clearEmail) {
|
||
updates.email = null;
|
||
}
|
||
await setState(updates);
|
||
broadcastDataUpdate(updates);
|
||
}
|
||
|
||
function getManualAliasUsageMap(state) {
|
||
return normalizeBooleanMap(state?.manualAliasUsage);
|
||
}
|
||
|
||
function getPreservedAliasMap(state) {
|
||
return normalizeBooleanMap(state?.preservedAliases);
|
||
}
|
||
|
||
function isAliasPreserved(state, email) {
|
||
const normalizedEmail = String(email || '').trim().toLowerCase();
|
||
if (!normalizedEmail) return false;
|
||
return Boolean(getPreservedAliasMap(state)[normalizedEmail]);
|
||
}
|
||
|
||
function getEffectiveUsedEmails(state) {
|
||
return toNormalizedEmailSet(getManualAliasUsageMap(state));
|
||
}
|
||
|
||
function normalizeIcloudAliasCacheList(value = [], options = {}) {
|
||
const aliases = Array.isArray(value) ? value : [];
|
||
const usedEmails = toNormalizedEmailSet(options.usedEmails);
|
||
const preservedEmails = toNormalizedEmailSet(options.preservedEmails);
|
||
return aliases
|
||
.map((alias) => normalizeIcloudAliasRecord(alias, { usedEmails, preservedEmails }))
|
||
.filter(Boolean)
|
||
.sort((left, right) => {
|
||
if (left.active !== right.active) return left.active ? -1 : 1;
|
||
if (left.used !== right.used) return left.used ? 1 : -1;
|
||
return String(left.email).localeCompare(String(right.email));
|
||
});
|
||
}
|
||
|
||
function getIcloudAliasCacheFromState(state, options = {}) {
|
||
const maxAgeMs = Math.max(0, Number(options.maxAgeMs) || ICLOUD_ALIAS_CACHE_MAX_AGE_MS);
|
||
const cachedAt = Number(state?.icloudAliasCacheAt || 0);
|
||
if (!Array.isArray(state?.icloudAliasCache) || state.icloudAliasCache.length <= 0) {
|
||
return [];
|
||
}
|
||
if (maxAgeMs > 0 && cachedAt > 0 && Date.now() - cachedAt > maxAgeMs) {
|
||
return [];
|
||
}
|
||
return normalizeIcloudAliasCacheList(state.icloudAliasCache, {
|
||
usedEmails: getEffectiveUsedEmails(state),
|
||
preservedEmails: getPreservedAliasMap(state),
|
||
});
|
||
}
|
||
|
||
function isLikelyIcloudAliasEmail(value = '') {
|
||
const email = String(value || '').trim().toLowerCase();
|
||
if (!email || !email.includes('@')) {
|
||
return false;
|
||
}
|
||
return /@(icloud\.com|me\.com|mac\.com|privaterelay\.appleid\.com)$/.test(email);
|
||
}
|
||
|
||
function buildIcloudAliasFallbackFromLocalState(state = {}) {
|
||
const manualAliasUsage = getManualAliasUsageMap(state);
|
||
const preservedAliases = getPreservedAliasMap(state);
|
||
const candidates = new Set();
|
||
|
||
for (const email of Object.keys(manualAliasUsage)) {
|
||
if (isLikelyIcloudAliasEmail(email)) {
|
||
candidates.add(String(email).trim().toLowerCase());
|
||
}
|
||
}
|
||
for (const email of Object.keys(preservedAliases)) {
|
||
if (isLikelyIcloudAliasEmail(email)) {
|
||
candidates.add(String(email).trim().toLowerCase());
|
||
}
|
||
}
|
||
|
||
const currentEmail = String(state?.email || '').trim().toLowerCase();
|
||
if (isLikelyIcloudAliasEmail(currentEmail)) {
|
||
candidates.add(currentEmail);
|
||
}
|
||
|
||
if (!candidates.size) {
|
||
return [];
|
||
}
|
||
|
||
const aliases = Array.from(candidates, (email) => ({
|
||
hme: email,
|
||
email,
|
||
state: 'active',
|
||
active: true,
|
||
}));
|
||
return normalizeIcloudAliasCacheList(aliases, {
|
||
usedEmails: getEffectiveUsedEmails(state),
|
||
preservedEmails: preservedAliases,
|
||
});
|
||
}
|
||
|
||
async function setIcloudAliasUsedState(payload = {}, options = {}) {
|
||
const email = String(payload.email || '').trim().toLowerCase();
|
||
if (!email) {
|
||
throw new Error('未提供 iCloud 隐私邮箱地址。');
|
||
}
|
||
|
||
const used = Boolean(payload.used);
|
||
const state = await getState();
|
||
const manualAliasUsage = getManualAliasUsageMap(state);
|
||
manualAliasUsage[email] = used;
|
||
await setState({ manualAliasUsage });
|
||
if (!options.silentLog) {
|
||
await addLog(`iCloud:已将 ${email} 标记为${used ? '已用' : '未用'}`, 'ok');
|
||
}
|
||
broadcastIcloudAliasesChanged({ reason: 'used-updated', email, used });
|
||
return { email, used };
|
||
}
|
||
|
||
async function setIcloudAliasPreservedState(payload = {}) {
|
||
const email = String(payload.email || '').trim().toLowerCase();
|
||
if (!email) {
|
||
throw new Error('未提供 iCloud 隐私邮箱地址。');
|
||
}
|
||
|
||
const preserved = Boolean(payload.preserved);
|
||
const state = await getState();
|
||
const preservedAliases = getPreservedAliasMap(state);
|
||
preservedAliases[email] = preserved;
|
||
await setState({ preservedAliases });
|
||
await addLog(`iCloud:已将 ${email} ${preserved ? '设为保留' : '取消保留'}`, 'ok');
|
||
broadcastIcloudAliasesChanged({ reason: 'preserved-updated', email, preserved });
|
||
return { email, preserved };
|
||
}
|
||
|
||
async function resetState() {
|
||
console.log(LOG_PREFIX, 'Resetting all state');
|
||
// Preserve settings and persistent data across resets
|
||
const [prev, persistedSettings, persistedAliasState] = await Promise.all([
|
||
chrome.storage.session.get([
|
||
'seenCodes',
|
||
'seenInbucketMailIds',
|
||
'accounts',
|
||
'tabRegistry',
|
||
'sourceLastUrls',
|
||
'reusablePhoneActivation',
|
||
'luckmailApiKey',
|
||
'luckmailBaseUrl',
|
||
'luckmailEmailType',
|
||
'luckmailDomain',
|
||
'luckmailUsedPurchases',
|
||
'luckmailPreserveTagId',
|
||
'luckmailPreserveTagName',
|
||
'preferredIcloudHost',
|
||
...CONTRIBUTION_RUNTIME_KEYS,
|
||
]),
|
||
getPersistedSettings(),
|
||
getPersistedAliasState(),
|
||
]);
|
||
const contributionModeState = buildContributionModeState(Boolean(prev.contributionMode), persistedSettings, prev);
|
||
const reusablePhoneActivation = (
|
||
prev.reusablePhoneActivation
|
||
&& typeof prev.reusablePhoneActivation === 'object'
|
||
&& !Array.isArray(prev.reusablePhoneActivation)
|
||
&& String(
|
||
prev.reusablePhoneActivation.activationId
|
||
?? prev.reusablePhoneActivation.id
|
||
?? prev.reusablePhoneActivation.activation
|
||
?? ''
|
||
).trim()
|
||
&& String(
|
||
prev.reusablePhoneActivation.phoneNumber
|
||
?? prev.reusablePhoneActivation.number
|
||
?? prev.reusablePhoneActivation.phone
|
||
?? ''
|
||
).trim()
|
||
)
|
||
? prev.reusablePhoneActivation
|
||
: null;
|
||
await chrome.storage.session.clear();
|
||
await chrome.storage.session.set({
|
||
...DEFAULT_STATE,
|
||
...persistedSettings,
|
||
...persistedAliasState,
|
||
...contributionModeState,
|
||
seenCodes: prev.seenCodes || [],
|
||
seenInbucketMailIds: prev.seenInbucketMailIds || [],
|
||
accounts: prev.accounts || [],
|
||
tabRegistry: prev.tabRegistry || {},
|
||
sourceLastUrls: prev.sourceLastUrls || {},
|
||
luckmailApiKey: String(prev.luckmailApiKey || ''),
|
||
luckmailBaseUrl: normalizeLuckmailBaseUrl(prev.luckmailBaseUrl),
|
||
luckmailEmailType: normalizeLuckmailEmailType(prev.luckmailEmailType),
|
||
luckmailDomain: String(prev.luckmailDomain || '').trim(),
|
||
luckmailUsedPurchases: normalizeLuckmailUsedPurchases(prev.luckmailUsedPurchases),
|
||
luckmailPreserveTagId: Number(prev.luckmailPreserveTagId) || 0,
|
||
luckmailPreserveTagName: String(prev.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
|
||
currentLuckmailPurchase: null,
|
||
currentLuckmailMailCursor: null,
|
||
// Keep reusable phone activation across round resets so the same number can be reactivated up to maxUses.
|
||
reusablePhoneActivation,
|
||
preferredIcloudHost: prev.preferredIcloudHost || '',
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Generate a random password: 14 chars, mix of uppercase, lowercase, digits, symbols.
|
||
*/
|
||
function generatePassword() {
|
||
const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
|
||
const lower = 'abcdefghjkmnpqrstuvwxyz';
|
||
const digits = '23456789';
|
||
const symbols = '!@#$%&*?';
|
||
const all = upper + lower + digits + symbols;
|
||
|
||
// Ensure at least one of each type
|
||
let pw = '';
|
||
pw += upper[Math.floor(Math.random() * upper.length)];
|
||
pw += lower[Math.floor(Math.random() * lower.length)];
|
||
pw += digits[Math.floor(Math.random() * digits.length)];
|
||
pw += symbols[Math.floor(Math.random() * symbols.length)];
|
||
|
||
// Fill remaining 10 chars
|
||
for (let i = 0; i < 10; i++) {
|
||
pw += all[Math.floor(Math.random() * all.length)];
|
||
}
|
||
|
||
// Shuffle
|
||
return pw.split('').sort(() => Math.random() - 0.5).join('');
|
||
}
|
||
|
||
function normalizePayPalAccount(account = {}) {
|
||
if (self.PayPalUtils?.normalizePayPalAccount) {
|
||
return self.PayPalUtils.normalizePayPalAccount(account);
|
||
}
|
||
return {
|
||
id: String(account.id || crypto.randomUUID()),
|
||
email: String(account.email || '').trim().toLowerCase(),
|
||
password: String(account.password || ''),
|
||
createdAt: Number.isFinite(Number(account.createdAt)) ? Number(account.createdAt) : Date.now(),
|
||
updatedAt: Number.isFinite(Number(account.updatedAt)) ? Number(account.updatedAt) : Date.now(),
|
||
lastUsedAt: Number.isFinite(Number(account.lastUsedAt)) ? Number(account.lastUsedAt) : 0,
|
||
};
|
||
}
|
||
|
||
function normalizePayPalAccounts(accounts) {
|
||
if (self.PayPalUtils?.normalizePayPalAccounts) {
|
||
return self.PayPalUtils.normalizePayPalAccounts(accounts);
|
||
}
|
||
return Array.isArray(accounts) ? accounts.map((account) => normalizePayPalAccount(account)) : [];
|
||
}
|
||
|
||
function findPayPalAccount(accounts, accountId) {
|
||
if (self.PayPalUtils?.findPayPalAccount) {
|
||
return self.PayPalUtils.findPayPalAccount(accounts, accountId);
|
||
}
|
||
const normalizedId = String(accountId || '').trim();
|
||
if (!normalizedId) return null;
|
||
return normalizePayPalAccounts(accounts).find((account) => account.id === normalizedId) || null;
|
||
}
|
||
|
||
function upsertPayPalAccountInList(accounts, nextAccount) {
|
||
if (self.PayPalUtils?.upsertPayPalAccountInList) {
|
||
return self.PayPalUtils.upsertPayPalAccountInList(accounts, nextAccount);
|
||
}
|
||
const normalizedNext = normalizePayPalAccount(nextAccount);
|
||
const list = normalizePayPalAccounts(accounts);
|
||
const existingIndex = list.findIndex((account) => account.id === normalizedNext.id);
|
||
if (existingIndex >= 0) {
|
||
list[existingIndex] = normalizedNext;
|
||
return list;
|
||
}
|
||
return [...list, normalizedNext];
|
||
}
|
||
|
||
function normalizeHotmailAccount(account = {}) {
|
||
const normalizedLastAuthAt = Number.isFinite(Number(account.lastAuthAt)) ? Number(account.lastAuthAt) : 0;
|
||
const normalizedStatus = String(
|
||
account.status
|
||
|| (normalizedLastAuthAt > 0 ? 'authorized' : 'pending')
|
||
);
|
||
return {
|
||
id: String(account.id || crypto.randomUUID()),
|
||
email: String(account.email || '').trim(),
|
||
password: String(account.password || ''),
|
||
clientId: String(account.clientId || '').trim(),
|
||
refreshToken: String(account.refreshToken || ''),
|
||
status: normalizedStatus,
|
||
enabled: account.enabled !== undefined ? Boolean(account.enabled) : true,
|
||
used: Boolean(account.used),
|
||
lastUsedAt: Number.isFinite(Number(account.lastUsedAt)) ? Number(account.lastUsedAt) : 0,
|
||
lastAuthAt: normalizedLastAuthAt,
|
||
lastError: String(account.lastError || ''),
|
||
};
|
||
}
|
||
|
||
function normalizeHotmailAccounts(accounts) {
|
||
if (!Array.isArray(accounts)) return [];
|
||
|
||
const deduped = new Map();
|
||
for (const account of accounts) {
|
||
const normalized = normalizeHotmailAccount(account);
|
||
if (!normalized.email && !normalized.id) continue;
|
||
deduped.set(normalized.id, normalized);
|
||
}
|
||
return [...deduped.values()];
|
||
}
|
||
|
||
function findHotmailAccount(accounts, accountId) {
|
||
return normalizeHotmailAccounts(accounts).find((account) => account.id === accountId) || null;
|
||
}
|
||
|
||
function isHotmailProvider(stateOrProvider) {
|
||
const provider = typeof stateOrProvider === 'string'
|
||
? stateOrProvider
|
||
: stateOrProvider?.mailProvider;
|
||
return provider === HOTMAIL_PROVIDER;
|
||
}
|
||
|
||
function isLuckmailProvider(stateOrProvider) {
|
||
const provider = typeof stateOrProvider === 'string'
|
||
? stateOrProvider
|
||
: stateOrProvider?.mailProvider;
|
||
return provider === LUCKMAIL_PROVIDER;
|
||
}
|
||
|
||
function isCustomMailProvider(stateOrProvider) {
|
||
const provider = typeof stateOrProvider === 'string'
|
||
? stateOrProvider
|
||
: stateOrProvider?.mailProvider;
|
||
return provider === 'custom';
|
||
}
|
||
|
||
function getMail2925Mode(stateOrMode) {
|
||
if (typeof stateOrMode === 'string') {
|
||
return normalizeMail2925Mode(stateOrMode);
|
||
}
|
||
return normalizeMail2925Mode(stateOrMode?.mail2925Mode);
|
||
}
|
||
|
||
async function syncHotmailAccounts(accounts) {
|
||
const normalized = normalizeHotmailAccounts(accounts);
|
||
await setPersistentSettings({ hotmailAccounts: normalized });
|
||
await setState({ hotmailAccounts: normalized });
|
||
broadcastDataUpdate({ hotmailAccounts: normalized });
|
||
return normalized;
|
||
}
|
||
|
||
async function upsertHotmailAccount(input) {
|
||
const state = await getState();
|
||
const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
|
||
const normalizedEmail = String(input?.email || '').trim().toLowerCase();
|
||
const existing = input?.id
|
||
? findHotmailAccount(accounts, input.id)
|
||
: accounts.find((account) => account.email.toLowerCase() === normalizedEmail) || null;
|
||
const credentialsChanged = !existing
|
||
|| (input?.clientId !== undefined && String(input.clientId).trim() !== existing.clientId)
|
||
|| (input?.refreshToken !== undefined && String(input.refreshToken).trim() !== existing.refreshToken)
|
||
|| (input?.email !== undefined && String(input.email).trim().toLowerCase() !== existing.email.toLowerCase());
|
||
const normalized = normalizeHotmailAccount({
|
||
...(existing || {}),
|
||
...(credentialsChanged ? {
|
||
status: 'pending',
|
||
lastAuthAt: 0,
|
||
lastError: '',
|
||
} : {}),
|
||
...input,
|
||
id: input?.id || existing?.id || crypto.randomUUID(),
|
||
});
|
||
|
||
const nextAccounts = existing
|
||
? accounts.map((account) => (account.id === normalized.id ? normalized : account))
|
||
: [...accounts, normalized];
|
||
|
||
await syncHotmailAccounts(nextAccounts);
|
||
return normalized;
|
||
}
|
||
|
||
async function deleteHotmailAccount(accountId) {
|
||
const state = await getState();
|
||
const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
|
||
const nextAccounts = accounts.filter((account) => account.id !== accountId);
|
||
await syncHotmailAccounts(nextAccounts);
|
||
|
||
if (state.currentHotmailAccountId === accountId) {
|
||
await setState({ currentHotmailAccountId: null });
|
||
if (isHotmailProvider(state)) {
|
||
await setEmailState(null);
|
||
}
|
||
broadcastDataUpdate({ currentHotmailAccountId: null });
|
||
}
|
||
}
|
||
|
||
async function deleteHotmailAccounts(mode = 'all') {
|
||
const state = await getState();
|
||
const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
|
||
const targets = filterHotmailAccountsByUsage(accounts, mode);
|
||
const targetIds = new Set(targets.map((account) => account.id));
|
||
const nextAccounts = mode === 'used'
|
||
? accounts.filter((account) => !targetIds.has(account.id))
|
||
: [];
|
||
|
||
await syncHotmailAccounts(nextAccounts);
|
||
|
||
if (state.currentHotmailAccountId && targetIds.has(state.currentHotmailAccountId)) {
|
||
await setState({ currentHotmailAccountId: null });
|
||
if (isHotmailProvider(state)) {
|
||
await setEmailState(null);
|
||
}
|
||
broadcastDataUpdate({ currentHotmailAccountId: null });
|
||
}
|
||
|
||
return {
|
||
deletedCount: targets.length,
|
||
remainingCount: nextAccounts.length,
|
||
};
|
||
}
|
||
|
||
async function patchHotmailAccount(accountId, updates = {}) {
|
||
const state = await getState();
|
||
const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
|
||
const account = findHotmailAccount(accounts, accountId);
|
||
if (!account) {
|
||
throw new Error('未找到对应的 Hotmail 账号。');
|
||
}
|
||
|
||
const nextAccount = normalizeHotmailAccount({
|
||
...account,
|
||
...updates,
|
||
id: account.id,
|
||
});
|
||
|
||
await syncHotmailAccounts(accounts.map((item) => (item.id === account.id ? nextAccount : item)));
|
||
|
||
if (state.currentHotmailAccountId === account.id && shouldClearHotmailCurrentSelection(nextAccount)) {
|
||
await setState({ currentHotmailAccountId: null });
|
||
broadcastDataUpdate({ currentHotmailAccountId: null });
|
||
if (isHotmailProvider(state)) {
|
||
await setEmailState(null);
|
||
}
|
||
}
|
||
|
||
return nextAccount;
|
||
}
|
||
|
||
async function setCurrentHotmailAccount(accountId, options = {}) {
|
||
const { markUsed = false, syncEmail = true } = options;
|
||
const state = await getState();
|
||
const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
|
||
const account = findHotmailAccount(accounts, accountId);
|
||
if (!account) {
|
||
throw new Error('未找到对应的 Hotmail 账号。');
|
||
}
|
||
|
||
if (markUsed) {
|
||
account.lastUsedAt = Date.now();
|
||
await syncHotmailAccounts(accounts.map((item) => (item.id === account.id ? account : item)));
|
||
}
|
||
|
||
await setState({ currentHotmailAccountId: account.id });
|
||
broadcastDataUpdate({ currentHotmailAccountId: account.id });
|
||
if (syncEmail) {
|
||
await setEmailState(account.email || null);
|
||
}
|
||
return account;
|
||
}
|
||
|
||
function isAuthorizedHotmailRunAccount(candidate) {
|
||
return Boolean(candidate)
|
||
&& candidate.status === 'authorized'
|
||
&& !candidate.used
|
||
&& Boolean(candidate.refreshToken);
|
||
}
|
||
|
||
function isPendingHotmailVerificationCandidate(candidate) {
|
||
return Boolean(candidate)
|
||
&& candidate.status === 'pending'
|
||
&& !candidate.used
|
||
&& Boolean(candidate.refreshToken);
|
||
}
|
||
|
||
function compareHotmailAccountAllocationPriority(left, right) {
|
||
const leftUsedAt = Number(left?.lastUsedAt) || 0;
|
||
const rightUsedAt = Number(right?.lastUsedAt) || 0;
|
||
if (leftUsedAt !== rightUsedAt) {
|
||
return leftUsedAt - rightUsedAt;
|
||
}
|
||
|
||
return String(left?.email || '').localeCompare(String(right?.email || ''));
|
||
}
|
||
|
||
function pickPendingHotmailAccountForVerification(accounts, options = {}) {
|
||
const excludeIds = new Set((options.excludeIds || []).filter(Boolean));
|
||
const candidates = normalizeHotmailAccounts(accounts)
|
||
.filter((candidate) => isPendingHotmailVerificationCandidate(candidate) && !excludeIds.has(candidate.id));
|
||
if (!candidates.length) {
|
||
return null;
|
||
}
|
||
|
||
const preferredAccountId = String(options.preferredAccountId || '').trim();
|
||
if (preferredAccountId) {
|
||
const preferredCandidate = candidates.find((candidate) => candidate.id === preferredAccountId);
|
||
if (preferredCandidate) {
|
||
return preferredCandidate;
|
||
}
|
||
}
|
||
|
||
return candidates
|
||
.slice()
|
||
.sort(compareHotmailAccountAllocationPriority)[0] || null;
|
||
}
|
||
|
||
async function ensureHotmailAccountForFlow(options = {}) {
|
||
const {
|
||
allowAllocate = true,
|
||
markUsed = false,
|
||
preferredAccountId = null,
|
||
excludeIds = [],
|
||
} = options;
|
||
const state = await getState();
|
||
const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
|
||
const excludedAccountIds = new Set((excludeIds || []).filter(Boolean));
|
||
const availableAccounts = accounts.filter((candidate) => isAuthorizedHotmailRunAccount(candidate) && !excludedAccountIds.has(candidate.id));
|
||
|
||
let account = null;
|
||
if (preferredAccountId && !excludedAccountIds.has(preferredAccountId)) {
|
||
account = findHotmailAccount(accounts, preferredAccountId);
|
||
}
|
||
if ((!account || !isAuthorizedHotmailRunAccount(account)) && state.currentHotmailAccountId && !excludedAccountIds.has(state.currentHotmailAccountId)) {
|
||
account = findHotmailAccount(accounts, state.currentHotmailAccountId);
|
||
}
|
||
if ((!account || !isAuthorizedHotmailRunAccount(account)) && allowAllocate) {
|
||
account = availableAccounts.length ? pickHotmailAccountForRun(availableAccounts, {}) : null;
|
||
}
|
||
|
||
if (!account) {
|
||
throw new Error('没有可用的 Hotmail 账号。请先在侧边栏添加至少一个带刷新令牌(refresh token)的账号。');
|
||
}
|
||
if (!isAuthorizedHotmailRunAccount(account)) {
|
||
throw new Error(`Hotmail 账号 ${account.email || account.id} 尚未就绪,无法读取邮件。`);
|
||
}
|
||
|
||
return setCurrentHotmailAccount(account.id, { markUsed, syncEmail: true });
|
||
}
|
||
|
||
function buildHotmailLocalEndpoint(baseUrl, path) {
|
||
const normalizedBaseUrl = normalizeHotmailLocalBaseUrl(baseUrl);
|
||
return new URL(path, `${normalizedBaseUrl}/`).toString();
|
||
}
|
||
|
||
async function requestHotmailRemoteMailbox(account, mailbox = 'INBOX') {
|
||
if (!account?.email) {
|
||
throw new Error('Hotmail 账号缺少邮箱地址。');
|
||
}
|
||
if (!account?.clientId) {
|
||
throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少客户端 ID。`);
|
||
}
|
||
if (!account?.refreshToken) {
|
||
throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少刷新令牌(refresh token)。`);
|
||
}
|
||
|
||
const { timeoutMs } = getHotmailMailApiRequestConfig();
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
|
||
|
||
try {
|
||
const result = await fetchMicrosoftMailboxMessages({
|
||
clientId: account.clientId,
|
||
refreshToken: account.refreshToken,
|
||
mailbox,
|
||
top: 10,
|
||
signal: controller.signal,
|
||
});
|
||
|
||
return {
|
||
mailbox,
|
||
payload: {
|
||
source: 'microsoft-api',
|
||
transport: result.transport,
|
||
tokenStrategy: result.tokenStrategy,
|
||
},
|
||
messages: normalizeHotmailMailApiMessages(result.messages).map((message) => ({
|
||
...message,
|
||
mailbox: message?.mailbox || mailbox,
|
||
})),
|
||
nextRefreshToken: result.nextRefreshToken,
|
||
};
|
||
} catch (err) {
|
||
if (err?.name === 'AbortError') {
|
||
throw new Error(`Hotmail API 对接请求超时(>${Math.round(timeoutMs / 1000)} 秒):${mailbox}`);
|
||
}
|
||
throw new Error(`Hotmail API 对接请求失败:${err.message}`);
|
||
} finally {
|
||
clearTimeout(timeoutId);
|
||
}
|
||
}
|
||
|
||
function applyHotmailApiResultToAccount(account, apiResult) {
|
||
const nextRefreshToken = String(apiResult?.nextRefreshToken || '').trim();
|
||
return {
|
||
...account,
|
||
refreshToken: nextRefreshToken || account.refreshToken,
|
||
status: 'authorized',
|
||
lastAuthAt: Date.now(),
|
||
lastError: '',
|
||
};
|
||
}
|
||
|
||
function buildHotmailMailApiFailureAccount(account, errorMessage) {
|
||
return normalizeHotmailAccount({
|
||
...account,
|
||
status: 'error',
|
||
lastError: String(errorMessage || ''),
|
||
});
|
||
}
|
||
|
||
async function fetchHotmailMailboxMessagesFromRemoteService(account, mailboxes = HOTMAIL_MAILBOXES) {
|
||
let workingAccount = normalizeHotmailAccount(account);
|
||
const mailboxResults = [];
|
||
|
||
try {
|
||
for (const mailbox of mailboxes) {
|
||
const result = await requestHotmailRemoteMailbox(workingAccount, mailbox);
|
||
workingAccount = applyHotmailApiResultToAccount(workingAccount, result);
|
||
mailboxResults.push({
|
||
mailbox,
|
||
count: result.messages.length,
|
||
messages: result.messages.map((message) => ({
|
||
...message,
|
||
mailbox: message?.mailbox || mailbox,
|
||
})),
|
||
});
|
||
}
|
||
} catch (err) {
|
||
const failedAccount = buildHotmailMailApiFailureAccount(workingAccount, err.message);
|
||
await upsertHotmailAccount(failedAccount);
|
||
throw err;
|
||
}
|
||
|
||
const savedAccount = await upsertHotmailAccount(workingAccount);
|
||
return {
|
||
account: savedAccount,
|
||
mailboxResults,
|
||
messages: mailboxResults.flatMap((item) => item.messages),
|
||
};
|
||
}
|
||
|
||
async function requestHotmailLocalMessages(account, mailboxes = HOTMAIL_MAILBOXES) {
|
||
if (!account?.email) {
|
||
throw new Error('Hotmail 账号缺少邮箱地址。');
|
||
}
|
||
if (!account?.clientId) {
|
||
throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少客户端 ID。`);
|
||
}
|
||
if (!account?.refreshToken) {
|
||
throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少刷新令牌(refresh token)。`);
|
||
}
|
||
|
||
const serviceSettings = getHotmailServiceSettings(await getState());
|
||
const { timeoutMs } = getHotmailMailApiRequestConfig();
|
||
const requestTimeoutMs = Math.max(timeoutMs, HOTMAIL_LOCAL_HELPER_TIMEOUT_MS);
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), requestTimeoutMs);
|
||
|
||
let response;
|
||
try {
|
||
response = await fetch(buildHotmailLocalEndpoint(serviceSettings.localBaseUrl, '/messages'), {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
Accept: 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
email: account.email,
|
||
clientId: account.clientId,
|
||
refreshToken: account.refreshToken,
|
||
mailboxes,
|
||
top: 5,
|
||
}),
|
||
signal: controller.signal,
|
||
});
|
||
} catch (err) {
|
||
if (err?.name === 'AbortError') {
|
||
throw new Error(`Hotmail 本地助手请求超时(>${Math.round(requestTimeoutMs / 1000)} 秒)`);
|
||
}
|
||
throw new Error(`Hotmail 本地助手请求失败:${err.message}`);
|
||
} finally {
|
||
clearTimeout(timeoutId);
|
||
}
|
||
|
||
const text = await response.text();
|
||
let payload = {};
|
||
try {
|
||
payload = text ? JSON.parse(text) : {};
|
||
} catch {
|
||
payload = { raw: text };
|
||
}
|
||
|
||
if (!response.ok || payload?.ok === false) {
|
||
const errorText = payload?.error || payload?.message || text || `HTTP ${response.status}`;
|
||
throw new Error(`Hotmail 本地助手返回失败:${errorText}`);
|
||
}
|
||
|
||
const rawMessages = Array.isArray(payload?.messages) ? payload.messages : [];
|
||
const normalizedMessages = normalizeHotmailMailApiMessages(rawMessages).map((message, index) => ({
|
||
...message,
|
||
mailbox: rawMessages[index]?.mailbox || 'INBOX',
|
||
receivedTimestamp: Number(rawMessages[index]?.receivedTimestamp || 0) || 0,
|
||
}));
|
||
const mailboxResults = Array.isArray(payload?.mailboxResults)
|
||
? payload.mailboxResults.map((item) => ({
|
||
mailbox: String(item?.mailbox || 'INBOX'),
|
||
count: Number(item?.count || 0),
|
||
messages: normalizedMessages.filter((message) => String(message.mailbox || 'INBOX') === String(item?.mailbox || 'INBOX')),
|
||
}))
|
||
: mailboxes.map((mailbox) => ({
|
||
mailbox,
|
||
count: normalizedMessages.filter((message) => String(message.mailbox || 'INBOX') === mailbox).length,
|
||
messages: normalizedMessages.filter((message) => String(message.mailbox || 'INBOX') === mailbox),
|
||
}));
|
||
|
||
const nextAccount = applyHotmailApiResultToAccount(account, {
|
||
nextRefreshToken: String(payload?.nextRefreshToken || '').trim(),
|
||
});
|
||
const savedAccount = await upsertHotmailAccount(nextAccount);
|
||
return {
|
||
account: savedAccount,
|
||
mailboxResults,
|
||
messages: normalizedMessages,
|
||
};
|
||
}
|
||
|
||
async function requestHotmailLocalCode(account, pollPayload = {}) {
|
||
if (!account?.email) {
|
||
throw new Error('Hotmail 账号缺少邮箱地址。');
|
||
}
|
||
if (!account?.clientId) {
|
||
throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少客户端 ID。`);
|
||
}
|
||
if (!account?.refreshToken) {
|
||
throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少刷新令牌(refresh token)。`);
|
||
}
|
||
|
||
const serviceSettings = getHotmailServiceSettings(await getState());
|
||
const { timeoutMs } = getHotmailMailApiRequestConfig();
|
||
const requestTimeoutMs = Math.max(timeoutMs, HOTMAIL_LOCAL_HELPER_TIMEOUT_MS);
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), requestTimeoutMs);
|
||
|
||
let response;
|
||
try {
|
||
response = await fetch(buildHotmailLocalEndpoint(serviceSettings.localBaseUrl, '/code'), {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
Accept: 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
email: account.email,
|
||
clientId: account.clientId,
|
||
refreshToken: account.refreshToken,
|
||
mailboxes: HOTMAIL_MAILBOXES,
|
||
top: 5,
|
||
senderFilters: pollPayload.senderFilters || [],
|
||
subjectFilters: pollPayload.subjectFilters || [],
|
||
excludeCodes: pollPayload.excludeCodes || [],
|
||
filterAfterTimestamp: Number(pollPayload.filterAfterTimestamp || 0) || 0,
|
||
}),
|
||
signal: controller.signal,
|
||
});
|
||
} catch (err) {
|
||
if (err?.name === 'AbortError') {
|
||
throw new Error(`Hotmail 本地助手请求超时(>${Math.round(requestTimeoutMs / 1000)} 秒)`);
|
||
}
|
||
throw new Error(`Hotmail 本地助手请求失败:${err.message}`);
|
||
} finally {
|
||
clearTimeout(timeoutId);
|
||
}
|
||
|
||
const text = await response.text();
|
||
let payload = {};
|
||
try {
|
||
payload = text ? JSON.parse(text) : {};
|
||
} catch {
|
||
payload = { raw: text };
|
||
}
|
||
|
||
if (!response.ok || payload?.ok === false) {
|
||
const errorText = payload?.error || payload?.message || text || `HTTP ${response.status}`;
|
||
throw new Error(`Hotmail 本地助手返回失败:${errorText}`);
|
||
}
|
||
|
||
const normalizedMessage = payload?.message
|
||
? {
|
||
...normalizeHotmailMailApiMessages([payload.message])[0],
|
||
mailbox: payload?.message?.mailbox || 'INBOX',
|
||
receivedTimestamp: Number(payload?.message?.receivedTimestamp || 0) || 0,
|
||
}
|
||
: null;
|
||
const nextAccount = applyHotmailApiResultToAccount(account, {
|
||
nextRefreshToken: String(payload?.nextRefreshToken || '').trim(),
|
||
});
|
||
const savedAccount = await upsertHotmailAccount(nextAccount);
|
||
return {
|
||
account: savedAccount,
|
||
code: String(payload?.code || ''),
|
||
message: normalizedMessage,
|
||
usedTimeFallback: Boolean(payload?.usedTimeFallback),
|
||
selectionSource: String(payload?.selectionSource || ''),
|
||
};
|
||
}
|
||
|
||
async function pollHotmailVerificationCodeViaLocalHelper(step, account, pollPayload = {}) {
|
||
const maxAttempts = Number(pollPayload.maxAttempts) || 5;
|
||
const intervalMs = Number(pollPayload.intervalMs) || 3000;
|
||
let workingAccount = account;
|
||
let lastError = null;
|
||
|
||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||
throwIfStopped();
|
||
try {
|
||
await addLog(`步骤 ${step}:正在通过本地助手轮询 Hotmail 验证码(${attempt}/${maxAttempts})...`, 'info');
|
||
const fetchResult = await requestHotmailLocalCode(workingAccount, pollPayload);
|
||
workingAccount = fetchResult.account;
|
||
|
||
if (fetchResult.code) {
|
||
const mailboxLabel = fetchResult.message?.mailbox || 'INBOX';
|
||
if (fetchResult.usedTimeFallback) {
|
||
await addLog(`步骤 ${step}:本地助手使用时间回退后命中 Hotmail ${mailboxLabel} 验证码。`, 'warn');
|
||
}
|
||
await addLog(`步骤 ${step}:已通过本地助手在 Hotmail ${mailboxLabel} 中找到验证码:${fetchResult.code}`, 'ok');
|
||
return {
|
||
ok: true,
|
||
code: fetchResult.code,
|
||
emailTimestamp: fetchResult.message?.receivedTimestamp || Date.now(),
|
||
mailId: fetchResult.message?.id || '',
|
||
};
|
||
}
|
||
|
||
lastError = new Error(`步骤 ${step}:本地助手暂未返回匹配验证码(${attempt}/${maxAttempts})。`);
|
||
await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info');
|
||
} catch (err) {
|
||
lastError = err;
|
||
await addLog(`步骤 ${step}:本地助手轮询 Hotmail 失败:${err.message}`, 'warn');
|
||
}
|
||
|
||
if (attempt < maxAttempts) {
|
||
await sleepWithStop(intervalMs);
|
||
}
|
||
}
|
||
|
||
throw lastError || new Error(`步骤 ${step}:本地助手未返回新的匹配验证码。`);
|
||
}
|
||
|
||
async function fetchHotmailMailboxMessages(account, mailboxes = HOTMAIL_MAILBOXES) {
|
||
const serviceSettings = getHotmailServiceSettings(await getState());
|
||
if (serviceSettings.mode === HOTMAIL_SERVICE_MODE_LOCAL) {
|
||
return requestHotmailLocalMessages(account, mailboxes);
|
||
}
|
||
return fetchHotmailMailboxMessagesFromRemoteService(account, mailboxes);
|
||
}
|
||
|
||
async function verifyHotmailAccount(accountId) {
|
||
const state = await getState();
|
||
const account = findHotmailAccount(state.hotmailAccounts, accountId);
|
||
if (!account) {
|
||
throw new Error('未找到需要校验的 Hotmail 账号。');
|
||
}
|
||
|
||
const result = await fetchHotmailMailboxMessages(account, ['INBOX']);
|
||
return {
|
||
account: result.account,
|
||
messageCount: result.mailboxResults[0]?.count || 0,
|
||
};
|
||
}
|
||
|
||
async function ensureHotmailMailboxReadyForAutoRunRound(options = {}) {
|
||
const {
|
||
targetRun = 0,
|
||
totalRuns = 0,
|
||
attemptRun = 1,
|
||
} = options;
|
||
const state = await getState();
|
||
if (!isHotmailProvider(state)) {
|
||
return null;
|
||
}
|
||
|
||
const buildRoundLabel = () => {
|
||
if (targetRun > 0 && totalRuns > 0) {
|
||
return `第 ${targetRun}/${totalRuns} 轮`;
|
||
}
|
||
return '当前轮';
|
||
};
|
||
const exhaustedAccountIds = new Set();
|
||
let preferredAccountId = state.currentHotmailAccountId || null;
|
||
let lastError = null;
|
||
|
||
while (true) {
|
||
throwIfStopped();
|
||
const latestState = await getState();
|
||
const latestAccounts = normalizeHotmailAccounts(latestState.hotmailAccounts);
|
||
const remainingAuthorizedAccounts = latestAccounts
|
||
.filter((candidate) => isAuthorizedHotmailRunAccount(candidate) && !exhaustedAccountIds.has(candidate.id));
|
||
const remainingPendingAccounts = latestAccounts
|
||
.filter((candidate) => isPendingHotmailVerificationCandidate(candidate) && !exhaustedAccountIds.has(candidate.id));
|
||
if (!remainingAuthorizedAccounts.length && !remainingPendingAccounts.length) {
|
||
if (lastError) {
|
||
throw new Error(`自动运行${buildRoundLabel()}开始前未找到可通过校验的 Hotmail 账号:${lastError.message}`);
|
||
}
|
||
throw new Error('没有可用的 Hotmail 账号。请先在侧边栏添加至少一个带刷新令牌(refresh token)的账号。');
|
||
}
|
||
|
||
let account = null;
|
||
if (remainingAuthorizedAccounts.length) {
|
||
account = await ensureHotmailAccountForFlow({
|
||
allowAllocate: true,
|
||
markUsed: false,
|
||
preferredAccountId,
|
||
excludeIds: [...exhaustedAccountIds],
|
||
});
|
||
} else {
|
||
const pendingAccount = pickPendingHotmailAccountForVerification(latestAccounts, {
|
||
preferredAccountId,
|
||
excludeIds: [...exhaustedAccountIds],
|
||
});
|
||
if (!pendingAccount) {
|
||
throw new Error('没有可用的 Hotmail 账号。请先在侧边栏添加至少一个带刷新令牌(refresh token)的账号。');
|
||
}
|
||
account = await setCurrentHotmailAccount(pendingAccount.id, {
|
||
markUsed: false,
|
||
syncEmail: true,
|
||
});
|
||
await addLog(
|
||
`自动运行${buildRoundLabel()}开始前未找到已校验 Hotmail 账号,正在尝试校验待校验账号 ${account.email}。`,
|
||
'warn'
|
||
);
|
||
}
|
||
|
||
try {
|
||
await addLog(
|
||
`自动运行${buildRoundLabel()}第 ${attemptRun} 次尝试开始前,正在校验 Hotmail 账号 ${account.email} 的邮箱可用性。`,
|
||
'info'
|
||
);
|
||
const result = await verifyHotmailAccount(account.id);
|
||
await addLog(
|
||
`自动运行${buildRoundLabel()}开始前已校验 Hotmail 账号 ${result.account?.email || account.email},INBOX 当前 ${result.messageCount} 封邮件。`,
|
||
'ok'
|
||
);
|
||
return result.account;
|
||
} catch (error) {
|
||
lastError = error;
|
||
exhaustedAccountIds.add(account.id);
|
||
preferredAccountId = null;
|
||
const latestErrorMessage = error?.message || '未知错误';
|
||
await addLog(
|
||
`自动运行${buildRoundLabel()}开始前校验 Hotmail 账号 ${account.email} 失败:${latestErrorMessage}`,
|
||
'warn'
|
||
);
|
||
const nextState = await getState();
|
||
const hasRemainingAccounts = normalizeHotmailAccounts(nextState.hotmailAccounts)
|
||
.some((candidate) => (
|
||
isAuthorizedHotmailRunAccount(candidate) || isPendingHotmailVerificationCandidate(candidate)
|
||
) && !exhaustedAccountIds.has(candidate.id));
|
||
if (hasRemainingAccounts) {
|
||
await addLog(`自动运行${buildRoundLabel()}开始前将切换下一个 Hotmail 账号并重试。`, 'warn');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
async function testHotmailAccountMailAccess(accountId) {
|
||
const state = await getState();
|
||
const account = findHotmailAccount(state.hotmailAccounts, accountId);
|
||
if (!account) {
|
||
throw new Error('未找到需要测试的 Hotmail 账号。');
|
||
}
|
||
|
||
const result = await fetchHotmailMailboxMessages(account, HOTMAIL_MAILBOXES);
|
||
const latestMessage = getLatestHotmailMessage(result.messages);
|
||
const latestCode = latestMessage ? extractVerificationCodeFromMessage(latestMessage) : null;
|
||
|
||
return {
|
||
account: result.account,
|
||
accountId: result.account.id,
|
||
email: result.account.email,
|
||
messageCount: result.messages.length,
|
||
latestSubject: latestMessage?.subject || '',
|
||
latestMailbox: latestMessage?.mailbox || '',
|
||
latestCode: latestCode || '',
|
||
inboxCount: result.mailboxResults.find((item) => item.mailbox === 'INBOX')?.count || 0,
|
||
junkCount: result.mailboxResults.find((item) => item.mailbox === 'Junk')?.count || 0,
|
||
};
|
||
}
|
||
|
||
async function pollHotmailVerificationCode(step, state, pollPayload = {}) {
|
||
await addLog(`步骤 ${step}:正在确定 Hotmail 收信账号...`, 'info');
|
||
let account = await ensureHotmailAccountForFlow({
|
||
allowAllocate: true,
|
||
markUsed: false,
|
||
preferredAccountId: state.currentHotmailAccountId || null,
|
||
});
|
||
await addLog(`步骤 ${step}:当前使用 Hotmail 账号 ${account.email} 轮询收件箱。`, 'info');
|
||
|
||
const serviceSettings = getHotmailServiceSettings(state);
|
||
if (serviceSettings.mode === HOTMAIL_SERVICE_MODE_LOCAL) {
|
||
return pollHotmailVerificationCodeViaLocalHelper(step, account, pollPayload);
|
||
}
|
||
|
||
const maxAttempts = Number(pollPayload.maxAttempts) || 5;
|
||
const intervalMs = Number(pollPayload.intervalMs) || 3000;
|
||
let lastError = null;
|
||
|
||
function summarizeMessagesForLog(messages) {
|
||
return (messages || [])
|
||
.slice()
|
||
.sort((left, right) => {
|
||
const leftTime = Date.parse(left.receivedDateTime || '') || 0;
|
||
const rightTime = Date.parse(right.receivedDateTime || '') || 0;
|
||
return rightTime - leftTime;
|
||
})
|
||
.slice(0, 3)
|
||
.map((message) => {
|
||
const receivedAt = message?.receivedDateTime || '未知时间';
|
||
const sender = message?.from?.emailAddress?.address || '未知发件人';
|
||
const subject = message?.subject || '(无主题)';
|
||
const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80);
|
||
return `[${message.mailbox || 'INBOX'}] ${receivedAt} | ${sender} | ${subject} | ${preview}`;
|
||
})
|
||
.join(' || ');
|
||
}
|
||
|
||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||
throwIfStopped();
|
||
try {
|
||
await addLog(`步骤 ${step}:正在通过 API对接 轮询 Hotmail 邮件(${attempt}/${maxAttempts})...`, 'info');
|
||
const fetchResult = await fetchHotmailMailboxMessages(account, HOTMAIL_MAILBOXES);
|
||
account = fetchResult.account;
|
||
const matchResult = pickVerificationMessageWithTimeFallback(fetchResult.messages, {
|
||
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
|
||
senderFilters: pollPayload.senderFilters || [],
|
||
subjectFilters: pollPayload.subjectFilters || [],
|
||
excludeCodes: pollPayload.excludeCodes || [],
|
||
});
|
||
const match = matchResult.match;
|
||
|
||
if (match?.code) {
|
||
const mailboxLabel = match.message?.mailbox || 'INBOX';
|
||
if (matchResult.usedRelaxedFilters) {
|
||
const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配';
|
||
await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Hotmail ${mailboxLabel} 验证码。`, 'warn');
|
||
}
|
||
await addLog(`步骤 ${step}:已通过 API对接 在 Hotmail ${mailboxLabel} 中找到验证码:${match.code}`, 'ok');
|
||
return {
|
||
ok: true,
|
||
code: match.code,
|
||
emailTimestamp: match.receivedAt || Date.now(),
|
||
mailId: match.message?.id || '',
|
||
};
|
||
}
|
||
|
||
lastError = new Error(`步骤 ${step}:暂未在 Hotmail 收件箱中找到匹配验证码(${attempt}/${maxAttempts})。`);
|
||
await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info');
|
||
const mailSummary = summarizeMessagesForLog(fetchResult.messages);
|
||
if (mailSummary) {
|
||
await addLog(`步骤 ${step}:最近邮件样本:${mailSummary}`, 'info');
|
||
}
|
||
} catch (err) {
|
||
lastError = err;
|
||
await addLog(`步骤 ${step}:Hotmail API 对接轮询失败:${err.message}`, 'warn');
|
||
}
|
||
|
||
if (attempt < maxAttempts) {
|
||
await sleepWithStop(intervalMs);
|
||
}
|
||
}
|
||
|
||
throw lastError || new Error(`步骤 ${step}:未在 Hotmail 收件箱中找到新的匹配验证码。`);
|
||
}
|
||
|
||
function generateRandomSuffix(length = 6) {
|
||
const chars = 'abcdefghjkmnpqrstuvwxyz23456789';
|
||
let suffix = '';
|
||
for (let i = 0; i < length; i++) {
|
||
suffix += chars[Math.floor(Math.random() * chars.length)];
|
||
}
|
||
return suffix;
|
||
}
|
||
|
||
const GMAIL_ALIAS_WORDS = [
|
||
'amber', 'apple', 'ash', 'berry', 'birch', 'blue', 'brook', 'cedar',
|
||
'cloud', 'clover', 'coast', 'cocoa', 'coral', 'dawn', 'delta', 'echo',
|
||
'ember', 'field', 'flint', 'flora', 'forest', 'frost', 'glade', 'harbor',
|
||
'hazel', 'honey', 'ivory', 'jade', 'lake', 'leaf', 'light', 'lilac',
|
||
'lotus', 'lunar', 'maple', 'meadow', 'mist', 'moon', 'nova', 'oasis',
|
||
'olive', 'opal', 'pearl', 'pine', 'pixel', 'plum', 'quartz', 'rain',
|
||
'raven', 'river', 'rose', 'sage', 'shore', 'sky', 'solar', 'spark',
|
||
'stone', 'storm', 'sun', 'terra', 'vale', 'wave', 'willow', 'zephyr',
|
||
];
|
||
|
||
function generateRandomWordAliasTag(parts = 3) {
|
||
const selected = [];
|
||
for (let i = 0; i < parts; i++) {
|
||
selected.push(GMAIL_ALIAS_WORDS[Math.floor(Math.random() * GMAIL_ALIAS_WORDS.length)]);
|
||
}
|
||
return selected.join('');
|
||
}
|
||
|
||
function parseGmailBaseEmail(rawValue) {
|
||
const value = String(rawValue || '').trim().toLowerCase();
|
||
const match = value.match(/^([^@\s+]+)@((?:gmail|googlemail)\.com)$/i);
|
||
if (!match) return null;
|
||
return {
|
||
localPart: match[1],
|
||
domain: match[2].toLowerCase(),
|
||
};
|
||
}
|
||
|
||
function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) {
|
||
if (
|
||
stateOrProvider
|
||
&& typeof stateOrProvider === 'object'
|
||
&& !Array.isArray(stateOrProvider)
|
||
&& normalizeEmailGenerator(stateOrProvider.emailGenerator) === (
|
||
typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
|
||
? CUSTOM_EMAIL_POOL_GENERATOR
|
||
: 'custom-pool'
|
||
)
|
||
) {
|
||
return false;
|
||
}
|
||
const provider = typeof stateOrProvider === 'string'
|
||
? stateOrProvider
|
||
: stateOrProvider?.mailProvider;
|
||
const resolvedMail2925Mode = mail2925Mode !== undefined
|
||
? normalizeMail2925Mode(mail2925Mode)
|
||
: getMail2925Mode(stateOrProvider);
|
||
const utils = (typeof self !== 'undefined' ? self : globalThis).MultiPageManagedAliasUtils || null;
|
||
if (utils?.usesManagedAliasGeneration) {
|
||
return utils.usesManagedAliasGeneration(provider, { mail2925Mode: resolvedMail2925Mode });
|
||
}
|
||
if (utils?.isManagedAliasProvider) {
|
||
if (String(provider || '').trim().toLowerCase() === '2925') {
|
||
return utils.isManagedAliasProvider(provider) && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE;
|
||
}
|
||
return utils.isManagedAliasProvider(provider);
|
||
}
|
||
return provider === GMAIL_PROVIDER
|
||
|| (provider === '2925' && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE);
|
||
}
|
||
|
||
function shouldUseCustomRegistrationEmail(state = {}) {
|
||
return isCustomMailProvider(state)
|
||
|| (!isHotmailProvider(state)
|
||
&& !isGeneratedAliasProvider(state)
|
||
&& normalizeEmailGenerator(state.emailGenerator) === 'custom');
|
||
}
|
||
|
||
function buildGeneratedAliasEmail(state) {
|
||
const provider = state.mailProvider || '163';
|
||
const emailPrefix = (state.emailPrefix || '').trim();
|
||
|
||
if (provider === GMAIL_PROVIDER) {
|
||
if (!emailPrefix) {
|
||
throw new Error('Gmail 原邮箱未设置,请先在侧边栏填写。');
|
||
}
|
||
const parsed = parseGmailBaseEmail(emailPrefix);
|
||
if (!parsed) {
|
||
throw new Error('Gmail 原邮箱格式不正确,请填写类似 name@gmail.com 的地址。');
|
||
}
|
||
return `${parsed.localPart}+${generateRandomWordAliasTag()}@${parsed.domain}`;
|
||
}
|
||
|
||
if (!emailPrefix) {
|
||
throw new Error('2925 邮箱前缀未设置,请先在侧边栏填写。');
|
||
}
|
||
|
||
if (provider === '2925' && isGeneratedAliasProvider(state)) {
|
||
return `${emailPrefix}${generateRandomSuffix(6)}@2925.com`;
|
||
}
|
||
|
||
throw new Error(`未支持的别名邮箱类型:${provider}`);
|
||
}
|
||
|
||
function getManagedAliasUtils() {
|
||
return (typeof self !== 'undefined' ? self : globalThis).MultiPageManagedAliasUtils || null;
|
||
}
|
||
|
||
function parseGmailBaseEmail(rawValue) {
|
||
const utils = getManagedAliasUtils();
|
||
if (utils?.parseManagedAliasBaseEmail) {
|
||
return utils.parseManagedAliasBaseEmail(rawValue, GMAIL_PROVIDER);
|
||
}
|
||
|
||
const value = String(rawValue || '').trim().toLowerCase();
|
||
const match = value.match(/^([^@\s+]+)@((?:gmail|googlemail)\.com)$/i);
|
||
if (!match) return null;
|
||
return {
|
||
localPart: match[1],
|
||
domain: match[2].toLowerCase(),
|
||
};
|
||
}
|
||
|
||
function parseManagedAliasBaseEmail(rawValue, provider) {
|
||
const utils = getManagedAliasUtils();
|
||
if (utils?.parseManagedAliasBaseEmail) {
|
||
return utils.parseManagedAliasBaseEmail(rawValue, provider);
|
||
}
|
||
|
||
if (provider === GMAIL_PROVIDER) {
|
||
return parseGmailBaseEmail(rawValue);
|
||
}
|
||
|
||
const value = String(rawValue || '').trim().toLowerCase();
|
||
const match = value.match(/^([^@\s+]+)@(2925\.com)$/i);
|
||
if (!match) return null;
|
||
return {
|
||
localPart: match[1],
|
||
domain: match[2].toLowerCase(),
|
||
};
|
||
}
|
||
|
||
function isManagedAliasEmail(value, provider, baseEmail = '') {
|
||
const utils = getManagedAliasUtils();
|
||
if (utils?.isManagedAliasEmail) {
|
||
return utils.isManagedAliasEmail(value, provider, baseEmail);
|
||
}
|
||
|
||
const normalizedValue = String(value || '').trim().toLowerCase();
|
||
if (!normalizedValue) return false;
|
||
const parsedEmail = normalizedValue.match(/^([^@\s]+)@([^@\s]+\.[^@\s]+)$/);
|
||
if (!parsedEmail) return false;
|
||
|
||
const candidateLocalPart = parsedEmail[1];
|
||
const candidateDomain = parsedEmail[2];
|
||
if (provider === GMAIL_PROVIDER) {
|
||
if (!/^(?:gmail|googlemail)\.com$/i.test(candidateDomain)) {
|
||
return false;
|
||
}
|
||
const parsedBaseEmail = parseManagedAliasBaseEmail(baseEmail, provider);
|
||
if (!parsedBaseEmail) {
|
||
return true;
|
||
}
|
||
return candidateDomain === parsedBaseEmail.domain
|
||
&& candidateLocalPart.split('+')[0] === parsedBaseEmail.localPart;
|
||
}
|
||
|
||
if (provider !== '2925' || candidateDomain !== '2925.com') {
|
||
return false;
|
||
}
|
||
|
||
const parsedBaseEmail = parseManagedAliasBaseEmail(baseEmail, provider);
|
||
if (!parsedBaseEmail) {
|
||
return true;
|
||
}
|
||
|
||
return candidateLocalPart === parsedBaseEmail.localPart || candidateLocalPart.startsWith(parsedBaseEmail.localPart);
|
||
}
|
||
|
||
function getManagedAliasBaseEmail(state = {}, provider = state?.mailProvider) {
|
||
const normalizedProvider = String(provider || '').trim().toLowerCase();
|
||
const legacyEmailPrefix = String(state?.emailPrefix || '').trim();
|
||
if (normalizedProvider === GMAIL_PROVIDER) {
|
||
const gmailBaseEmail = String(state?.gmailBaseEmail || '').trim();
|
||
if (gmailBaseEmail) {
|
||
return gmailBaseEmail;
|
||
}
|
||
return parseManagedAliasBaseEmail(legacyEmailPrefix, normalizedProvider) ? legacyEmailPrefix : '';
|
||
}
|
||
|
||
if (normalizedProvider === '2925') {
|
||
const currentAccount = Boolean(state?.mail2925UseAccountPool)
|
||
? getCurrentMail2925Account(state)
|
||
: null;
|
||
if (currentAccount?.email) {
|
||
return currentAccount.email;
|
||
}
|
||
const mail2925BaseEmail = String(state?.mail2925BaseEmail || '').trim();
|
||
if (mail2925BaseEmail) {
|
||
return mail2925BaseEmail;
|
||
}
|
||
return parseManagedAliasBaseEmail(legacyEmailPrefix, normalizedProvider) ? legacyEmailPrefix : '';
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) {
|
||
if (
|
||
stateOrProvider
|
||
&& typeof stateOrProvider === 'object'
|
||
&& !Array.isArray(stateOrProvider)
|
||
&& normalizeEmailGenerator(stateOrProvider.emailGenerator) === (
|
||
typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
|
||
? CUSTOM_EMAIL_POOL_GENERATOR
|
||
: 'custom-pool'
|
||
)
|
||
) {
|
||
return false;
|
||
}
|
||
const provider = typeof stateOrProvider === 'string'
|
||
? stateOrProvider
|
||
: stateOrProvider?.mailProvider;
|
||
const resolvedMail2925Mode = mail2925Mode !== undefined
|
||
? normalizeMail2925Mode(mail2925Mode)
|
||
: getMail2925Mode(stateOrProvider);
|
||
const utils = getManagedAliasUtils();
|
||
if (utils?.usesManagedAliasGeneration) {
|
||
return utils.usesManagedAliasGeneration(provider, { mail2925Mode: resolvedMail2925Mode });
|
||
}
|
||
if (utils?.isManagedAliasProvider) {
|
||
if (String(provider || '').trim().toLowerCase() === '2925') {
|
||
return utils.isManagedAliasProvider(provider) && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE;
|
||
}
|
||
return utils.isManagedAliasProvider(provider);
|
||
}
|
||
return provider === GMAIL_PROVIDER
|
||
|| (provider === '2925' && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE);
|
||
}
|
||
|
||
function shouldUseCustomRegistrationEmail(state = {}) {
|
||
return isCustomMailProvider(state)
|
||
|| (!isHotmailProvider(state)
|
||
&& !isGeneratedAliasProvider(state)
|
||
&& normalizeEmailGenerator(state.emailGenerator) === 'custom');
|
||
}
|
||
|
||
function isReusableGeneratedAliasEmail(state = {}, email = state?.email) {
|
||
if (!isGeneratedAliasProvider(state)) {
|
||
return false;
|
||
}
|
||
|
||
return isManagedAliasEmail(email, state?.mailProvider, getManagedAliasBaseEmail(state));
|
||
}
|
||
|
||
function buildGeneratedAliasEmail(state) {
|
||
const provider = state.mailProvider || '163';
|
||
const baseEmail = getManagedAliasBaseEmail(state, provider);
|
||
const baseLabel = provider === GMAIL_PROVIDER ? 'Gmail 原邮箱' : '2925 基邮箱';
|
||
const exampleEmail = provider === GMAIL_PROVIDER ? 'name@gmail.com' : 'name@2925.com';
|
||
|
||
if (!baseEmail) {
|
||
throw new Error(`${baseLabel}未设置,请先在侧边栏填写,或直接在“注册邮箱”中手动填写完整邮箱。`);
|
||
}
|
||
|
||
if (!parseManagedAliasBaseEmail(baseEmail, provider)) {
|
||
throw new Error(`${baseLabel}格式不正确,请填写类似 ${exampleEmail} 的地址。`);
|
||
}
|
||
|
||
const utils = getManagedAliasUtils();
|
||
if (utils?.buildManagedAliasEmail) {
|
||
return utils.buildManagedAliasEmail(
|
||
provider,
|
||
baseEmail,
|
||
provider === GMAIL_PROVIDER ? generateRandomWordAliasTag() : generateRandomSuffix(6)
|
||
);
|
||
}
|
||
|
||
const parsedBaseEmail = parseManagedAliasBaseEmail(baseEmail, provider);
|
||
if (provider === GMAIL_PROVIDER) {
|
||
return `${parsedBaseEmail.localPart}+${generateRandomWordAliasTag()}@${parsedBaseEmail.domain}`;
|
||
}
|
||
if (provider === '2925') {
|
||
return `${parsedBaseEmail.localPart}${generateRandomSuffix(6)}@${parsedBaseEmail.domain}`;
|
||
}
|
||
|
||
throw new Error(`未支持的别名邮箱类型:${provider}`);
|
||
}
|
||
|
||
function getLuckmailSessionConfig(state = {}) {
|
||
return {
|
||
apiKey: String(state.luckmailApiKey || ''),
|
||
baseUrl: normalizeLuckmailBaseUrl(state.luckmailBaseUrl),
|
||
emailType: normalizeLuckmailEmailType(state.luckmailEmailType),
|
||
domain: String(state.luckmailDomain || '').trim(),
|
||
};
|
||
}
|
||
|
||
function ensureLuckmailApiKey(state = {}) {
|
||
const apiKey = String(state.luckmailApiKey || '').trim();
|
||
if (!apiKey) {
|
||
throw new Error('LuckMail API Key 为空,请先在侧边栏填写。');
|
||
}
|
||
return apiKey;
|
||
}
|
||
|
||
async function requestLuckmail(method, path, { baseUrl, apiKey, params, jsonData, timeout = 30000 } = {}) {
|
||
const requestUrl = new URL(`${normalizeLuckmailBaseUrl(baseUrl)}${path}`);
|
||
if (params && typeof params === 'object') {
|
||
for (const [key, value] of Object.entries(params)) {
|
||
if (value === undefined || value === null || value === '') continue;
|
||
requestUrl.searchParams.set(key, String(value));
|
||
}
|
||
}
|
||
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||
const headers = {
|
||
Accept: 'application/json',
|
||
};
|
||
if (apiKey) {
|
||
headers['X-API-Key'] = apiKey;
|
||
}
|
||
|
||
const upperMethod = String(method || 'GET').toUpperCase();
|
||
const fetchOptions = {
|
||
method: upperMethod,
|
||
headers,
|
||
signal: controller.signal,
|
||
};
|
||
if (jsonData !== undefined) {
|
||
headers['Content-Type'] = 'application/json';
|
||
fetchOptions.body = JSON.stringify(jsonData || {});
|
||
}
|
||
|
||
let response = null;
|
||
try {
|
||
response = await fetch(requestUrl.toString(), fetchOptions);
|
||
} catch (err) {
|
||
if (err?.name === 'AbortError') {
|
||
throw new Error(`LuckMail 请求超时:${path}`);
|
||
}
|
||
throw new Error(`LuckMail 请求失败:${err.message}`);
|
||
} finally {
|
||
clearTimeout(timeoutId);
|
||
}
|
||
|
||
let payload = null;
|
||
try {
|
||
payload = await response.json();
|
||
} catch {
|
||
throw new Error(`LuckMail 返回了无法解析的响应:${path}`);
|
||
}
|
||
|
||
if (!response.ok) {
|
||
const errorText = String(payload?.message || response.statusText || 'HTTP error');
|
||
throw new Error(`LuckMail 请求失败:${errorText}`);
|
||
}
|
||
|
||
if (!payload || typeof payload !== 'object') {
|
||
throw new Error(`LuckMail 返回数据无效:${path}`);
|
||
}
|
||
|
||
if (payload.code !== 0) {
|
||
const errorText = String(payload.message || 'Unknown error');
|
||
throw new Error(`LuckMail 接口返回失败:${errorText}`);
|
||
}
|
||
|
||
return payload.data;
|
||
}
|
||
|
||
function createLuckmailClient(state = {}) {
|
||
const config = getLuckmailSessionConfig(state);
|
||
const apiKey = ensureLuckmailApiKey(state);
|
||
const request = (method, path, options = {}) => requestLuckmail(method, path, {
|
||
baseUrl: config.baseUrl,
|
||
apiKey,
|
||
...options,
|
||
});
|
||
|
||
return {
|
||
user: {
|
||
async purchaseEmails(projectCode, quantity, { emailType, domain } = {}) {
|
||
const body = {
|
||
project_code: projectCode,
|
||
quantity,
|
||
email_type: normalizeLuckmailEmailType(emailType),
|
||
};
|
||
if (domain) {
|
||
body.domain = String(domain).trim();
|
||
}
|
||
return request('POST', '/api/v1/openapi/email/purchase', {
|
||
jsonData: body,
|
||
});
|
||
},
|
||
async getPurchases({ page = 1, pageSize = 100, projectId, tagId, keyword, userDisabled } = {}) {
|
||
return normalizeLuckmailPurchaseListPage(await request('GET', '/api/v1/openapi/email/purchases', {
|
||
params: {
|
||
page,
|
||
page_size: pageSize,
|
||
project_id: projectId,
|
||
tag_id: tagId,
|
||
keyword,
|
||
user_disabled: userDisabled,
|
||
},
|
||
}));
|
||
},
|
||
async getTokenCode(token) {
|
||
return normalizeLuckmailTokenCode(await request(
|
||
'GET',
|
||
`/api/v1/openapi/email/token/${encodeURIComponent(token)}/code`
|
||
));
|
||
},
|
||
async checkTokenAlive(token) {
|
||
const data = await request(
|
||
'GET',
|
||
`/api/v1/openapi/email/token/${encodeURIComponent(token)}/alive`
|
||
);
|
||
return {
|
||
email_address: String(data?.email_address || ''),
|
||
project: String(data?.project || ''),
|
||
alive: Boolean(data?.alive),
|
||
status: String(data?.status || ''),
|
||
message: String(data?.message || ''),
|
||
mail_count: Number(data?.mail_count) || 0,
|
||
};
|
||
},
|
||
async getTokenMails(token) {
|
||
const data = await request('GET', `/api/v1/openapi/email/token/${encodeURIComponent(token)}/mails`);
|
||
return {
|
||
email_address: String(data?.email_address || ''),
|
||
project: String(data?.project || ''),
|
||
warranty_until: String(data?.warranty_until || ''),
|
||
mails: normalizeLuckmailTokenMails(data?.mails || []),
|
||
};
|
||
},
|
||
async getTokenMailDetail(token, messageId) {
|
||
return normalizeLuckmailTokenMail(await request(
|
||
'GET',
|
||
`/api/v1/openapi/email/token/${encodeURIComponent(token)}/mails/${encodeURIComponent(messageId)}`
|
||
));
|
||
},
|
||
async setPurchaseDisabled(purchaseId, disabled) {
|
||
await request('PUT', `/api/v1/openapi/email/purchases/${encodeURIComponent(purchaseId)}/disabled`, {
|
||
jsonData: {
|
||
disabled: disabled ? 1 : 0,
|
||
},
|
||
});
|
||
},
|
||
async batchSetPurchaseDisabled(ids, disabled) {
|
||
await request('POST', '/api/v1/openapi/email/purchases/batch-disabled', {
|
||
jsonData: {
|
||
ids: (Array.isArray(ids) ? ids : []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0),
|
||
disabled: disabled ? 1 : 0,
|
||
},
|
||
});
|
||
},
|
||
async setPurchaseTag(purchaseId, { tagId, tagName } = {}) {
|
||
const body = {};
|
||
if (tagId !== undefined) {
|
||
body.tag_id = Number(tagId) || 0;
|
||
}
|
||
if (tagName !== undefined) {
|
||
body.tag_name = String(tagName || '').trim();
|
||
}
|
||
await request('PUT', `/api/v1/openapi/email/purchases/${encodeURIComponent(purchaseId)}/tag`, {
|
||
jsonData: body,
|
||
});
|
||
},
|
||
async batchSetPurchaseTag(ids, { tagId, tagName } = {}) {
|
||
const body = {
|
||
ids: (Array.isArray(ids) ? ids : []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0),
|
||
};
|
||
if (tagId !== undefined) {
|
||
body.tag_id = Number(tagId) || 0;
|
||
}
|
||
if (tagName !== undefined) {
|
||
body.tag_name = String(tagName || '').trim();
|
||
}
|
||
await request('POST', '/api/v1/openapi/email/purchases/batch-tag', {
|
||
jsonData: body,
|
||
});
|
||
},
|
||
async getTags() {
|
||
return normalizeLuckmailTags(await request('GET', '/api/v1/openapi/email/tags'));
|
||
},
|
||
async createTag(name, limitType, remark) {
|
||
const body = {
|
||
name: String(name || '').trim(),
|
||
limit_type: Number(limitType) || 0,
|
||
};
|
||
if (remark !== undefined) {
|
||
body.remark = String(remark || '').trim();
|
||
}
|
||
return normalizeLuckmailTags([await request('POST', '/api/v1/openapi/email/tags', {
|
||
jsonData: body,
|
||
})])[0] || null;
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
function getCurrentLuckmailPurchase(state = {}) {
|
||
return state.currentLuckmailPurchase
|
||
? normalizeLuckmailPurchase(state.currentLuckmailPurchase)
|
||
: null;
|
||
}
|
||
|
||
function buildLuckmailPurchaseView(purchase, state = {}) {
|
||
const normalizedPurchase = normalizeLuckmailPurchase(purchase);
|
||
const usedPurchases = getLuckmailUsedPurchases(state);
|
||
const preserveTagInfo = getLuckmailPreserveTagInfo(state);
|
||
|
||
return {
|
||
id: normalizedPurchase.id,
|
||
email_address: normalizedPurchase.email_address,
|
||
project_name: normalizeLuckmailProjectName(normalizedPurchase.project_name) || DEFAULT_LUCKMAIL_PROJECT_CODE,
|
||
price: normalizedPurchase.price,
|
||
status: normalizedPurchase.status,
|
||
tag_id: normalizedPurchase.tag_id,
|
||
tag_name: normalizedPurchase.tag_name,
|
||
user_disabled: normalizedPurchase.user_disabled,
|
||
warranty_hours: normalizedPurchase.warranty_hours,
|
||
warranty_until: normalizedPurchase.warranty_until,
|
||
created_at: normalizedPurchase.created_at,
|
||
used: Boolean(usedPurchases[normalizeLuckmailPurchaseId(normalizedPurchase.id)]),
|
||
preserved: isLuckmailPurchasePreserved(normalizedPurchase, {
|
||
preserveTagId: preserveTagInfo.id,
|
||
preserveTagName: preserveTagInfo.name,
|
||
}),
|
||
disabled: normalizedPurchase.user_disabled === 1,
|
||
current: Number(getCurrentLuckmailPurchase(state)?.id) === normalizedPurchase.id,
|
||
reusable: isLuckmailPurchaseReusable(normalizedPurchase, {
|
||
projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE,
|
||
usedPurchases,
|
||
preserveTagId: preserveTagInfo.id,
|
||
preserveTagName: preserveTagInfo.name,
|
||
now: Date.now(),
|
||
}),
|
||
};
|
||
}
|
||
|
||
async function getAllLuckmailPurchases(state, options = {}) {
|
||
const client = options.client || createLuckmailClient(state);
|
||
const pageSize = Math.max(1, Math.min(100, Number(options.pageSize) || 100));
|
||
const maxPages = Math.max(1, Number(options.maxPages) || 50);
|
||
const purchases = [];
|
||
|
||
for (let page = 1; page <= maxPages; page += 1) {
|
||
const pageResult = await client.user.getPurchases({
|
||
page,
|
||
pageSize,
|
||
keyword: options.keyword,
|
||
projectId: options.projectId,
|
||
tagId: options.tagId,
|
||
userDisabled: options.userDisabled,
|
||
});
|
||
const normalizedPage = normalizeLuckmailPurchaseListPage(pageResult);
|
||
purchases.push(...normalizedPage.list);
|
||
|
||
if (normalizedPage.list.length === 0) {
|
||
break;
|
||
}
|
||
if (normalizedPage.total > 0 && purchases.length >= normalizedPage.total) {
|
||
break;
|
||
}
|
||
if (normalizedPage.list.length < normalizedPage.page_size) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
return purchases;
|
||
}
|
||
|
||
async function listLuckmailPurchasesByProject(state, options = {}) {
|
||
const projectCode = normalizeLuckmailProjectName(options.projectCode || DEFAULT_LUCKMAIL_PROJECT_CODE)
|
||
|| DEFAULT_LUCKMAIL_PROJECT_CODE;
|
||
const purchases = await getAllLuckmailPurchases(state, options);
|
||
return purchases.filter((purchase) => isLuckmailPurchaseForProject(purchase, projectCode));
|
||
}
|
||
|
||
async function getLuckmailPurchaseById(state, purchaseId, options = {}) {
|
||
const normalizedPurchaseId = Number(normalizeLuckmailPurchaseId(purchaseId)) || 0;
|
||
if (!normalizedPurchaseId) {
|
||
throw new Error('LuckMail 邮箱 ID 无效。');
|
||
}
|
||
|
||
const purchases = await listLuckmailPurchasesByProject(state, options);
|
||
const purchase = purchases.find((item) => item.id === normalizedPurchaseId) || null;
|
||
if (!purchase) {
|
||
throw new Error(`未找到 ID=${normalizedPurchaseId} 的 openai LuckMail 邮箱。`);
|
||
}
|
||
return purchase;
|
||
}
|
||
|
||
async function listLuckmailPurchasesForManagement() {
|
||
const state = await getState();
|
||
const purchases = await listLuckmailPurchasesByProject(state, {
|
||
projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE,
|
||
});
|
||
return purchases.map((purchase) => buildLuckmailPurchaseView(purchase, state));
|
||
}
|
||
|
||
async function ensureLuckmailPreserveTag(client, state = null) {
|
||
const resolvedState = state || await getState();
|
||
const preserveTagInfo = getLuckmailPreserveTagInfo(resolvedState);
|
||
if (preserveTagInfo.id > 0) {
|
||
return preserveTagInfo;
|
||
}
|
||
|
||
const tags = normalizeLuckmailTags(await client.user.getTags());
|
||
let preserveTag = tags.find(
|
||
(tag) => normalizeLuckmailProjectName(tag.name) === normalizeLuckmailProjectName(preserveTagInfo.name)
|
||
) || null;
|
||
|
||
if (!preserveTag) {
|
||
preserveTag = await client.user.createTag(
|
||
DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
|
||
0,
|
||
'保留邮箱(不参与自动复用)'
|
||
);
|
||
}
|
||
|
||
await setLuckmailPreserveTagInfo(preserveTag);
|
||
return {
|
||
id: Number(preserveTag?.id) || 0,
|
||
name: String(preserveTag?.name || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
|
||
};
|
||
}
|
||
|
||
async function activateLuckmailPurchaseForFlow(state, client, purchase, options = {}) {
|
||
const normalizedPurchase = normalizeLuckmailPurchase(purchase);
|
||
if (!normalizedPurchase?.email_address || !normalizedPurchase?.token) {
|
||
throw new Error('LuckMail 邮箱缺少 email/token,无法用于当前流程。');
|
||
}
|
||
|
||
let baselineCursor = null;
|
||
if (options.initializeCursor !== false) {
|
||
const mailList = await client.user.getTokenMails(normalizedPurchase.token);
|
||
baselineCursor = buildLuckmailBaselineCursor(mailList?.mails || []);
|
||
}
|
||
|
||
await setLuckmailPurchaseState(normalizedPurchase);
|
||
await setLuckmailMailCursorState(baselineCursor);
|
||
await setEmailState(normalizedPurchase.email_address);
|
||
|
||
if (options.logMessage) {
|
||
await addLog(options.logMessage, options.logLevel || 'ok');
|
||
}
|
||
|
||
return normalizedPurchase;
|
||
}
|
||
|
||
async function findReusableLuckmailPurchaseForFlow(state, client) {
|
||
const preserveTagInfo = getLuckmailPreserveTagInfo(state);
|
||
const reusablePurchases = filterReusableLuckmailPurchases(
|
||
await listLuckmailPurchasesByProject(state, {
|
||
client,
|
||
projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE,
|
||
}),
|
||
{
|
||
projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE,
|
||
usedPurchases: getLuckmailUsedPurchases(state),
|
||
preserveTagId: preserveTagInfo.id,
|
||
preserveTagName: preserveTagInfo.name,
|
||
now: Date.now(),
|
||
}
|
||
);
|
||
|
||
for (const candidate of reusablePurchases) {
|
||
try {
|
||
const aliveResult = await client.user.checkTokenAlive(candidate.token);
|
||
if (!aliveResult?.alive) {
|
||
await addLog(
|
||
`LuckMail:跳过不可复用邮箱 ${candidate.email_address}:${aliveResult?.message || aliveResult?.status || 'token 不可用'}`,
|
||
'warn'
|
||
);
|
||
continue;
|
||
}
|
||
return candidate;
|
||
} catch (err) {
|
||
await addLog(`LuckMail:检测复用邮箱 ${candidate.email_address} 失败:${err.message}`, 'warn');
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
async function selectLuckmailPurchase(purchaseId) {
|
||
const state = await ensureManualInteractionAllowed('切换 LuckMail 邮箱');
|
||
const client = createLuckmailClient(state);
|
||
const purchase = await getLuckmailPurchaseById(state, purchaseId, {
|
||
client,
|
||
projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE,
|
||
});
|
||
|
||
if (purchase.user_disabled === 1) {
|
||
throw new Error(`LuckMail 邮箱 ${purchase.email_address} 已禁用,无法使用。`);
|
||
}
|
||
|
||
const aliveResult = await client.user.checkTokenAlive(purchase.token);
|
||
if (!aliveResult?.alive) {
|
||
throw new Error(`LuckMail 邮箱 ${purchase.email_address} 当前不可用:${aliveResult?.message || aliveResult?.status || 'token 已失效'}`);
|
||
}
|
||
|
||
const activatedPurchase = await activateLuckmailPurchaseForFlow(state, client, purchase, {
|
||
initializeCursor: true,
|
||
logMessage: `LuckMail:已切换当前邮箱为 ${purchase.email_address}`,
|
||
});
|
||
const nextState = await getState();
|
||
return buildLuckmailPurchaseView(activatedPurchase, nextState);
|
||
}
|
||
|
||
async function setLuckmailPurchasePreservedState(purchaseId, preserved) {
|
||
const state = await ensureManualInteractionAllowed('设置 LuckMail 邮箱保留状态');
|
||
const client = createLuckmailClient(state);
|
||
const purchase = await getLuckmailPurchaseById(state, purchaseId, {
|
||
client,
|
||
projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE,
|
||
});
|
||
|
||
if (preserved) {
|
||
const preserveTag = await ensureLuckmailPreserveTag(client, state);
|
||
await client.user.setPurchaseTag(purchase.id, { tagId: preserveTag.id });
|
||
} else {
|
||
await client.user.setPurchaseTag(purchase.id, { tagId: 0 });
|
||
}
|
||
|
||
await addLog(`LuckMail:已将 ${purchase.email_address} ${preserved ? '设为保留' : '取消保留'}`, 'ok');
|
||
const refreshedState = await getState();
|
||
const refreshedPurchase = await getLuckmailPurchaseById(refreshedState, purchase.id, {
|
||
client,
|
||
projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE,
|
||
});
|
||
return buildLuckmailPurchaseView(refreshedPurchase, await getState());
|
||
}
|
||
|
||
async function setLuckmailPurchaseDisabledState(purchaseId, disabled) {
|
||
const state = await ensureManualInteractionAllowed(disabled ? '禁用 LuckMail 邮箱' : '启用 LuckMail 邮箱');
|
||
const client = createLuckmailClient(state);
|
||
const purchase = await getLuckmailPurchaseById(state, purchaseId, {
|
||
client,
|
||
projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE,
|
||
});
|
||
|
||
await client.user.setPurchaseDisabled(purchase.id, disabled ? 1 : 0);
|
||
|
||
const currentPurchase = getCurrentLuckmailPurchase(await getState());
|
||
if (disabled && currentPurchase?.id === purchase.id) {
|
||
await clearLuckmailRuntimeState({ clearEmail: isLuckmailProvider(await getState()) });
|
||
}
|
||
|
||
await addLog(`LuckMail:已将 ${purchase.email_address} ${disabled ? '禁用' : '启用'}`, 'ok');
|
||
const refreshedState = await getState();
|
||
const refreshedPurchase = await getLuckmailPurchaseById(refreshedState, purchase.id, {
|
||
client,
|
||
projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE,
|
||
});
|
||
return buildLuckmailPurchaseView(refreshedPurchase, await getState());
|
||
}
|
||
|
||
async function batchUpdateLuckmailPurchases(input = {}) {
|
||
const action = String(input.action || '').trim();
|
||
const selectedIds = Array.isArray(input.ids)
|
||
? [...new Set(input.ids.map((id) => Number(normalizeLuckmailPurchaseId(id)) || 0).filter((id) => id > 0))]
|
||
: [];
|
||
if (!selectedIds.length) {
|
||
throw new Error('请先选择至少一个 LuckMail 邮箱。');
|
||
}
|
||
|
||
const state = await ensureManualInteractionAllowed('批量更新 LuckMail 邮箱');
|
||
const client = createLuckmailClient(state);
|
||
const purchases = await listLuckmailPurchasesByProject(state, {
|
||
client,
|
||
projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE,
|
||
});
|
||
const purchaseMap = new Map(purchases.map((purchase) => [purchase.id, purchase]));
|
||
const targetPurchases = selectedIds.map((id) => purchaseMap.get(id)).filter(Boolean);
|
||
|
||
if (!targetPurchases.length) {
|
||
throw new Error('未找到可批量处理的 openai LuckMail 邮箱。');
|
||
}
|
||
|
||
const targetIds = targetPurchases.map((purchase) => purchase.id);
|
||
|
||
if (action === 'used' || action === 'unused') {
|
||
const nextUsedState = getLuckmailUsedPurchases(state);
|
||
targetIds.forEach((id) => {
|
||
const key = normalizeLuckmailPurchaseId(id);
|
||
if (!key) return;
|
||
if (action === 'used') {
|
||
nextUsedState[key] = true;
|
||
} else {
|
||
delete nextUsedState[key];
|
||
}
|
||
});
|
||
await setLuckmailUsedPurchasesState(nextUsedState);
|
||
await addLog(`LuckMail:已批量${action === 'used' ? '标记已用' : '标记未用'} ${targetIds.length} 个邮箱`, 'ok');
|
||
} else if (action === 'preserve' || action === 'unpreserve') {
|
||
if (action === 'preserve') {
|
||
const preserveTag = await ensureLuckmailPreserveTag(client, state);
|
||
await client.user.batchSetPurchaseTag(targetIds, { tagId: preserveTag.id });
|
||
} else {
|
||
await client.user.batchSetPurchaseTag(targetIds, { tagId: 0 });
|
||
}
|
||
await addLog(`LuckMail:已批量${action === 'preserve' ? '保留' : '取消保留'} ${targetIds.length} 个邮箱`, 'ok');
|
||
} else if (action === 'disable' || action === 'enable') {
|
||
await client.user.batchSetPurchaseDisabled(targetIds, action === 'disable' ? 1 : 0);
|
||
const currentPurchase = getCurrentLuckmailPurchase(await getState());
|
||
if (action === 'disable' && currentPurchase?.id && targetIds.includes(currentPurchase.id)) {
|
||
await clearLuckmailRuntimeState({ clearEmail: isLuckmailProvider(await getState()) });
|
||
}
|
||
await addLog(`LuckMail:已批量${action === 'disable' ? '禁用' : '启用'} ${targetIds.length} 个邮箱`, 'ok');
|
||
} else {
|
||
throw new Error(`不支持的 LuckMail 批量操作:${action}`);
|
||
}
|
||
|
||
return {
|
||
updatedIds: targetIds,
|
||
};
|
||
}
|
||
|
||
async function disableUsedLuckmailPurchases() {
|
||
const state = await ensureManualInteractionAllowed('禁用已用 LuckMail 邮箱');
|
||
const usedPurchases = getLuckmailUsedPurchases(state);
|
||
const preserveTagInfo = getLuckmailPreserveTagInfo(state);
|
||
const client = createLuckmailClient(state);
|
||
const purchases = await listLuckmailPurchasesByProject(state, {
|
||
client,
|
||
projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE,
|
||
});
|
||
const targets = purchases.filter((purchase) => {
|
||
const purchaseId = normalizeLuckmailPurchaseId(purchase.id);
|
||
return Boolean(purchaseId && usedPurchases[purchaseId])
|
||
&& !isLuckmailPurchasePreserved(purchase, {
|
||
preserveTagId: preserveTagInfo.id,
|
||
preserveTagName: preserveTagInfo.name,
|
||
})
|
||
&& purchase.user_disabled !== 1;
|
||
});
|
||
|
||
if (!targets.length) {
|
||
return { disabledIds: [] };
|
||
}
|
||
|
||
const targetIds = targets.map((purchase) => purchase.id);
|
||
await client.user.batchSetPurchaseDisabled(targetIds, 1);
|
||
const currentPurchase = getCurrentLuckmailPurchase(await getState());
|
||
if (currentPurchase?.id && targetIds.includes(currentPurchase.id)) {
|
||
await clearLuckmailRuntimeState({ clearEmail: isLuckmailProvider(await getState()) });
|
||
}
|
||
await addLog(`LuckMail:已禁用 ${targetIds.length} 个本地已用邮箱`, 'ok');
|
||
return { disabledIds: targetIds };
|
||
}
|
||
|
||
async function ensureLuckmailPurchaseForFlow(options = {}) {
|
||
const { allowReuse = true } = options;
|
||
const state = await getState();
|
||
const existingPurchase = getCurrentLuckmailPurchase(state);
|
||
if (allowReuse && existingPurchase?.email_address && existingPurchase?.token) {
|
||
if (state.email !== existingPurchase.email_address) {
|
||
await setEmailState(existingPurchase.email_address);
|
||
}
|
||
return existingPurchase;
|
||
}
|
||
|
||
const config = getLuckmailSessionConfig(state);
|
||
const client = createLuckmailClient(state);
|
||
if (allowReuse) {
|
||
const reusablePurchase = await findReusableLuckmailPurchaseForFlow(state, client);
|
||
if (reusablePurchase) {
|
||
return activateLuckmailPurchaseForFlow(state, client, reusablePurchase, {
|
||
initializeCursor: true,
|
||
logMessage: `LuckMail:已复用 openai 邮箱 ${reusablePurchase.email_address}`,
|
||
});
|
||
}
|
||
}
|
||
|
||
const result = await client.user.purchaseEmails(DEFAULT_LUCKMAIL_PROJECT_CODE, 1, {
|
||
emailType: config.emailType,
|
||
domain: config.domain || undefined,
|
||
});
|
||
const purchases = normalizeLuckmailPurchases(result);
|
||
const purchase = purchases[0] || null;
|
||
if (!purchase?.email_address || !purchase?.token) {
|
||
throw new Error('LuckMail 购邮成功,但未返回可用邮箱或 token。');
|
||
}
|
||
|
||
return activateLuckmailPurchaseForFlow(state, client, purchase, {
|
||
initializeCursor: false,
|
||
logMessage: `LuckMail:已购买邮箱 ${purchase.email_address}(类型:${config.emailType},项目:${DEFAULT_LUCKMAIL_PROJECT_CODE})`,
|
||
});
|
||
}
|
||
|
||
async function resolveLuckmailVerificationMail(client, token, filters = {}, tokenCodeResult = null) {
|
||
const tokenCode = tokenCodeResult ? normalizeLuckmailTokenCode(tokenCodeResult) : null;
|
||
if (tokenCode?.mail) {
|
||
const tokenMail = tokenCode.verification_code && !tokenCode.mail.verification_code
|
||
? {
|
||
...tokenCode.mail,
|
||
verification_code: tokenCode.verification_code,
|
||
}
|
||
: tokenCode.mail;
|
||
const inlineMatch = pickLuckmailVerificationMail([tokenMail], filters);
|
||
if (inlineMatch) {
|
||
return inlineMatch;
|
||
}
|
||
}
|
||
|
||
const mailList = await client.user.getTokenMails(token);
|
||
let match = pickLuckmailVerificationMail(mailList.mails, filters);
|
||
if (match?.mail?.message_id && !match.mail.verification_code) {
|
||
const detail = await client.user.getTokenMailDetail(token, match.mail.message_id);
|
||
match = pickLuckmailVerificationMail([detail], filters);
|
||
}
|
||
return match || null;
|
||
}
|
||
|
||
async function legacyPollLuckmailVerificationCode(step, state, pollPayload = {}) {
|
||
const purchase = getCurrentLuckmailPurchase(state);
|
||
if (!purchase?.token) {
|
||
throw new Error('LuckMail 当前没有可用 token,请先执行步骤 3 购买邮箱。');
|
||
}
|
||
|
||
const client = createLuckmailClient(state);
|
||
const maxAttempts = Math.max(1, Number(pollPayload.maxAttempts) || 3);
|
||
const intervalMs = Math.max(15000, Number(pollPayload.intervalMs) || 15000);
|
||
const excludedCodes = new Set((pollPayload.excludeCodes || []).filter(Boolean));
|
||
|
||
const initialCursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor);
|
||
if (!initialCursor.messageId && !initialCursor.receivedAt) {
|
||
const mailList = await client.user.getTokenMails(purchase.token);
|
||
const baselineCursor = buildLuckmailBaselineCursor(mailList?.mails || []);
|
||
await setLuckmailMailCursorState(baselineCursor);
|
||
if (baselineCursor?.messageId || baselineCursor?.receivedAt) {
|
||
await addLog(`步骤 ${step}:LuckMail 已保存当前邮箱旧邮件快照,后续仅使用新收到的验证码。`, 'info');
|
||
}
|
||
}
|
||
|
||
let lastError = null;
|
||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||
throwIfStopped();
|
||
await addLog(`步骤 ${step}:正在通过 LuckMail 轮询验证码(${attempt}/${maxAttempts})...`, 'info');
|
||
|
||
try {
|
||
const tokenCode = await client.user.getTokenCode(purchase.token);
|
||
const cursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor);
|
||
if (tokenCode.verification_code && tokenCode.mail && !isLuckmailMailNewerThanCursor(tokenCode.mail, cursor)) {
|
||
throw new Error(`步骤 ${step}:LuckMail 返回的最新邮件仍是旧验证码。`);
|
||
}
|
||
|
||
let match = null;
|
||
if (tokenCode.has_new_mail || tokenCode.verification_code) {
|
||
match = await resolveLuckmailVerificationMail(client, purchase.token, filters, tokenCode);
|
||
}
|
||
if (!match) {
|
||
match = await resolveLuckmailVerificationMail(client, purchase.token, filters, null);
|
||
}
|
||
|
||
if (match?.mail) {
|
||
const cursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor);
|
||
if (!isLuckmailMailNewerThanCursor(match.mail, cursor)) {
|
||
throw new Error(`步骤 ${step}:LuckMail 命中的邮件不是新邮件。`);
|
||
}
|
||
|
||
await setLuckmailMailCursorState(buildLuckmailMailCursor(match.mail));
|
||
return {
|
||
ok: true,
|
||
code: match.code,
|
||
emailTimestamp: normalizeLuckmailTimestamp(match.mail.received_at) || Date.now(),
|
||
mailId: match.mail.message_id,
|
||
};
|
||
}
|
||
|
||
lastError = new Error(`步骤 ${step}:暂未在 LuckMail 邮箱中找到新的匹配验证码。`);
|
||
} catch (err) {
|
||
if (isStopError(err)) {
|
||
throw err;
|
||
}
|
||
lastError = err;
|
||
await addLog(`步骤 ${step}:LuckMail 轮询失败:${err.message}`, 'warn');
|
||
}
|
||
|
||
if (attempt < maxAttempts) {
|
||
await sleepWithStop(intervalMs);
|
||
}
|
||
}
|
||
|
||
throw lastError || new Error(`步骤 ${step}:未在 LuckMail 邮箱中找到新的匹配验证码。`);
|
||
}
|
||
|
||
async function pollLuckmailVerificationCode(step, state, pollPayload = {}) {
|
||
const purchase = getCurrentLuckmailPurchase(state);
|
||
if (!purchase?.token) {
|
||
throw new Error('LuckMail 当前没有可用 token,请先执行步骤 3 购买邮箱。');
|
||
}
|
||
|
||
const client = createLuckmailClient(state);
|
||
const maxAttempts = Math.max(1, Number(pollPayload.maxAttempts) || 3);
|
||
const intervalMs = Math.max(15000, Number(pollPayload.intervalMs) || 15000);
|
||
const excludedCodes = new Set((pollPayload.excludeCodes || []).filter(Boolean));
|
||
|
||
const initialCursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor);
|
||
if (!initialCursor.messageId && !initialCursor.receivedAt) {
|
||
const mailList = await client.user.getTokenMails(purchase.token);
|
||
const baselineCursor = buildLuckmailBaselineCursor(mailList?.mails || []);
|
||
await setLuckmailMailCursorState(baselineCursor);
|
||
if (baselineCursor?.messageId || baselineCursor?.receivedAt) {
|
||
await addLog(`步骤 ${step}:LuckMail 已保存当前邮箱旧邮件快照,后续仅使用新收到的验证码。`, 'info');
|
||
}
|
||
}
|
||
|
||
let lastError = null;
|
||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||
throwIfStopped();
|
||
await addLog(`步骤 ${step}:正在通过 LuckMail /code 接口轮询验证码(${attempt}/${maxAttempts})...`, 'info');
|
||
|
||
try {
|
||
const tokenCode = await client.user.getTokenCode(purchase.token);
|
||
const remoteEmail = String(tokenCode?.email_address || '').trim().toLowerCase();
|
||
const expectedEmail = String(purchase.email_address || state?.email || '').trim().toLowerCase();
|
||
if (remoteEmail && expectedEmail && remoteEmail !== expectedEmail) {
|
||
throw new Error(`步骤 ${step}:LuckMail token 对应邮箱与当前邮箱不一致。当前邮箱:${expectedEmail};token 邮箱:${remoteEmail}`);
|
||
}
|
||
|
||
const tokenMail = tokenCode.verification_code && tokenCode.mail && !tokenCode.mail.verification_code
|
||
? {
|
||
...tokenCode.mail,
|
||
verification_code: tokenCode.verification_code,
|
||
}
|
||
: tokenCode.mail;
|
||
const code = String(tokenCode?.verification_code || tokenMail?.verification_code || '').trim();
|
||
const cursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor);
|
||
|
||
if (!code || !tokenMail) {
|
||
lastError = new Error(`步骤 ${step}:LuckMail /code 接口暂未返回新的验证码。`);
|
||
} else if (excludedCodes.has(code)) {
|
||
lastError = new Error(`步骤 ${step}:LuckMail 返回的验证码 ${code} 已试过,等待 15 秒后再次轮询。`);
|
||
} else if (!isLuckmailMailNewerThanCursor(tokenMail, cursor)) {
|
||
lastError = new Error(`步骤 ${step}:LuckMail /code 返回的最新邮件仍是旧验证码。`);
|
||
} else {
|
||
await setLuckmailMailCursorState(buildLuckmailMailCursor(tokenMail));
|
||
return {
|
||
ok: true,
|
||
code,
|
||
emailTimestamp: normalizeLuckmailTimestamp(tokenMail.received_at) || Date.now(),
|
||
mailId: tokenMail.message_id,
|
||
};
|
||
}
|
||
} catch (err) {
|
||
if (isStopError(err)) {
|
||
throw err;
|
||
}
|
||
lastError = err;
|
||
await addLog(`步骤 ${step}:LuckMail /code 轮询失败:${err.message}`, 'warn');
|
||
}
|
||
|
||
if (attempt < maxAttempts) {
|
||
await sleepWithStop(intervalMs);
|
||
}
|
||
}
|
||
|
||
throw lastError || new Error(`步骤 ${step}:未在 LuckMail /code 接口中获取到新的验证码。`);
|
||
}
|
||
|
||
function summarizeCloudflareTempEmailMessagesForLog(messages) {
|
||
return (messages || [])
|
||
.slice()
|
||
.sort((left, right) => {
|
||
const leftTime = Date.parse(left.receivedDateTime || '') || 0;
|
||
const rightTime = Date.parse(right.receivedDateTime || '') || 0;
|
||
return rightTime - leftTime;
|
||
})
|
||
.slice(0, 3)
|
||
.map((message) => {
|
||
const receivedAt = message?.receivedDateTime || '未知时间';
|
||
const sender = message?.from?.emailAddress?.address || '未知发件人';
|
||
const subject = message?.subject || '(无主题)';
|
||
const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80);
|
||
const address = message?.address || '未知地址';
|
||
return `[${address}] ${receivedAt} | ${sender} | ${subject} | ${preview}`;
|
||
})
|
||
.join(' || ');
|
||
}
|
||
|
||
async function deleteCloudflareTempEmailMail(config, mailId) {
|
||
const normalizedMailId = String(mailId || '').trim();
|
||
if (!normalizedMailId) return false;
|
||
|
||
await requestCloudflareTempEmailJson(config, `/admin/mails/${encodeURIComponent(normalizedMailId)}`, {
|
||
method: 'DELETE',
|
||
});
|
||
return true;
|
||
}
|
||
|
||
async function listCloudflareTempEmailMessages(state, options = {}) {
|
||
const config = ensureCloudflareTempEmailConfig(state, { requireAdminAuth: true });
|
||
const address = normalizeCloudflareTempEmailAddress(options.address);
|
||
const payload = await requestCloudflareTempEmailJson(config, '/admin/mails', {
|
||
method: 'GET',
|
||
searchParams: {
|
||
limit: Number(options.limit) || CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE,
|
||
offset: Number(options.offset) || 0,
|
||
address,
|
||
},
|
||
});
|
||
|
||
const messages = normalizeCloudflareTempEmailMailApiMessages(payload).filter((message) => {
|
||
if (!address) return true;
|
||
return !message.address || normalizeCloudflareTempEmailAddress(message.address) === address;
|
||
});
|
||
|
||
return { config, messages };
|
||
}
|
||
|
||
async function pollCloudflareTempEmailVerificationCode(step, state, pollPayload = {}) {
|
||
const config = ensureCloudflareTempEmailConfig(state, { requireAdminAuth: true });
|
||
const targetEmail = resolveCloudflareTempEmailPollTargetEmail(state, pollPayload, config);
|
||
const registrationEmail = normalizeCloudflareTempEmailReceiveMailbox(state.email);
|
||
if (!targetEmail) {
|
||
throw new Error('Cloudflare Temp Email 轮询前缺少目标邮箱地址,请先填写注册邮箱或“邮件接收”邮箱。');
|
||
}
|
||
|
||
if (registrationEmail && registrationEmail !== targetEmail) {
|
||
await addLog(`步骤 ${step}:正在轮询 Cloudflare Temp Email 收件邮箱(${targetEmail}),注册邮箱为 ${registrationEmail}...`, 'info');
|
||
} else {
|
||
await addLog(`步骤 ${step}:正在轮询 Cloudflare Temp Email 邮件(${targetEmail})...`, 'info');
|
||
}
|
||
const maxAttempts = Number(pollPayload.maxAttempts) || 5;
|
||
const intervalMs = Number(pollPayload.intervalMs) || 3000;
|
||
let lastError = null;
|
||
|
||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||
throwIfStopped();
|
||
try {
|
||
const { messages } = await listCloudflareTempEmailMessages(state, {
|
||
address: targetEmail,
|
||
limit: pollPayload.limit || CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE,
|
||
offset: pollPayload.offset || 0,
|
||
});
|
||
const matchResult = pickVerificationMessageWithTimeFallback(messages, {
|
||
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
|
||
senderFilters: pollPayload.senderFilters || [],
|
||
subjectFilters: pollPayload.subjectFilters || [],
|
||
excludeCodes: pollPayload.excludeCodes || [],
|
||
});
|
||
const match = matchResult.match;
|
||
|
||
if (match?.code) {
|
||
if (matchResult.usedRelaxedFilters) {
|
||
const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配';
|
||
await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Cloudflare Temp Email 验证码。`, 'warn');
|
||
}
|
||
try {
|
||
await deleteCloudflareTempEmailMail(config, match.message?.id);
|
||
} catch (err) {
|
||
await addLog(`步骤 ${step}:删除 Cloudflare Temp Email 邮件失败:${err.message}`, 'warn');
|
||
}
|
||
return {
|
||
ok: true,
|
||
code: match.code,
|
||
emailTimestamp: match.receivedAt || Date.now(),
|
||
mailId: match.message?.id || '',
|
||
};
|
||
}
|
||
|
||
lastError = new Error(`步骤 ${step}:暂未在 Cloudflare Temp Email 中找到匹配验证码(${attempt}/${maxAttempts})。`);
|
||
await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info');
|
||
const sample = summarizeCloudflareTempEmailMessagesForLog(messages);
|
||
if (sample) {
|
||
await addLog(`步骤 ${step}:最近邮件样本:${sample}`, 'info');
|
||
}
|
||
} catch (err) {
|
||
lastError = err;
|
||
await addLog(`步骤 ${step}:Cloudflare Temp Email 轮询失败:${err.message}`, 'warn');
|
||
}
|
||
|
||
if (attempt < maxAttempts) {
|
||
await sleepWithStop(intervalMs);
|
||
}
|
||
}
|
||
|
||
throw lastError || new Error(`步骤 ${step}:未在 Cloudflare Temp Email 中找到新的匹配验证码。`);
|
||
}
|
||
|
||
async function getOpenIcloudHostPreference() {
|
||
try {
|
||
const tabs = await chrome.tabs.query({
|
||
url: ICLOUD_TAB_URL_PATTERNS,
|
||
});
|
||
|
||
const activeTab = tabs.find((tab) => tab.active);
|
||
const candidates = activeTab ? [activeTab, ...tabs.filter((tab) => tab.id !== activeTab.id)] : tabs;
|
||
for (const tab of candidates) {
|
||
try {
|
||
const host = normalizeIcloudHost(new URL(tab.url).host);
|
||
if (host) return host;
|
||
} catch {}
|
||
}
|
||
} catch {}
|
||
|
||
return '';
|
||
}
|
||
|
||
async function getPreferredIcloudLoginUrl(error = null, state = null) {
|
||
const currentState = state || await getState();
|
||
const configuredHost = getConfiguredIcloudHostPreference(currentState);
|
||
if (configuredHost) {
|
||
return getIcloudLoginUrlForHost(configuredHost);
|
||
}
|
||
|
||
const openHost = await getOpenIcloudHostPreference();
|
||
if (openHost) {
|
||
return getIcloudLoginUrlForHost(openHost);
|
||
}
|
||
|
||
const savedHost = normalizeIcloudHost(currentState?.preferredIcloudHost);
|
||
if (savedHost) {
|
||
return getIcloudLoginUrlForHost(savedHost);
|
||
}
|
||
|
||
const messageHint = getIcloudHostHintFromMessage(getErrorMessage(error));
|
||
if (messageHint) {
|
||
return getIcloudLoginUrlForHost(messageHint);
|
||
}
|
||
|
||
return ICLOUD_LOGIN_URLS[0];
|
||
}
|
||
|
||
async function getPreferredIcloudSetupUrls(state = null, error = null) {
|
||
const preferredLoginUrl = await getPreferredIcloudLoginUrl(error, state);
|
||
const preferredHost = normalizeIcloudHost(new URL(preferredLoginUrl).host);
|
||
const preferredSetupUrl = getIcloudSetupUrlForHost(preferredHost);
|
||
if (!preferredSetupUrl) {
|
||
return [...ICLOUD_SETUP_URLS];
|
||
}
|
||
return [
|
||
preferredSetupUrl,
|
||
...ICLOUD_SETUP_URLS.filter((url) => url !== preferredSetupUrl),
|
||
];
|
||
}
|
||
|
||
function isIcloudLoginRequiredError(error) {
|
||
const message = getErrorMessage(error).toLowerCase();
|
||
const hasAuthStatus401 = /\bstatus 401\b/.test(message);
|
||
const hasAuthStatus403 = /\bstatus 403\b/.test(message);
|
||
const hasTransientStatus = /\bstatus (409|421|429|5\d\d)\b/.test(message);
|
||
const hasTransientNetworkHint = message.includes('failed to fetch')
|
||
|| message.includes('networkerror')
|
||
|| message.includes('network request failed')
|
||
|| message.includes('timeout')
|
||
|| message.includes('timed out')
|
||
|| message.includes('cors')
|
||
|| message.includes('address space');
|
||
const hasExplicitLoginHint = message.includes('please sign in')
|
||
|| message.includes('sign in required')
|
||
|| message.includes('not logged in')
|
||
|| message.includes('login required')
|
||
|| message.includes('re-authentication required')
|
||
|| message.includes('unauthenticated')
|
||
|| message.includes('authentication required')
|
||
|| message.includes('需要先登录')
|
||
|| message.includes('请先登录');
|
||
const hasSelfPromptHint = message.includes('请先在新打开的 icloud 页面中完成登录')
|
||
|| message.includes('请先在当前浏览器登录');
|
||
const hasAuthStatusWithExplicitLoginHint = (hasAuthStatus401 || hasAuthStatus403)
|
||
&& hasExplicitLoginHint;
|
||
|
||
// Keep transient validate/network/cors errors out of login-required path.
|
||
if (message.includes('could not validate icloud session')) {
|
||
return false;
|
||
}
|
||
if (message.includes('page_context:')) {
|
||
return false;
|
||
}
|
||
if (hasSelfPromptHint) {
|
||
return false;
|
||
}
|
||
if (hasTransientStatus || hasTransientNetworkHint) {
|
||
return false;
|
||
}
|
||
|
||
if (hasAuthStatusWithExplicitLoginHint) {
|
||
return true;
|
||
}
|
||
|
||
if (hasExplicitLoginHint) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function isIcloudTransientContextError(error) {
|
||
const message = getErrorMessage(error).toLowerCase();
|
||
return /\bstatus (401|403|409|421|429|5\d\d)\b/.test(message)
|
||
|| message.includes('could not validate icloud session')
|
||
|| message.includes('page_context:')
|
||
|| message.includes('failed to fetch')
|
||
|| message.includes('networkerror')
|
||
|| message.includes('network request failed')
|
||
|| message.includes('cors')
|
||
|| message.includes('address space')
|
||
|| message.includes('timeout')
|
||
|| message.includes('timed out');
|
||
}
|
||
|
||
let lastIcloudLoginPromptAt = 0;
|
||
const activeIcloudRequestControllers = new Set();
|
||
let lastResolvedIcloudServiceUrl = '';
|
||
const icloudTransientLogThrottle = new Map();
|
||
|
||
function shouldEmitIcloudTransientLog(key, windowMs = 1500) {
|
||
const normalizedKey = String(key || '').trim();
|
||
if (!normalizedKey) {
|
||
return true;
|
||
}
|
||
const now = Date.now();
|
||
const lastAt = Number(icloudTransientLogThrottle.get(normalizedKey) || 0);
|
||
if (now - lastAt < Math.max(200, Number(windowMs) || 1500)) {
|
||
return false;
|
||
}
|
||
icloudTransientLogThrottle.set(normalizedKey, now);
|
||
return true;
|
||
}
|
||
|
||
async function openIcloudLoginPage(preferredUrl) {
|
||
const tabs = await chrome.tabs.query({
|
||
url: ICLOUD_TAB_URL_PATTERNS,
|
||
});
|
||
const preferredHost = new URL(preferredUrl).host;
|
||
const preferredIcloudHost = normalizeIcloudHost(preferredHost);
|
||
const existingSameHost = tabs.find((tab) => {
|
||
try {
|
||
return normalizeIcloudHost(new URL(tab.url).host) === preferredIcloudHost;
|
||
} catch {
|
||
return false;
|
||
}
|
||
});
|
||
const existingAnyIcloudTab = tabs.find((tab) => Number.isInteger(tab?.id));
|
||
|
||
if (existingSameHost?.id) {
|
||
await chrome.tabs.update(existingSameHost.id, { active: true });
|
||
return existingSameHost.id;
|
||
}
|
||
|
||
if (existingAnyIcloudTab?.id) {
|
||
await chrome.tabs.update(existingAnyIcloudTab.id, { active: true });
|
||
return existingAnyIcloudTab.id;
|
||
}
|
||
|
||
const created = await chrome.tabs.create({ url: preferredUrl, active: true });
|
||
return created.id;
|
||
}
|
||
|
||
async function promptIcloudLogin(error, actionLabel = 'iCloud 操作') {
|
||
const now = Date.now();
|
||
const preferredUrl = await getPreferredIcloudLoginUrl(error);
|
||
const originalError = getErrorMessage(error);
|
||
|
||
chrome.runtime.sendMessage({
|
||
type: 'ICLOUD_LOGIN_REQUIRED',
|
||
payload: {
|
||
actionLabel,
|
||
loginUrl: preferredUrl,
|
||
message: '需要先登录 iCloud,我已经为你打开登录页。',
|
||
detail: originalError,
|
||
},
|
||
}).catch(() => { });
|
||
|
||
if (now - lastIcloudLoginPromptAt < 15000) {
|
||
return;
|
||
}
|
||
lastIcloudLoginPromptAt = now;
|
||
|
||
await addLog(`iCloud:${actionLabel}时需要登录,正在打开 ${new URL(preferredUrl).host} ...`, 'warn');
|
||
|
||
try {
|
||
await openIcloudLoginPage(preferredUrl);
|
||
} catch (tabErr) {
|
||
await addLog(`iCloud:自动打开登录页失败:${getErrorMessage(tabErr)}`, 'warn');
|
||
}
|
||
}
|
||
|
||
async function withIcloudLoginHelp(actionLabel, action) {
|
||
const 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;
|
||
}
|
||
const hostname = String(parsedUrl.hostname || '').trim().toLowerCase().replace(/\.$/, '');
|
||
if (!hostname) {
|
||
return false;
|
||
}
|
||
return hostname === 'icloud.com'
|
||
|| hostname.endsWith('.icloud.com')
|
||
|| hostname === 'icloud.com.cn'
|
||
|| hostname.endsWith('.icloud.com.cn');
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function normalizeIcloudServiceUrl(rawUrl = '') {
|
||
const value = String(rawUrl || '').trim();
|
||
if (!value) {
|
||
return '';
|
||
}
|
||
try {
|
||
const parsedUrl = new URL(value);
|
||
if ((parsedUrl.protocol === 'https:' && parsedUrl.port === '443')
|
||
|| (parsedUrl.protocol === 'http:' && parsedUrl.port === '80')) {
|
||
parsedUrl.port = '';
|
||
}
|
||
return parsedUrl.toString().replace(/\/$/, '');
|
||
} catch {
|
||
return value.replace(/\/$/, '');
|
||
}
|
||
}
|
||
|
||
function rememberIcloudServiceUrl(rawUrl = '') {
|
||
const normalized = normalizeIcloudServiceUrl(rawUrl);
|
||
if (normalized) {
|
||
lastResolvedIcloudServiceUrl = normalized;
|
||
}
|
||
return normalized;
|
||
}
|
||
|
||
function isIcloudMaildomainwsHost(rawHost = '') {
|
||
const host = String(rawHost || '').trim().toLowerCase().replace(/\.$/, '');
|
||
if (!host) {
|
||
return false;
|
||
}
|
||
return host.endsWith('maildomainws.icloud.com') || host.endsWith('maildomainws.icloud.com.cn');
|
||
}
|
||
|
||
function appendIcloudClientQueryParams(rawUrl = '') {
|
||
const input = String(rawUrl || '').trim();
|
||
if (!input) {
|
||
return '';
|
||
}
|
||
try {
|
||
const parsed = new URL(input);
|
||
if (!isIcloudMaildomainwsHost(parsed.hostname)) {
|
||
return input;
|
||
}
|
||
|
||
if (!parsed.searchParams.has('clientBuildNumber')) {
|
||
parsed.searchParams.set('clientBuildNumber', ICLOUD_MAILDOMAINWS_CLIENT_BUILD_NUMBER);
|
||
}
|
||
if (!parsed.searchParams.has('clientMasteringNumber')) {
|
||
parsed.searchParams.set('clientMasteringNumber', ICLOUD_MAILDOMAINWS_CLIENT_BUILD_NUMBER);
|
||
}
|
||
if (!parsed.searchParams.has('clientId')) {
|
||
parsed.searchParams.set('clientId', '');
|
||
}
|
||
if (!parsed.searchParams.has('dsid')) {
|
||
parsed.searchParams.set('dsid', '');
|
||
}
|
||
return parsed.toString();
|
||
} catch {
|
||
return input;
|
||
}
|
||
}
|
||
|
||
function isIcloudMailPageUrl(rawUrl = '') {
|
||
try {
|
||
const parsedUrl = new URL(String(rawUrl || '').trim());
|
||
if (!normalizeIcloudHost(parsedUrl.hostname)) {
|
||
return false;
|
||
}
|
||
const pathname = String(parsedUrl.pathname || '').toLowerCase();
|
||
return pathname === '/mail' || pathname.startsWith('/mail/');
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function waitForIcloudMailTabReady(tabId, timeoutMs = 8000) {
|
||
if (!Number.isInteger(tabId)) {
|
||
return false;
|
||
}
|
||
const deadline = Date.now() + Math.max(500, Number(timeoutMs) || 8000);
|
||
while (Date.now() < deadline) {
|
||
try {
|
||
const tab = await chrome.tabs.get(tabId);
|
||
const status = String(tab?.status || '');
|
||
if (isIcloudMailPageUrl(tab?.url) && status === 'complete') {
|
||
return true;
|
||
}
|
||
} catch {
|
||
return false;
|
||
}
|
||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function ensureIcloudMailContextTab(tabs = [], targetHost = '', preferredHost = '') {
|
||
const tabList = Array.isArray(tabs) ? tabs : [];
|
||
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);
|
||
}
|
||
}
|
||
|
||
throw lastError || new Error(`iCloud 请求失败:${method} ${requestUrl}`);
|
||
}
|
||
|
||
async function validateIcloudSession(setupUrl) {
|
||
const data = await icloudRequest('POST', `${setupUrl}/validate`);
|
||
if (!data?.webservices?.premiummailsettings?.url) {
|
||
throw new Error('Could not validate iCloud session. Hide My Email service was unavailable.');
|
||
}
|
||
return data;
|
||
}
|
||
|
||
function shouldTryIcloudPageContextFallback(errors = []) {
|
||
const combinedMessage = String((errors || []).join(' | ')).toLowerCase();
|
||
if (!combinedMessage) {
|
||
return false;
|
||
}
|
||
return combinedMessage.includes('status 401')
|
||
|| combinedMessage.includes('status 403')
|
||
|| combinedMessage.includes('status 421')
|
||
|| combinedMessage.includes('networkerror')
|
||
|| combinedMessage.includes('network request failed')
|
||
|| combinedMessage.includes('failed to fetch')
|
||
|| combinedMessage.includes('timed out')
|
||
|| combinedMessage.includes('timeout')
|
||
|| combinedMessage.includes('cors');
|
||
}
|
||
|
||
async function validateIcloudSessionViaPageContext(tabId, setupUrl) {
|
||
const host = new URL(setupUrl).host;
|
||
try {
|
||
const injections = await chrome.scripting.executeScript({
|
||
target: { tabId, allFrames: false },
|
||
world: 'MAIN',
|
||
func: async (targetSetupUrl) => {
|
||
const timeoutMs = 12000;
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||
try {
|
||
const response = await fetch(`${targetSetupUrl}/validate`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
cache: 'no-store',
|
||
mode: 'cors',
|
||
signal: controller.signal,
|
||
});
|
||
const text = await response.text();
|
||
let data = null;
|
||
try {
|
||
data = text ? JSON.parse(text) : null;
|
||
} catch {}
|
||
return {
|
||
ok: Boolean(response.ok),
|
||
status: Number(response.status) || 0,
|
||
data,
|
||
error: '',
|
||
};
|
||
} catch (err) {
|
||
return {
|
||
ok: false,
|
||
status: 0,
|
||
data: null,
|
||
error: String(err?.message || err || 'unknown error'),
|
||
};
|
||
} finally {
|
||
clearTimeout(timeoutId);
|
||
}
|
||
},
|
||
args: [setupUrl],
|
||
});
|
||
|
||
const result = injections?.[0]?.result || null;
|
||
if (result?.ok && result?.data?.webservices?.premiummailsettings?.url) {
|
||
return {
|
||
setupUrl,
|
||
serviceUrl: normalizeIcloudServiceUrl(result.data.webservices.premiummailsettings.url),
|
||
resolvedBy: 'page_context',
|
||
};
|
||
}
|
||
|
||
if (result?.status) {
|
||
throw new Error(`status ${result.status}`);
|
||
}
|
||
throw new Error(result?.error || 'page context validate failed');
|
||
} catch (err) {
|
||
throw new Error(`${host}: ${getErrorMessage(err)}`);
|
||
}
|
||
}
|
||
|
||
async function resolveIcloudPremiumMailServiceViaPageContext(setupUrls, state) {
|
||
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();
|
||
const explicitHost = normalizeIcloudHost(options?.hostPreference || options?.preferredHost || '');
|
||
const setupUrls = explicitHost
|
||
? (() => {
|
||
const forcedSetupUrl = getIcloudSetupUrlForHost(explicitHost);
|
||
return forcedSetupUrl ? [forcedSetupUrl] : [];
|
||
})()
|
||
: await getPreferredIcloudSetupUrls(state);
|
||
|
||
for (const setupUrl of setupUrls) {
|
||
try {
|
||
const data = await validateIcloudSession(setupUrl);
|
||
const preferredIcloudHost = normalizeIcloudHost(new URL(setupUrl).host);
|
||
if (preferredIcloudHost && preferredIcloudHost !== normalizeIcloudHost(state.preferredIcloudHost)) {
|
||
await setState({ preferredIcloudHost });
|
||
}
|
||
return {
|
||
setupUrl,
|
||
serviceUrl: rememberIcloudServiceUrl(data.webservices.premiummailsettings.url),
|
||
};
|
||
} catch (err) {
|
||
errors.push(`${new URL(setupUrl).host}: ${getErrorMessage(err)}`);
|
||
}
|
||
}
|
||
|
||
if (shouldTryIcloudPageContextFallback(errors)) {
|
||
const {
|
||
service,
|
||
errors: pageContextErrors,
|
||
noTab: pageContextNoTab = false,
|
||
} = await resolveIcloudPremiumMailServiceViaPageContext(setupUrls, state);
|
||
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。');
|
||
}
|
||
|
||
function getIcloudAliasLabel() {
|
||
const now = new Date();
|
||
const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
||
return `MultiPage ${dateStr}`;
|
||
}
|
||
|
||
async function checkIcloudSession(options = {}) {
|
||
const actionLabel = String(options?.actionLabel || '检查 iCloud 会话').trim() || '检查 iCloud 会话';
|
||
const { actionLabel: _ignoredActionLabel, ...resolveOptions } = options || {};
|
||
return withIcloudLoginHelp(actionLabel, async () => {
|
||
const { setupUrl } = await resolveIcloudPremiumMailService(resolveOptions);
|
||
await addLog(`iCloud:会话校验通过(${new URL(setupUrl).host})`, 'ok');
|
||
return { ok: true, setupUrl };
|
||
});
|
||
}
|
||
|
||
async function loadNormalizedIcloudAliases(options = {}) {
|
||
const {
|
||
resolveOptions = {},
|
||
serviceUrl: initialServiceUrl = '',
|
||
silent = false,
|
||
} = options;
|
||
|
||
let serviceUrl = String(initialServiceUrl || '').trim().replace(/\/$/, '');
|
||
let lastError = null;
|
||
|
||
for (let endpointAttempt = 1; endpointAttempt <= 2; endpointAttempt += 1) {
|
||
throwIfStopped();
|
||
|
||
if (!serviceUrl) {
|
||
const resolved = await resolveIcloudPremiumMailService(resolveOptions);
|
||
serviceUrl = resolved.serviceUrl;
|
||
}
|
||
|
||
try {
|
||
if (!silent) {
|
||
await addLog(`iCloud:正在从 ${new URL(serviceUrl).host} 加载 Hide My Email 别名列表...`, 'info');
|
||
}
|
||
const response = await icloudRequest('GET', `${serviceUrl}/v2/hme/list`, {
|
||
timeoutMs: ICLOUD_REQUEST_TIMEOUT_MS,
|
||
maxAttempts: ICLOUD_LIST_MAX_ATTEMPTS,
|
||
retryLabel: '加载 iCloud 别名列表',
|
||
logRetries: true,
|
||
});
|
||
const state = await getState();
|
||
return {
|
||
serviceUrl,
|
||
aliases: normalizeIcloudAliasList(response, {
|
||
usedEmails: getEffectiveUsedEmails(state),
|
||
preservedEmails: getPreservedAliasMap(state),
|
||
}),
|
||
};
|
||
} catch (err) {
|
||
lastError = err;
|
||
if (endpointAttempt >= 2 || !isIcloudRetryableError(err)) {
|
||
throw err;
|
||
}
|
||
await addLog(`iCloud:${new URL(serviceUrl).host} 别名列表请求失败,正在刷新服务节点后重试...`, 'warn');
|
||
serviceUrl = '';
|
||
}
|
||
}
|
||
|
||
throw lastError || new Error('加载 iCloud 别名列表失败。');
|
||
}
|
||
|
||
async function listIcloudAliases(options = {}) {
|
||
try {
|
||
return await withIcloudLoginHelp('加载 iCloud 隐私邮箱列表', async () => {
|
||
const { serviceUrl } = await resolveIcloudPremiumMailService(options);
|
||
const response = await icloudRequest('GET', `${serviceUrl}/v2/hme/list`);
|
||
const state = await getState();
|
||
const aliases = normalizeIcloudAliasList(response, {
|
||
usedEmails: getEffectiveUsedEmails(state),
|
||
preservedEmails: getPreservedAliasMap(state),
|
||
});
|
||
await setState({
|
||
icloudAliasCache: normalizeIcloudAliasCacheList(aliases),
|
||
icloudAliasCacheAt: Date.now(),
|
||
});
|
||
return aliases;
|
||
});
|
||
} catch (err) {
|
||
const message = getErrorMessage(err);
|
||
const transientContextError = err?.code === 'ICLOUD_TRANSIENT_CONTEXT'
|
||
|| message.includes('网络/上下文波动');
|
||
if (!transientContextError) {
|
||
throw err;
|
||
}
|
||
const state = await getState();
|
||
const freshCachedAliases = getIcloudAliasCacheFromState(state);
|
||
if (freshCachedAliases.length) {
|
||
await addLog(`iCloud:加载别名失败,已回退最近缓存(${freshCachedAliases.length} 条)。`, 'warn');
|
||
return freshCachedAliases;
|
||
}
|
||
|
||
const staleCachedAliases = getIcloudAliasCacheFromState(state, { maxAgeMs: 0 });
|
||
if (staleCachedAliases.length) {
|
||
await addLog(`iCloud:加载别名失败,已回退历史缓存(${staleCachedAliases.length} 条)。`, 'warn');
|
||
return staleCachedAliases;
|
||
}
|
||
|
||
const localFallbackAliases = buildIcloudAliasFallbackFromLocalState(state);
|
||
if (localFallbackAliases.length) {
|
||
await addLog(`iCloud:加载别名失败,已回退本地别名记录(${localFallbackAliases.length} 条)。`, 'warn');
|
||
return localFallbackAliases;
|
||
}
|
||
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
async function deleteIcloudAlias(payload) {
|
||
return withIcloudLoginHelp('删除 iCloud 隐私邮箱', async () => {
|
||
const alias = typeof payload === 'string'
|
||
? { email: String(payload).trim().toLowerCase(), anonymousId: '' }
|
||
: {
|
||
email: String(payload?.email || '').trim().toLowerCase(),
|
||
anonymousId: String(payload?.anonymousId || '').trim(),
|
||
};
|
||
|
||
if (!alias.email) {
|
||
throw new Error('未提供需要删除的 iCloud 隐私邮箱。');
|
||
}
|
||
if (!alias.anonymousId) {
|
||
throw new Error(`缺少 ${alias.email} 的 anonymousId,请先刷新 iCloud 别名列表。`);
|
||
}
|
||
|
||
let serviceUrl = '';
|
||
try {
|
||
({ serviceUrl } = await resolveIcloudPremiumMailService());
|
||
} catch (resolveErr) {
|
||
const canFallbackToCachedService = isIcloudTransientContextError(resolveErr)
|
||
&& Boolean(lastResolvedIcloudServiceUrl);
|
||
if (!canFallbackToCachedService) {
|
||
throw resolveErr;
|
||
}
|
||
serviceUrl = lastResolvedIcloudServiceUrl;
|
||
await addLog(`iCloud:会话校验暂时不可用,已回退最近可用服务节点 ${new URL(serviceUrl).host} 继续删除。`, 'warn');
|
||
}
|
||
|
||
try {
|
||
const directDelete = await icloudRequest('POST', `${serviceUrl}/v1/hme/delete`, {
|
||
data: { anonymousId: alias.anonymousId },
|
||
});
|
||
if (directDelete?.success === false) {
|
||
throw new Error(directDelete?.error?.errorMessage || 'delete failed');
|
||
}
|
||
} catch (err) {
|
||
await addLog(`iCloud:直接删除 ${alias.email} 失败,尝试先停用再删除...`, 'warn');
|
||
|
||
const deactivated = await icloudRequest('POST', `${serviceUrl}/v1/hme/deactivate`, {
|
||
data: { anonymousId: alias.anonymousId },
|
||
});
|
||
if (deactivated?.success === false) {
|
||
throw new Error(deactivated?.error?.errorMessage || `停用 ${alias.email} 失败`);
|
||
}
|
||
|
||
const deleted = await icloudRequest('POST', `${serviceUrl}/v1/hme/delete`, {
|
||
data: { anonymousId: alias.anonymousId },
|
||
});
|
||
if (deleted?.success === false) {
|
||
throw new Error(deleted?.error?.errorMessage || `删除 ${alias.email} 失败`);
|
||
}
|
||
}
|
||
|
||
const state = await getState();
|
||
const manualAliasUsage = getManualAliasUsageMap(state);
|
||
const preservedAliases = getPreservedAliasMap(state);
|
||
delete manualAliasUsage[alias.email];
|
||
delete preservedAliases[alias.email];
|
||
await setState({ manualAliasUsage, preservedAliases });
|
||
|
||
await addLog(`iCloud:已删除 ${alias.email}`, 'ok');
|
||
broadcastIcloudAliasesChanged({ reason: 'deleted', email: alias.email });
|
||
return { email: alias.email };
|
||
});
|
||
}
|
||
|
||
async function deleteUsedIcloudAliases() {
|
||
const aliases = await listIcloudAliases();
|
||
const usedAliases = aliases.filter((alias) => alias.used);
|
||
if (!usedAliases.length) {
|
||
return { deleted: [], skipped: [] };
|
||
}
|
||
|
||
const deleted = [];
|
||
const skipped = [];
|
||
for (const alias of usedAliases) {
|
||
if (alias.preserved) {
|
||
skipped.push({ email: alias.email, error: 'preserved' });
|
||
continue;
|
||
}
|
||
try {
|
||
await deleteIcloudAlias(alias);
|
||
deleted.push(alias.email);
|
||
} catch (err) {
|
||
skipped.push({ email: alias.email, error: getErrorMessage(err) });
|
||
}
|
||
}
|
||
return { deleted, skipped };
|
||
}
|
||
|
||
async function fetchIcloudHideMyEmail(options = {}) {
|
||
return withIcloudLoginHelp('获取 iCloud 隐私邮箱', async () => {
|
||
throwIfStopped();
|
||
const generateNew = Boolean(options?.generateNew);
|
||
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)
|
||
);
|
||
|
||
if (!generateNew) {
|
||
const reusableAlias = pickReusableIcloudAlias(existingAliases);
|
||
if (reusableAlias) {
|
||
await setEmailState(reusableAlias.email);
|
||
await addLog(`iCloud:复用未使用别名 ${reusableAlias.email}`, 'ok');
|
||
broadcastIcloudAliasesChanged({ reason: 'selected', email: reusableAlias.email });
|
||
return reusableAlias.email;
|
||
}
|
||
} else {
|
||
await addLog('iCloud:已启用“始终创建新别名”,本次将跳过复用。', 'info');
|
||
}
|
||
|
||
await addLog('iCloud:没有可复用别名,开始生成新的 Hide My Email 地址...', 'warn');
|
||
await addLog(`iCloud:正在向 ${new URL(activeServiceUrl).host} 请求新的 Hide My Email 候选地址...`, 'info');
|
||
|
||
try {
|
||
let generated = null;
|
||
try {
|
||
generated = await icloudRequest('POST', `${activeServiceUrl}/v1/hme/generate`, {
|
||
timeoutMs: ICLOUD_REQUEST_TIMEOUT_MS,
|
||
maxAttempts: ICLOUD_WRITE_MAX_ATTEMPTS,
|
||
retryLabel: '生成 Hide My Email 地址',
|
||
logRetries: true,
|
||
});
|
||
} catch (err) {
|
||
if (!isIcloudRetryableError(err)) {
|
||
throw err;
|
||
}
|
||
await addLog('iCloud:生成候选别名失败,正在刷新服务节点后再试一次...', 'warn');
|
||
const refreshedService = await resolveIcloudPremiumMailService();
|
||
activeServiceUrl = refreshedService.serviceUrl;
|
||
generated = await icloudRequest('POST', `${activeServiceUrl}/v1/hme/generate`, {
|
||
timeoutMs: ICLOUD_REQUEST_TIMEOUT_MS,
|
||
maxAttempts: ICLOUD_WRITE_MAX_ATTEMPTS,
|
||
retryLabel: '生成 Hide My Email 地址',
|
||
logRetries: true,
|
||
});
|
||
}
|
||
|
||
if (!generated?.success || !generated?.result?.hme) {
|
||
throw new Error(generated?.error?.errorMessage || 'iCloud 隐私邮箱生成失败。');
|
||
}
|
||
|
||
const generatedHmeRaw = generated.result.hme;
|
||
const generatedAlias = String(
|
||
(typeof generatedHmeRaw === 'string'
|
||
? generatedHmeRaw
|
||
: generatedHmeRaw?.hme
|
||
|| generatedHmeRaw?.email
|
||
|| generatedHmeRaw?.alias
|
||
|| generatedHmeRaw?.address
|
||
|| '')
|
||
).trim().toLowerCase();
|
||
if (!generatedAlias) {
|
||
throw new Error('iCloud 隐私邮箱生成失败:未返回可用别名。');
|
||
}
|
||
await addLog(`iCloud:已生成候选别名 ${generatedAlias},正在保留...`, 'info');
|
||
|
||
const reserveData = {
|
||
...(generatedHmeRaw && typeof generatedHmeRaw === 'object' && !Array.isArray(generatedHmeRaw)
|
||
? generatedHmeRaw
|
||
: {}),
|
||
hme: generatedAlias,
|
||
label: getIcloudAliasLabel(),
|
||
note: 'Generated through Multi-Page Automation',
|
||
};
|
||
|
||
let alias = '';
|
||
try {
|
||
const reserved = await icloudRequest('POST', `${activeServiceUrl}/v1/hme/reserve`, {
|
||
data: reserveData,
|
||
timeoutMs: ICLOUD_REQUEST_TIMEOUT_MS,
|
||
maxAttempts: 1,
|
||
});
|
||
|
||
if (!reserved?.success || !reserved?.result?.hme?.hme) {
|
||
throw new Error(reserved?.error?.errorMessage || 'iCloud 隐私邮箱保留失败。');
|
||
}
|
||
|
||
alias = String(reserved.result.hme.hme || '').trim().toLowerCase();
|
||
} catch (reserveErr) {
|
||
const reserveErrMessage = getErrorMessage(reserveErr);
|
||
const shouldTryListFallback = isIcloudRetryableError(reserveErr)
|
||
|| /\bstatus (?:401|403|409)\b/i.test(reserveErrMessage)
|
||
|| /failed to fetch/i.test(reserveErrMessage);
|
||
if (!shouldTryListFallback) {
|
||
throw reserveErr;
|
||
}
|
||
|
||
await addLog('iCloud:保留别名返回鉴权/网络异常,正在回查别名列表确认是否已创建...', 'warn');
|
||
const { aliases: aliasesAfterReserveFailure, serviceUrl: refreshedListServiceUrl } = await loadNormalizedIcloudAliases({
|
||
serviceUrl: activeServiceUrl,
|
||
silent: true,
|
||
});
|
||
activeServiceUrl = refreshedListServiceUrl || activeServiceUrl;
|
||
|
||
let recoveredAlias = findIcloudAliasByEmail(aliasesAfterReserveFailure, generatedAlias);
|
||
if (!recoveredAlias) {
|
||
recoveredAlias = aliasesAfterReserveFailure.find(
|
||
(aliasItem) => !existingAliasEmailSet.has(String(aliasItem?.email || '').trim().toLowerCase())
|
||
) || null;
|
||
}
|
||
|
||
if (recoveredAlias?.email) {
|
||
alias = String(recoveredAlias.email || '').trim().toLowerCase();
|
||
await addLog(`iCloud:保留请求异常,但已在列表确认别名 ${alias},继续使用。`, 'warn');
|
||
} else if (isIcloudRetryableError(reserveErr)) {
|
||
await addLog(`iCloud:列表中尚未出现 ${generatedAlias},正在刷新服务节点后重试保留一次...`, 'warn');
|
||
const refreshedService = await resolveIcloudPremiumMailService();
|
||
activeServiceUrl = refreshedService.serviceUrl;
|
||
const reservedRetry = await icloudRequest('POST', `${activeServiceUrl}/v1/hme/reserve`, {
|
||
data: reserveData,
|
||
timeoutMs: ICLOUD_REQUEST_TIMEOUT_MS,
|
||
maxAttempts: 1,
|
||
});
|
||
if (!reservedRetry?.success || !reservedRetry?.result?.hme?.hme) {
|
||
throw new Error(reservedRetry?.error?.errorMessage || 'iCloud 隐私邮箱保留失败。');
|
||
}
|
||
alias = String(reservedRetry.result.hme.hme || '').trim().toLowerCase();
|
||
} else {
|
||
alias = generatedAlias;
|
||
await addLog(`iCloud:保留请求异常,已回退使用生成别名 ${alias}。`, 'warn');
|
||
}
|
||
}
|
||
|
||
await setEmailState(alias);
|
||
await addLog(`iCloud:已创建并保留新别名 ${alias}`, 'ok');
|
||
broadcastIcloudAliasesChanged({ reason: 'created', email: alias });
|
||
return alias;
|
||
} catch (err) {
|
||
if (!shouldStopIcloudAutoFetchRetries(err)) {
|
||
throw err;
|
||
}
|
||
|
||
const reusableAlias = pickReusableIcloudAlias(existingAliases);
|
||
if (reusableAlias) {
|
||
await setEmailState(reusableAlias.email);
|
||
await addLog(
|
||
`iCloud:当前网络/上下文波动,暂无法创建新别名,已临时回退复用 ${reusableAlias.email}。`,
|
||
'warn'
|
||
);
|
||
broadcastIcloudAliasesChanged({ reason: 'selected', email: reusableAlias.email });
|
||
return reusableAlias.email;
|
||
}
|
||
|
||
throw new Error(
|
||
`iCloud 当前无法创建新别名:${getErrorMessage(err)}。请先确认 iCloud 页面已登录且网络可访问,再重试。`
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
async function finalizeIcloudAliasAfterSuccessfulFlow(state) {
|
||
const email = String(state?.email || '').trim().toLowerCase();
|
||
if (!email) {
|
||
return { handled: false, deleted: false };
|
||
}
|
||
|
||
const knownIcloudAlias = normalizeEmailGenerator(state?.emailGenerator) === 'icloud'
|
||
|| Object.prototype.hasOwnProperty.call(getManualAliasUsageMap(state), email)
|
||
|| Object.prototype.hasOwnProperty.call(getPreservedAliasMap(state), email);
|
||
if (!knownIcloudAlias) {
|
||
return { handled: false, deleted: false };
|
||
}
|
||
|
||
await setIcloudAliasUsedState({ email, used: true }, { silentLog: true });
|
||
await addLog(`iCloud:流程成功后已标记 ${email} 为已用。`, 'ok');
|
||
|
||
if (!state.autoDeleteUsedIcloudAlias) {
|
||
return { handled: true, deleted: false };
|
||
}
|
||
|
||
if (isAliasPreserved(state, email)) {
|
||
await addLog(`iCloud:${email} 已被标记为保留,跳过自动删除。`, 'info');
|
||
return { handled: true, deleted: false };
|
||
}
|
||
|
||
try {
|
||
const aliases = await listIcloudAliases();
|
||
const alias = findIcloudAliasByEmail(aliases, email);
|
||
if (!alias) {
|
||
await addLog(`iCloud:自动删除跳过,列表中未找到 ${email}。`, 'warn');
|
||
return { handled: true, deleted: false };
|
||
}
|
||
if (alias.preserved) {
|
||
await addLog(`iCloud:${email} 在最新别名列表中已是保留状态,跳过自动删除。`, 'info');
|
||
return { handled: true, deleted: false };
|
||
}
|
||
if (!alias.anonymousId) {
|
||
await addLog(`iCloud:自动删除跳过,${email} 缺少 anonymousId,请先刷新列表后重试。`, 'warn');
|
||
return { handled: true, deleted: false };
|
||
}
|
||
await deleteIcloudAlias(alias);
|
||
await addLog(`iCloud:流程成功后已自动删除 ${email}。`, 'ok');
|
||
return { handled: true, deleted: true };
|
||
} catch (err) {
|
||
if (isIcloudTransientContextError(err)) {
|
||
await addLog(`iCloud:自动删除 ${email} 暂时跳过(网络/上下文波动),可稍后手动删除。`, 'info');
|
||
} else {
|
||
await addLog(`iCloud:自动删除 ${email} 失败:${getErrorMessage(err)}`, 'warn');
|
||
}
|
||
return { handled: true, deleted: false };
|
||
}
|
||
}
|
||
|
||
async function finalizePhoneActivationAfterSuccessfulFlow(state) {
|
||
if (typeof phoneVerificationHelpers?.finalizePendingPhoneActivationConfirmation !== 'function') {
|
||
return null;
|
||
}
|
||
return phoneVerificationHelpers.finalizePendingPhoneActivationConfirmation(state);
|
||
}
|
||
|
||
// ============================================================
|
||
// Tab Registry
|
||
// ============================================================
|
||
|
||
async function getTabRegistry() {
|
||
return tabRuntime.getTabRegistry();
|
||
}
|
||
|
||
async function registerTab(source, tabId) {
|
||
return tabRuntime.registerTab(source, tabId);
|
||
}
|
||
|
||
async function isTabAlive(source) {
|
||
return tabRuntime.isTabAlive(source);
|
||
}
|
||
|
||
async function getTabId(source) {
|
||
return tabRuntime.getTabId(source);
|
||
}
|
||
|
||
function parseUrlSafely(rawUrl) {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.parseUrlSafely) {
|
||
return navigationUtils.parseUrlSafely(rawUrl);
|
||
}
|
||
if (!rawUrl) return null;
|
||
try {
|
||
return new URL(rawUrl);
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function normalizeSub2ApiUrl(rawUrl) {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.normalizeSub2ApiUrl) {
|
||
return navigationUtils.normalizeSub2ApiUrl(rawUrl);
|
||
}
|
||
const input = (rawUrl || '').trim() || DEFAULT_SUB2API_URL;
|
||
const withProtocol = /^https?:\/\//i.test(input) ? input : `https://${input}`;
|
||
const parsed = new URL(withProtocol);
|
||
if (!parsed.pathname || parsed.pathname === '/') {
|
||
parsed.pathname = '/admin/accounts';
|
||
}
|
||
parsed.hash = '';
|
||
return parsed.toString();
|
||
}
|
||
|
||
function normalizeCodex2ApiUrl(rawUrl) {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.normalizeCodex2ApiUrl) {
|
||
return navigationUtils.normalizeCodex2ApiUrl(rawUrl);
|
||
}
|
||
const input = (rawUrl || '').trim() || DEFAULT_CODEX2API_URL;
|
||
const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`;
|
||
const parsed = new URL(withProtocol);
|
||
if (!parsed.pathname || parsed.pathname === '/' || parsed.pathname === '/admin') {
|
||
parsed.pathname = '/admin/accounts';
|
||
}
|
||
parsed.hash = '';
|
||
return parsed.toString();
|
||
}
|
||
|
||
function getPanelMode(state = {}) {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.getPanelMode) {
|
||
return navigationUtils.getPanelMode(state);
|
||
}
|
||
if (state.panelMode === 'sub2api') {
|
||
return 'sub2api';
|
||
}
|
||
if (state.panelMode === 'codex2api') {
|
||
return 'codex2api';
|
||
}
|
||
return 'cpa';
|
||
}
|
||
|
||
function getPanelModeLabel(modeOrState) {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.getPanelModeLabel) {
|
||
return navigationUtils.getPanelModeLabel(modeOrState);
|
||
}
|
||
const mode = typeof modeOrState === 'string' ? modeOrState : getPanelMode(modeOrState);
|
||
if (mode === 'sub2api') {
|
||
return 'SUB2API';
|
||
}
|
||
if (mode === 'codex2api') {
|
||
return 'Codex2API';
|
||
}
|
||
return 'CPA';
|
||
}
|
||
|
||
function isSignupPageHost(hostname = '') {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.isSignupPageHost) {
|
||
return navigationUtils.isSignupPageHost(hostname);
|
||
}
|
||
return ['auth0.openai.com', 'auth.openai.com', 'accounts.openai.com'].includes(hostname);
|
||
}
|
||
|
||
function isSignupEntryHost(hostname = '') {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.isSignupEntryHost) {
|
||
return navigationUtils.isSignupEntryHost(hostname);
|
||
}
|
||
return ['chatgpt.com', 'chat.openai.com'].includes(hostname);
|
||
}
|
||
|
||
function isSignupPasswordPageUrl(rawUrl) {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.isSignupPasswordPageUrl) {
|
||
return navigationUtils.isSignupPasswordPageUrl(rawUrl);
|
||
}
|
||
const parsed = parseUrlSafely(rawUrl);
|
||
if (!parsed) return false;
|
||
return isSignupPageHost(parsed.hostname)
|
||
&& /\/create-account\/password(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||
}
|
||
|
||
function isSignupEmailVerificationPageUrl(rawUrl) {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.isSignupEmailVerificationPageUrl) {
|
||
return navigationUtils.isSignupEmailVerificationPageUrl(rawUrl);
|
||
}
|
||
const parsed = parseUrlSafely(rawUrl);
|
||
if (!parsed) return false;
|
||
return isSignupPageHost(parsed.hostname)
|
||
&& /\/email-verification(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||
}
|
||
|
||
function is163MailHost(hostname = '') {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.is163MailHost) {
|
||
return navigationUtils.is163MailHost(hostname);
|
||
}
|
||
return hostname === 'mail.163.com'
|
||
|| hostname.endsWith('.mail.163.com')
|
||
|| hostname === 'mail.126.com'
|
||
|| hostname.endsWith('.mail.126.com')
|
||
|| hostname === 'webmail.vip.163.com';
|
||
}
|
||
|
||
function isLocalhostOAuthCallbackUrl(rawUrl) {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.isLocalhostOAuthCallbackUrl) {
|
||
return navigationUtils.isLocalhostOAuthCallbackUrl(rawUrl);
|
||
}
|
||
const parsed = parseUrlSafely(rawUrl);
|
||
if (!parsed) return false;
|
||
if (!['http:', 'https:'].includes(parsed.protocol)) return false;
|
||
if (!['localhost', '127.0.0.1'].includes(parsed.hostname)) return false;
|
||
if (!['/auth/callback', '/codex/callback'].includes(parsed.pathname)) return false;
|
||
const code = (parsed.searchParams.get('code') || '').trim();
|
||
const state = (parsed.searchParams.get('state') || '').trim();
|
||
return Boolean(code && state);
|
||
}
|
||
|
||
function isLocalCpaUrl(rawUrl) {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.isLocalCpaUrl) {
|
||
return navigationUtils.isLocalCpaUrl(rawUrl);
|
||
}
|
||
const parsed = parseUrlSafely(rawUrl);
|
||
if (!parsed) return false;
|
||
if (!['http:', 'https:'].includes(parsed.protocol)) return false;
|
||
return ['localhost', '127.0.0.1'].includes(parsed.hostname);
|
||
}
|
||
|
||
function shouldBypassStep9ForLocalCpa(state) {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.shouldBypassStep9ForLocalCpa) {
|
||
return navigationUtils.shouldBypassStep9ForLocalCpa(state);
|
||
}
|
||
return normalizeLocalCpaStep9Mode(state?.localCpaStep9Mode) === 'bypass'
|
||
&& Boolean(state?.localhostUrl)
|
||
&& isLocalCpaUrl(state?.vpsUrl);
|
||
}
|
||
|
||
function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.matchesSourceUrlFamily) {
|
||
return navigationUtils.matchesSourceUrlFamily(source, candidateUrl, referenceUrl);
|
||
}
|
||
const candidate = parseUrlSafely(candidateUrl);
|
||
if (!candidate) return false;
|
||
const reference = parseUrlSafely(referenceUrl);
|
||
switch (source) {
|
||
case 'signup-page':
|
||
return isSignupPageHost(candidate.hostname) || isSignupEntryHost(candidate.hostname);
|
||
case 'duck-mail':
|
||
return candidate.hostname === 'duckduckgo.com' && candidate.pathname.startsWith('/email/');
|
||
case 'qq-mail':
|
||
return candidate.hostname === 'mail.qq.com' || candidate.hostname === 'wx.mail.qq.com';
|
||
case 'mail-163':
|
||
return is163MailHost(candidate.hostname);
|
||
case 'gmail-mail':
|
||
return candidate.hostname === 'mail.google.com';
|
||
case 'inbucket-mail':
|
||
return Boolean(reference) && candidate.origin === reference.origin && candidate.pathname.startsWith('/m/');
|
||
case 'mail-2925':
|
||
return candidate.hostname === '2925.com' || candidate.hostname === 'www.2925.com';
|
||
case 'vps-panel':
|
||
return Boolean(reference) && candidate.origin === reference.origin && candidate.pathname === reference.pathname;
|
||
case 'sub2api-panel':
|
||
return Boolean(reference)
|
||
&& candidate.origin === reference.origin
|
||
&& (candidate.pathname.startsWith('/admin/accounts') || candidate.pathname.startsWith('/login') || candidate.pathname === '/');
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function rememberSourceLastUrl(source, url) {
|
||
return tabRuntime.rememberSourceLastUrl(source, url);
|
||
}
|
||
|
||
async function closeConflictingTabsForSource(source, currentUrl, options = {}) {
|
||
return tabRuntime.closeConflictingTabsForSource(source, currentUrl, options);
|
||
}
|
||
|
||
function isLocalhostOAuthCallbackTabMatch(callbackUrl, candidateUrl) {
|
||
return tabRuntime.isLocalhostOAuthCallbackTabMatch(callbackUrl, candidateUrl);
|
||
}
|
||
|
||
async function closeLocalhostCallbackTabs(callbackUrl, options = {}) {
|
||
return tabRuntime.closeLocalhostCallbackTabs(callbackUrl, options);
|
||
}
|
||
|
||
function buildLocalhostCleanupPrefix(rawUrl) {
|
||
return tabRuntime.buildLocalhostCleanupPrefix(rawUrl);
|
||
}
|
||
|
||
async function closeTabsByUrlPrefix(prefix, options = {}) {
|
||
return tabRuntime.closeTabsByUrlPrefix(prefix, options);
|
||
}
|
||
|
||
async function pingContentScriptOnTab(tabId) {
|
||
return tabRuntime.pingContentScriptOnTab(tabId);
|
||
}
|
||
|
||
async function waitForTabUrlFamily(source, tabId, referenceUrl, options = {}) {
|
||
return tabRuntime.waitForTabUrlFamily(source, tabId, referenceUrl, options);
|
||
}
|
||
|
||
async function waitForTabUrlMatch(tabId, matcher, options = {}) {
|
||
return tabRuntime.waitForTabUrlMatch(tabId, matcher, options);
|
||
}
|
||
|
||
async function waitForTabUrlMatchUntilStopped(tabId, matcher, options = {}) {
|
||
const retryDelayMs = Math.max(100, Math.floor(Number(options.retryDelayMs) || 300));
|
||
while (true) {
|
||
throwIfStopped();
|
||
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||
if (!tab) {
|
||
throw new Error('目标标签页已关闭,无法继续等待页面跳转。');
|
||
}
|
||
if (typeof matcher === 'function' && matcher(tab.url || '', tab)) {
|
||
return tab;
|
||
}
|
||
await sleepWithStop(retryDelayMs);
|
||
}
|
||
}
|
||
|
||
async function waitForTabComplete(tabId, options = {}) {
|
||
return tabRuntime.waitForTabComplete(tabId, options);
|
||
}
|
||
|
||
async function waitForTabStableComplete(tabId, options = {}) {
|
||
return tabRuntime.waitForTabStableComplete(tabId, options);
|
||
}
|
||
|
||
async function waitForTabCompleteUntilStopped(tabId, options = {}) {
|
||
const retryDelayMs = Math.max(100, Math.floor(Number(options.retryDelayMs) || 300));
|
||
while (true) {
|
||
throwIfStopped();
|
||
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||
if (!tab) {
|
||
throw new Error('目标标签页已关闭,无法继续等待页面加载完成。');
|
||
}
|
||
if (tab.status === 'complete') {
|
||
return tab;
|
||
}
|
||
await sleepWithStop(retryDelayMs);
|
||
}
|
||
}
|
||
|
||
async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
|
||
return tabRuntime.ensureContentScriptReadyOnTab(source, tabId, options);
|
||
}
|
||
|
||
function isContentScriptReadyPong(source, pong) {
|
||
if (!pong?.ok) return false;
|
||
if (pong.source && pong.source !== source) return false;
|
||
if (source === 'plus-checkout') {
|
||
return Boolean(pong.plusCheckoutReady);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function isUnrecoverableContentScriptInjectError(error) {
|
||
return /Could not load file/i.test(String(error?.message || error || ''));
|
||
}
|
||
|
||
async function ensureContentScriptReadyOnTabUntilStopped(source, tabId, options = {}) {
|
||
const {
|
||
inject = null,
|
||
injectSource = null,
|
||
retryDelayMs = 700,
|
||
logMessage = '',
|
||
} = options;
|
||
let logged = false;
|
||
|
||
while (true) {
|
||
throwIfStopped();
|
||
const pong = await pingContentScriptOnTab(tabId);
|
||
if (isContentScriptReadyPong(source, pong)) {
|
||
await registerTab(source, tabId);
|
||
return;
|
||
}
|
||
|
||
if (!inject || !inject.length) {
|
||
throw new Error(`${getSourceLabel(source)} 内容脚本未就绪,且未提供可用的注入文件。`);
|
||
}
|
||
|
||
try {
|
||
if (injectSource) {
|
||
await chrome.scripting.executeScript({
|
||
target: { tabId },
|
||
func: (injectedSource) => {
|
||
window.__MULTIPAGE_SOURCE = injectedSource;
|
||
},
|
||
args: [injectSource],
|
||
});
|
||
}
|
||
await chrome.scripting.executeScript({
|
||
target: { tabId },
|
||
files: inject,
|
||
});
|
||
} catch (error) {
|
||
console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTabUntilStopped] inject failed for ${source}:`, error?.message || error);
|
||
if (isUnrecoverableContentScriptInjectError(error)) {
|
||
throw new Error(`${getSourceLabel(source)} 内容脚本文件加载失败:${error?.message || error}。请在扩展管理页重新加载当前扩展,确认文件已包含在已加载的扩展目录中。`);
|
||
}
|
||
}
|
||
|
||
const pongAfterInject = await pingContentScriptOnTab(tabId);
|
||
if (isContentScriptReadyPong(source, pongAfterInject)) {
|
||
await registerTab(source, tabId);
|
||
return;
|
||
}
|
||
|
||
if (logMessage && !logged) {
|
||
logged = true;
|
||
await addLog(logMessage, 'warn');
|
||
}
|
||
await sleepWithStop(retryDelayMs);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// Command Queue (for content scripts not yet ready)
|
||
// ============================================================
|
||
|
||
const pendingCommands = new Map(); // source -> { message, resolve, reject, timer }
|
||
|
||
function getContentScriptResponseTimeoutMs(message) {
|
||
return tabRuntime.getContentScriptResponseTimeoutMs(message);
|
||
}
|
||
|
||
function getMessageDebugLabel(source, message, tabId = null) {
|
||
return tabRuntime.getMessageDebugLabel(source, message, tabId);
|
||
}
|
||
|
||
function summarizeMessageResultForDebug(result) {
|
||
return tabRuntime.summarizeMessageResultForDebug(result);
|
||
}
|
||
|
||
function sendTabMessageWithTimeout(tabId, source, message, responseTimeoutMs = getContentScriptResponseTimeoutMs(message)) {
|
||
return tabRuntime.sendTabMessageWithTimeout(tabId, source, message, responseTimeoutMs);
|
||
}
|
||
|
||
async function sendTabMessageUntilStopped(tabId, source, message, options = {}) {
|
||
const retryDelayMs = Math.max(100, Math.floor(Number(options.retryDelayMs) || 300));
|
||
while (true) {
|
||
throwIfStopped();
|
||
try {
|
||
return await chrome.tabs.sendMessage(tabId, message);
|
||
} catch (error) {
|
||
if (!isRetryableContentScriptTransportError(error)) {
|
||
throw error;
|
||
}
|
||
await sleepWithStop(retryDelayMs);
|
||
}
|
||
}
|
||
}
|
||
|
||
function queueCommand(source, message, timeout = 15000) {
|
||
return tabRuntime.queueCommand(source, message, timeout);
|
||
}
|
||
|
||
function flushCommand(source, tabId) {
|
||
return tabRuntime.flushCommand(source, tabId);
|
||
}
|
||
|
||
function cancelPendingCommands(reason = STOP_ERROR_MESSAGE) {
|
||
return tabRuntime.cancelPendingCommands(reason);
|
||
}
|
||
|
||
// ============================================================
|
||
// Reuse or create tab
|
||
// ============================================================
|
||
|
||
async function reuseOrCreateTab(source, url, options = {}) {
|
||
return tabRuntime.reuseOrCreateTab(source, url, options);
|
||
}
|
||
|
||
// ============================================================
|
||
// Send command to content script (with readiness check)
|
||
// ============================================================
|
||
|
||
async function sendToContentScript(source, message, options = {}) {
|
||
return tabRuntime.sendToContentScript(source, message, options);
|
||
}
|
||
|
||
async function sendToContentScriptResilient(source, message, options = {}) {
|
||
return tabRuntime.sendToContentScriptResilient(source, message, options);
|
||
}
|
||
|
||
async function sendToMailContentScriptResilient(mail, message, options = {}) {
|
||
return tabRuntime.sendToMailContentScriptResilient(mail, message, options);
|
||
}
|
||
|
||
// ============================================================
|
||
// Logging
|
||
// ============================================================
|
||
|
||
async function addLog(message, level = 'info') {
|
||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.addLog) {
|
||
return loggingStatus.addLog(message, level);
|
||
}
|
||
const state = await getState();
|
||
const logs = state.logs || [];
|
||
const entry = { message, level, timestamp: Date.now() };
|
||
logs.push(entry);
|
||
if (logs.length > 500) logs.splice(0, logs.length - 500);
|
||
await setState({ logs });
|
||
chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => { });
|
||
}
|
||
|
||
function getStep8CallbackUrlFromNavigation(details, signupTabId) {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.getStep8CallbackUrlFromNavigation) {
|
||
return navigationUtils.getStep8CallbackUrlFromNavigation(details, signupTabId);
|
||
}
|
||
if (!Number.isInteger(signupTabId) || !details) return '';
|
||
if (details.tabId !== signupTabId) return '';
|
||
if (details.frameId !== 0) return '';
|
||
return isLocalhostOAuthCallbackUrl(details.url) ? details.url : '';
|
||
}
|
||
|
||
function getStep8CallbackUrlFromTabUpdate(tabId, changeInfo, tab, signupTabId) {
|
||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.getStep8CallbackUrlFromTabUpdate) {
|
||
return navigationUtils.getStep8CallbackUrlFromTabUpdate(tabId, changeInfo, tab, signupTabId);
|
||
}
|
||
if (!Number.isInteger(signupTabId) || tabId !== signupTabId) return '';
|
||
const candidates = [changeInfo?.url, tab?.url];
|
||
for (const candidate of candidates) {
|
||
if (isLocalhostOAuthCallbackUrl(candidate)) return candidate;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function getSourceLabel(source) {
|
||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.getSourceLabel) {
|
||
return loggingStatus.getSourceLabel(source);
|
||
}
|
||
const labels = {
|
||
'gmail-mail': 'Gmail 邮箱',
|
||
'sidepanel': '侧边栏',
|
||
'signup-page': '认证页',
|
||
'vps-panel': 'CPA 面板',
|
||
'sub2api-panel': 'SUB2API 后台',
|
||
'qq-mail': 'QQ 邮箱',
|
||
'mail-163': '163 邮箱',
|
||
'mail-2925': '2925 邮箱',
|
||
'inbucket-mail': 'Inbucket 邮箱',
|
||
'duck-mail': 'Duck 邮箱',
|
||
'hotmail-api': 'Hotmail(API对接/本地助手)',
|
||
'luckmail-api': 'LuckMail(API 购邮)',
|
||
'cloudflare-temp-email': 'Cloudflare Temp Email',
|
||
'plus-checkout': 'Plus Checkout',
|
||
'paypal-flow': 'PayPal 授权页',
|
||
};
|
||
return labels[source] || source || '未知来源';
|
||
}
|
||
|
||
// ============================================================
|
||
// Step Status Management
|
||
// ============================================================
|
||
|
||
async function setStepStatus(step, status) {
|
||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.setStepStatus) {
|
||
return loggingStatus.setStepStatus(step, status);
|
||
}
|
||
const state = await getState();
|
||
const statuses = { ...state.stepStatuses };
|
||
statuses[step] = status;
|
||
await setState({ stepStatuses: statuses, currentStep: step });
|
||
chrome.runtime.sendMessage({
|
||
type: 'STEP_STATUS_CHANGED',
|
||
payload: { step, status },
|
||
}).catch(() => { });
|
||
}
|
||
|
||
function isStopError(error) {
|
||
const message = typeof error === 'string' ? error : error?.message;
|
||
return message === STOP_ERROR_MESSAGE;
|
||
}
|
||
|
||
function isRetryableContentScriptTransportError(error) {
|
||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||
return /back\/forward cache|message channel is closed|Receiving end does not exist|port closed before a response was received|A listener indicated an asynchronous response|did not respond in \d+s/i.test(message);
|
||
}
|
||
|
||
const navigationUtils = self.MultiPageBackgroundNavigationUtils?.createNavigationUtils({
|
||
DEFAULT_CODEX2API_URL,
|
||
DEFAULT_SUB2API_URL,
|
||
normalizeLocalCpaStep9Mode,
|
||
});
|
||
|
||
const loggingStatus = self.MultiPageBackgroundLoggingStatus?.createLoggingStatus({
|
||
chrome,
|
||
DEFAULT_STATE,
|
||
getState,
|
||
isRecoverableStep9AuthFailure,
|
||
LOG_PREFIX,
|
||
setState,
|
||
STOP_ERROR_MESSAGE,
|
||
});
|
||
|
||
const tabRuntime = self.MultiPageBackgroundTabRuntime?.createTabRuntime({
|
||
addLog,
|
||
chrome,
|
||
getSourceLabel,
|
||
getState,
|
||
isLocalhostOAuthCallbackUrl,
|
||
isRetryableContentScriptTransportError,
|
||
LOG_PREFIX,
|
||
matchesSourceUrlFamily,
|
||
setState,
|
||
sleepWithStop,
|
||
STOP_ERROR_MESSAGE,
|
||
throwIfStopped,
|
||
});
|
||
|
||
function getErrorMessage(error) {
|
||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.getErrorMessage) {
|
||
return loggingStatus.getErrorMessage(error);
|
||
}
|
||
return String(typeof error === 'string' ? error : error?.message || '');
|
||
}
|
||
|
||
function isCloudflareSecurityBlockedError(error) {
|
||
return getErrorMessage(error).startsWith(CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX);
|
||
}
|
||
|
||
function isTerminalSecurityBlockedError(error) {
|
||
return isCloudflareSecurityBlockedError(error);
|
||
}
|
||
|
||
function getCloudflareSecurityBlockedMessage(error) {
|
||
const message = getErrorMessage(error);
|
||
if (message.startsWith(CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX)) {
|
||
return message.slice(CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX.length).trim() || CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE;
|
||
}
|
||
return CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE;
|
||
}
|
||
|
||
function getTerminalSecurityBlockedMessage(error) {
|
||
return getCloudflareSecurityBlockedMessage(error);
|
||
}
|
||
|
||
function getTerminalSecurityBlockedAlertText(error) {
|
||
return '检测到 Cloudflare 风控,请暂停当前操作。';
|
||
}
|
||
|
||
function getTerminalSecurityBlockedTitle(error) {
|
||
return 'Cloudflare 风控拦截';
|
||
}
|
||
|
||
function isBrowserSwitchRequiredError(error) {
|
||
return getErrorMessage(error).startsWith(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX);
|
||
}
|
||
|
||
function getBrowserSwitchRequiredMessage(error) {
|
||
const message = getErrorMessage(error);
|
||
return message.startsWith(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX)
|
||
? message.slice(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX.length).trim()
|
||
: message;
|
||
}
|
||
|
||
function broadcastSecurityBlockedAlert(title = '流程已完全停止', message = CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE, alertText = '检测到 Cloudflare 风控,请暂停当前操作。') {
|
||
chrome.runtime.sendMessage({
|
||
type: 'SECURITY_BLOCKED_ALERT',
|
||
payload: {
|
||
title,
|
||
message,
|
||
alert: {
|
||
text: alertText,
|
||
tone: 'danger',
|
||
},
|
||
},
|
||
}).catch(() => { });
|
||
}
|
||
|
||
async function handleCloudflareSecurityBlocked(error) {
|
||
const title = getTerminalSecurityBlockedTitle(error);
|
||
const message = getTerminalSecurityBlockedMessage(error);
|
||
const alertText = getTerminalSecurityBlockedAlertText(error);
|
||
await requestStop({ logMessage: message });
|
||
broadcastSecurityBlockedAlert(title, message, alertText);
|
||
return message;
|
||
}
|
||
|
||
async function handleBrowserSwitchRequired(error) {
|
||
const message = getBrowserSwitchRequiredMessage(error)
|
||
|| '检测到第 10 步的特殊冲突状态,请更换浏览器后重新进行注册登录。';
|
||
await requestStop({ logMessage: message });
|
||
return message;
|
||
}
|
||
|
||
function isVerificationMailPollingError(error) {
|
||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.isVerificationMailPollingError) {
|
||
return loggingStatus.isVerificationMailPollingError(error);
|
||
}
|
||
const message = getErrorMessage(error);
|
||
return /未在 .*邮箱中找到新的匹配邮件|未在 Hotmail 收件箱中找到新的匹配验证码|邮箱轮询结束,但未获取到验证码|无法获取新的(?:注册|登录)验证码|页面未能重新就绪|页面通信异常|did not respond in \d+s/i.test(message);
|
||
}
|
||
|
||
function isAddPhoneAuthFailure(error) {
|
||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.isAddPhoneAuthFailure) {
|
||
return loggingStatus.isAddPhoneAuthFailure(error);
|
||
}
|
||
const message = getErrorMessage(error);
|
||
if (/\u624b\u673a\u53f7\u8f93\u5165\u6a21\u5f0f|phone\s+entry/i.test(message)) {
|
||
return false;
|
||
}
|
||
return /https:\/\/auth\.openai\.com\/add-phone(?:[/?#]|$)|\badd-phone\b|\u6dfb\u52a0\u624b\u673a\u53f7|\u624b\u673a\u53f7\u7801|\u8fdb\u5165\u624b\u673a\u53f7\u9875\u9762|\u624b\u673a\u53f7\u9875|\u624b\u673a\u53f7\u9875\u9762|phone\s+number|telephone/i.test(message);
|
||
}
|
||
|
||
function getLoginAuthStateLabel(state) {
|
||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.getLoginAuthStateLabel) {
|
||
return loggingStatus.getLoginAuthStateLabel(state);
|
||
}
|
||
switch (state) {
|
||
case 'verification_page': return '登录验证码页';
|
||
case 'password_page': return '密码页';
|
||
case 'email_page': return '邮箱输入页';
|
||
case 'login_timeout_error_page': return '登录超时报错页';
|
||
case 'oauth_consent_page': return 'OAuth 授权页';
|
||
case 'add_phone_page': return '手机号页';
|
||
default: return '未知页面';
|
||
}
|
||
}
|
||
|
||
function isRestartCurrentAttemptError(error) {
|
||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.isRestartCurrentAttemptError) {
|
||
return loggingStatus.isRestartCurrentAttemptError(error);
|
||
}
|
||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||
return /当前邮箱已存在,需要重新开始新一轮/.test(message);
|
||
}
|
||
|
||
function isSignupUserAlreadyExistsFailure(error) {
|
||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.isSignupUserAlreadyExistsFailure) {
|
||
return loggingStatus.isSignupUserAlreadyExistsFailure(error);
|
||
}
|
||
const message = getErrorMessage(error);
|
||
return /SIGNUP_USER_ALREADY_EXISTS::|user_already_exists/i.test(message);
|
||
}
|
||
|
||
function isStep9RecoverableAuthError(error) {
|
||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||
return /STEP9_OAUTH_RETRY::/i.test(message)
|
||
|| isRecoverableStep9AuthFailure(message);
|
||
}
|
||
|
||
function isLegacyStep9RecoverableAuthError(error) {
|
||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||
return /STEP9_OAUTH_TIMEOUT::|认证失败:\s*(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded)/i.test(message);
|
||
}
|
||
|
||
function isStepDoneStatus(status) {
|
||
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
|
||
}
|
||
|
||
function getFirstUnfinishedStep(statuses = {}, stateOverride = null) {
|
||
const state = stateOverride || {};
|
||
const activeStepIds = typeof getStepIdsForState === 'function'
|
||
? getStepIdsForState(state)
|
||
: (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length
|
||
? STEP_IDS
|
||
: Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1));
|
||
for (const step of activeStepIds) {
|
||
if (!isStepDoneStatus(statuses[step] || 'pending')) return step;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function hasSavedProgress(statuses = {}, stateOverride = null) {
|
||
const state = stateOverride || {};
|
||
const merged = { ...DEFAULT_STATE.stepStatuses, ...statuses };
|
||
const activeStepIds = typeof getStepIdsForState === 'function'
|
||
? getStepIdsForState(state)
|
||
: (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length
|
||
? STEP_IDS
|
||
: Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1));
|
||
return activeStepIds.some((step) => (merged[step] || 'pending') !== 'pending');
|
||
}
|
||
|
||
function getDownstreamStateResets(step, state = {}) {
|
||
const stepKey = getStepExecutionKeyForState(step, state);
|
||
const plusRuntimeResets = {
|
||
plusCheckoutTabId: null,
|
||
plusCheckoutUrl: null,
|
||
plusCheckoutCountry: 'DE',
|
||
plusCheckoutCurrency: 'EUR',
|
||
plusBillingCountryText: '',
|
||
plusBillingAddress: null,
|
||
plusPaypalApprovedAt: null,
|
||
plusReturnUrl: '',
|
||
plusManualConfirmationPending: false,
|
||
plusManualConfirmationRequestId: '',
|
||
plusManualConfirmationStep: 0,
|
||
plusManualConfirmationMethod: '',
|
||
plusManualConfirmationTitle: '',
|
||
plusManualConfirmationMessage: '',
|
||
};
|
||
|
||
if (step <= 1) {
|
||
return {
|
||
...plusRuntimeResets,
|
||
oauthUrl: null,
|
||
cpaOAuthState: null,
|
||
cpaManagementOrigin: null,
|
||
sub2apiSessionId: null,
|
||
sub2apiOAuthState: null,
|
||
sub2apiGroupId: null,
|
||
sub2apiDraftName: null,
|
||
codex2apiSessionId: null,
|
||
codex2apiOAuthState: null,
|
||
flowStartTime: null,
|
||
password: null,
|
||
lastEmailTimestamp: null,
|
||
signupVerificationRequestedAt: null,
|
||
loginVerificationRequestedAt: null,
|
||
oauthFlowDeadlineAt: null,
|
||
oauthFlowDeadlineSourceUrl: null,
|
||
pendingPhoneActivationConfirmation: null,
|
||
lastSignupCode: null,
|
||
lastLoginCode: null,
|
||
localhostUrl: null,
|
||
currentPhoneVerificationCode: '',
|
||
};
|
||
}
|
||
if (step === 2) {
|
||
return {
|
||
...plusRuntimeResets,
|
||
password: null,
|
||
lastEmailTimestamp: null,
|
||
signupVerificationRequestedAt: null,
|
||
loginVerificationRequestedAt: null,
|
||
oauthFlowDeadlineAt: null,
|
||
oauthFlowDeadlineSourceUrl: null,
|
||
pendingPhoneActivationConfirmation: null,
|
||
lastSignupCode: null,
|
||
lastLoginCode: null,
|
||
localhostUrl: null,
|
||
currentPhoneVerificationCode: '',
|
||
};
|
||
}
|
||
if (step === 3 || step === 4) {
|
||
return {
|
||
...plusRuntimeResets,
|
||
lastEmailTimestamp: null,
|
||
signupVerificationRequestedAt: null,
|
||
loginVerificationRequestedAt: null,
|
||
oauthFlowDeadlineAt: null,
|
||
oauthFlowDeadlineSourceUrl: null,
|
||
pendingPhoneActivationConfirmation: null,
|
||
lastSignupCode: null,
|
||
lastLoginCode: null,
|
||
localhostUrl: null,
|
||
currentPhoneVerificationCode: '',
|
||
};
|
||
}
|
||
if (step === 5 || step === 6 || step === 7 || step === 8) {
|
||
return {
|
||
...(step <= 6 ? plusRuntimeResets : {}),
|
||
...(step === 7 ? {
|
||
plusBillingCountryText: '',
|
||
plusBillingAddress: null,
|
||
plusPaypalApprovedAt: null,
|
||
plusReturnUrl: '',
|
||
plusManualConfirmationPending: false,
|
||
plusManualConfirmationRequestId: '',
|
||
plusManualConfirmationStep: 0,
|
||
plusManualConfirmationMethod: '',
|
||
plusManualConfirmationTitle: '',
|
||
plusManualConfirmationMessage: '',
|
||
} : {}),
|
||
...(step === 8 ? {
|
||
plusPaypalApprovedAt: null,
|
||
plusReturnUrl: '',
|
||
} : {}),
|
||
lastLoginCode: null,
|
||
loginVerificationRequestedAt: null,
|
||
oauthFlowDeadlineAt: null,
|
||
oauthFlowDeadlineSourceUrl: null,
|
||
pendingPhoneActivationConfirmation: null,
|
||
localhostUrl: null,
|
||
currentPhoneVerificationCode: '',
|
||
};
|
||
}
|
||
if (step === 9) {
|
||
return {
|
||
pendingPhoneActivationConfirmation: null,
|
||
plusReturnUrl: '',
|
||
localhostUrl: null,
|
||
currentPhoneVerificationCode: '',
|
||
};
|
||
}
|
||
if (stepKey === 'oauth-login' || stepKey === 'fetch-login-code') {
|
||
return {
|
||
lastLoginCode: null,
|
||
loginVerificationRequestedAt: null,
|
||
oauthFlowDeadlineAt: null,
|
||
oauthFlowDeadlineSourceUrl: null,
|
||
pendingPhoneActivationConfirmation: null,
|
||
localhostUrl: null,
|
||
currentPhoneVerificationCode: '',
|
||
};
|
||
}
|
||
if (stepKey === 'confirm-oauth') {
|
||
return {
|
||
pendingPhoneActivationConfirmation: null,
|
||
localhostUrl: null,
|
||
};
|
||
}
|
||
return {};
|
||
}
|
||
|
||
async function invalidateDownstreamAfterStepRestart(step, options = {}) {
|
||
const { logLabel = `步骤 ${step} 重新执行` } = options;
|
||
const state = await getState();
|
||
const statuses = { ...(state.stepStatuses || {}) };
|
||
const changedSteps = [];
|
||
|
||
const activeStepIds = typeof getStepIdsForState === 'function'
|
||
? getStepIdsForState(state)
|
||
: (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length
|
||
? STEP_IDS
|
||
: Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1));
|
||
for (const downstream of activeStepIds) {
|
||
if (downstream <= step) {
|
||
continue;
|
||
}
|
||
if (statuses[downstream] !== 'pending') {
|
||
statuses[downstream] = 'pending';
|
||
changedSteps.push(downstream);
|
||
}
|
||
}
|
||
|
||
if (changedSteps.length) {
|
||
await setState({ stepStatuses: statuses });
|
||
for (const downstream of changedSteps) {
|
||
chrome.runtime.sendMessage({
|
||
type: 'STEP_STATUS_CHANGED',
|
||
payload: { step: downstream, status: 'pending' },
|
||
}).catch(() => { });
|
||
}
|
||
await addLog(`${logLabel},已重置后续步骤状态:${changedSteps.join(', ')}`, 'warn');
|
||
}
|
||
|
||
const resets = getDownstreamStateResets(step, state);
|
||
if (Object.keys(resets).length) {
|
||
await setState(resets);
|
||
broadcastDataUpdate(resets);
|
||
}
|
||
}
|
||
|
||
function clearStopRequest() {
|
||
stopRequested = false;
|
||
}
|
||
|
||
function getRunningSteps(statuses = {}, stateOverride = null) {
|
||
const state = stateOverride || {};
|
||
const merged = { ...DEFAULT_STATE.stepStatuses, ...statuses };
|
||
const activeStepIds = typeof getStepIdsForState === 'function'
|
||
? getStepIdsForState(state)
|
||
: (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length
|
||
? STEP_IDS
|
||
: Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1));
|
||
return activeStepIds
|
||
.filter((step) => merged[step] === 'running')
|
||
.sort((a, b) => a - b);
|
||
}
|
||
|
||
function inferStoppedRecordStep(state = {}) {
|
||
const statuses = { ...DEFAULT_STATE.stepStatuses, ...(state?.stepStatuses || {}) };
|
||
const stepIds = typeof getStepIdsForState === 'function'
|
||
? getStepIdsForState(state)
|
||
: (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length
|
||
? STEP_IDS
|
||
: Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1));
|
||
|
||
const runningSteps = stepIds.filter((step) => statuses[step] === 'running');
|
||
if (runningSteps.length) {
|
||
return runningSteps[0];
|
||
}
|
||
|
||
const hasProgress = stepIds.some((step) => statuses[step] !== 'pending');
|
||
if (!hasProgress) {
|
||
return null;
|
||
}
|
||
|
||
for (const step of stepIds) {
|
||
const status = statuses[step] || 'pending';
|
||
if (!(status === 'completed' || status === 'manual_completed' || status === 'skipped')) {
|
||
return step;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function resolveAccountRunRecordStatusForStop(status, state = {}) {
|
||
const normalizedStatus = String(status || '').trim().toLowerCase();
|
||
if (normalizedStatus === 'stopped') {
|
||
const inferredStep = inferStoppedRecordStep(state);
|
||
if (Number.isInteger(inferredStep) && inferredStep > 0) {
|
||
return `step${inferredStep}_stopped`;
|
||
}
|
||
}
|
||
return status;
|
||
}
|
||
|
||
function extractStoppedStepFromRecordStatus(status = '') {
|
||
const match = String(status || '').trim().toLowerCase().match(/^step(\d+)_stopped$/);
|
||
if (!match) {
|
||
return null;
|
||
}
|
||
const step = Number(match[1]);
|
||
return Number.isInteger(step) && step > 0 ? step : null;
|
||
}
|
||
|
||
function resolveAccountRunRecordReasonForStop(status, reason = '') {
|
||
const text = String(reason || '').trim();
|
||
const stoppedStep = extractStoppedStepFromRecordStatus(status);
|
||
|
||
if (!stoppedStep) {
|
||
if (!text || text === STOP_ERROR_MESSAGE || /^流程已被用户停止。?$/.test(text)) {
|
||
return '流程已停止。';
|
||
}
|
||
return text;
|
||
}
|
||
|
||
if (!text || text === STOP_ERROR_MESSAGE || /^流程已被用户停止。?$/.test(text)) {
|
||
return `步骤 ${stoppedStep} 已被用户停止。`;
|
||
}
|
||
|
||
if (/流程尚未完成/.test(text) || /已使用邮箱/.test(text)) {
|
||
return `步骤 ${stoppedStep} 已停止:邮箱已设置,流程尚未完成。`;
|
||
}
|
||
|
||
if (/步骤\s*\d+\s*已(?:被用户)?停止/.test(text)) {
|
||
return text.replace(/步骤\s*\d+/, `步骤 ${stoppedStep}`);
|
||
}
|
||
|
||
return text;
|
||
}
|
||
|
||
function getAutoRunStatusPayload(phase, payload = {}) {
|
||
const normalizedPayload = {
|
||
...payload,
|
||
currentRun: payload.currentRun ?? autoRunCurrentRun,
|
||
totalRuns: payload.totalRuns ?? autoRunTotalRuns,
|
||
attemptRun: payload.attemptRun ?? autoRunAttemptRun,
|
||
sessionId: payload.sessionId ?? payload.autoRunSessionId ?? autoRunSessionId,
|
||
};
|
||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.getAutoRunStatusPayload) {
|
||
return loggingStatus.getAutoRunStatusPayload(phase, normalizedPayload);
|
||
}
|
||
return {
|
||
autoRunning: phase === 'scheduled'
|
||
|| phase === 'running'
|
||
|| phase === 'waiting_step'
|
||
|| phase === 'waiting_email'
|
||
|| phase === 'retrying'
|
||
|| phase === 'waiting_interval',
|
||
autoRunPhase: phase,
|
||
autoRunCurrentRun: normalizedPayload.currentRun ?? 0,
|
||
autoRunTotalRuns: normalizedPayload.totalRuns ?? 1,
|
||
autoRunAttemptRun: normalizedPayload.attemptRun ?? 0,
|
||
autoRunSessionId: normalizeAutoRunSessionId(normalizedPayload.sessionId),
|
||
scheduledAutoRunAt: Number.isFinite(Number(normalizedPayload.scheduledAt)) ? Number(normalizedPayload.scheduledAt) : null,
|
||
autoRunCountdownAt: Number.isFinite(Number(normalizedPayload.countdownAt)) ? Number(normalizedPayload.countdownAt) : null,
|
||
autoRunCountdownTitle: normalizedPayload.countdownTitle === undefined ? '' : String(normalizedPayload.countdownTitle || ''),
|
||
autoRunCountdownNote: normalizedPayload.countdownNote === undefined ? '' : String(normalizedPayload.countdownNote || ''),
|
||
};
|
||
}
|
||
|
||
async function broadcastAutoRunStatus(phase, payload = {}, extraState = {}) {
|
||
const rawScheduledAt = phase === 'scheduled'
|
||
? (payload.scheduledAt ?? payload.scheduledAutoRunAt ?? null)
|
||
: null;
|
||
const rawCountdownAt = payload.countdownAt ?? payload.autoRunCountdownAt ?? null;
|
||
const statusPayload = {
|
||
phase,
|
||
currentRun: payload.currentRun ?? autoRunCurrentRun,
|
||
totalRuns: payload.totalRuns ?? autoRunTotalRuns,
|
||
attemptRun: payload.attemptRun ?? autoRunAttemptRun,
|
||
sessionId: payload.sessionId ?? payload.autoRunSessionId ?? autoRunSessionId,
|
||
scheduledAt: rawScheduledAt === null ? null : Number(rawScheduledAt),
|
||
countdownAt: rawCountdownAt === null ? null : Number(rawCountdownAt),
|
||
countdownTitle: payload.countdownTitle === undefined ? '' : String(payload.countdownTitle || ''),
|
||
countdownNote: payload.countdownNote === undefined ? '' : String(payload.countdownNote || ''),
|
||
};
|
||
|
||
await setState({
|
||
...extraState,
|
||
...getAutoRunStatusPayload(phase, statusPayload),
|
||
});
|
||
chrome.runtime.sendMessage({
|
||
type: 'AUTO_RUN_STATUS',
|
||
payload: statusPayload,
|
||
}).catch(() => { });
|
||
}
|
||
|
||
function isAutoRunLockedState(state) {
|
||
return Boolean(state.autoRunning)
|
||
&& (
|
||
state.autoRunPhase === 'running'
|
||
|| state.autoRunPhase === 'waiting_step'
|
||
|| state.autoRunPhase === 'retrying'
|
||
|| state.autoRunPhase === 'waiting_interval'
|
||
);
|
||
}
|
||
|
||
function isAutoRunPausedState(state) {
|
||
return Boolean(state.autoRunning) && state.autoRunPhase === 'waiting_email';
|
||
}
|
||
|
||
function isAutoRunScheduledState(state) {
|
||
const plan = normalizeAutoRunTimerPlanFromState(state);
|
||
const scheduledAt = state.scheduledAutoRunAt === null ? null : Number(state.scheduledAutoRunAt);
|
||
return Boolean(state.autoRunning)
|
||
&& state.autoRunPhase === 'scheduled'
|
||
&& Number.isFinite(scheduledAt)
|
||
&& plan?.kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START;
|
||
}
|
||
|
||
function getPendingAutoRunTimerPlan(state = {}) {
|
||
return normalizeAutoRunTimerPlanFromState(state);
|
||
}
|
||
|
||
function formatAutoRunScheduleTime(timestamp) {
|
||
return new Date(timestamp).toLocaleString('zh-CN', {
|
||
hour12: false,
|
||
timeZone: DISPLAY_TIMEZONE,
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
});
|
||
}
|
||
|
||
async function setAutoRunDelayEnabledState(enabled) {
|
||
const normalized = Boolean(enabled);
|
||
await setPersistentSettings({ autoRunDelayEnabled: normalized });
|
||
await setState({ autoRunDelayEnabled: normalized });
|
||
broadcastDataUpdate({ autoRunDelayEnabled: normalized });
|
||
}
|
||
|
||
async function ensureAutoRunTimerAlarm(fireAt) {
|
||
if (!Number.isFinite(fireAt) || fireAt <= Date.now()) {
|
||
return false;
|
||
}
|
||
|
||
const existingAlarm = await chrome.alarms.get(AUTO_RUN_TIMER_ALARM_NAME);
|
||
if (!existingAlarm || Math.abs((existingAlarm.scheduledTime || 0) - fireAt) > 1000) {
|
||
await chrome.alarms.clear(AUTO_RUN_TIMER_ALARM_NAME);
|
||
await chrome.alarms.create(AUTO_RUN_TIMER_ALARM_NAME, { when: fireAt });
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
async function clearAutoRunTimerAlarm() {
|
||
await chrome.alarms.clear(AUTO_RUN_TIMER_ALARM_NAME);
|
||
}
|
||
|
||
async function persistAutoRunTimerPlan(plan, extraState = {}) {
|
||
const normalizedPlan = normalizeAutoRunTimerPlan(plan);
|
||
if (!normalizedPlan) {
|
||
throw new Error('自动运行计时计划无效。');
|
||
}
|
||
|
||
const statusPayload = getAutoRunTimerStatusPayload(normalizedPlan);
|
||
await broadcastAutoRunStatus(
|
||
statusPayload.phase,
|
||
statusPayload,
|
||
{
|
||
...extraState,
|
||
autoRunTimerPlan: normalizedPlan,
|
||
scheduledAutoRunPlan: null,
|
||
}
|
||
);
|
||
await ensureAutoRunTimerAlarm(normalizedPlan.fireAt);
|
||
return normalizedPlan;
|
||
}
|
||
|
||
function getAutoRunTimerResumeOptions(plan) {
|
||
const normalizedPlan = normalizeAutoRunTimerPlan(plan);
|
||
if (!normalizedPlan) {
|
||
return null;
|
||
}
|
||
|
||
if (normalizedPlan.kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START) {
|
||
return {
|
||
loopOptions: {
|
||
autoRunSessionId: normalizedPlan.autoRunSessionId,
|
||
autoRunSkipFailures: normalizedPlan.autoRunSkipFailures,
|
||
mode: normalizedPlan.mode,
|
||
},
|
||
statusPayload: {
|
||
currentRun: 0,
|
||
totalRuns: normalizedPlan.totalRuns,
|
||
attemptRun: 0,
|
||
sessionId: normalizedPlan.autoRunSessionId,
|
||
},
|
||
};
|
||
}
|
||
|
||
if (normalizedPlan.kind === AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS) {
|
||
const nextRun = Math.min(normalizedPlan.currentRun + 1, normalizedPlan.totalRuns);
|
||
return {
|
||
loopOptions: {
|
||
autoRunSessionId: normalizedPlan.autoRunSessionId,
|
||
autoRunSkipFailures: normalizedPlan.autoRunSkipFailures,
|
||
mode: 'restart',
|
||
resumeCurrentRun: nextRun,
|
||
resumeAttemptRun: 1,
|
||
resumeRoundSummaries: normalizedPlan.roundSummaries,
|
||
},
|
||
statusPayload: {
|
||
currentRun: nextRun,
|
||
totalRuns: normalizedPlan.totalRuns,
|
||
attemptRun: 1,
|
||
sessionId: normalizedPlan.autoRunSessionId,
|
||
},
|
||
};
|
||
}
|
||
|
||
return {
|
||
loopOptions: {
|
||
autoRunSessionId: normalizedPlan.autoRunSessionId,
|
||
autoRunSkipFailures: normalizedPlan.autoRunSkipFailures,
|
||
mode: 'restart',
|
||
resumeCurrentRun: normalizedPlan.currentRun,
|
||
resumeAttemptRun: normalizedPlan.attemptRun,
|
||
resumeRoundSummaries: normalizedPlan.roundSummaries,
|
||
},
|
||
statusPayload: {
|
||
currentRun: normalizedPlan.currentRun,
|
||
totalRuns: normalizedPlan.totalRuns,
|
||
attemptRun: normalizedPlan.attemptRun,
|
||
sessionId: normalizedPlan.autoRunSessionId,
|
||
},
|
||
};
|
||
}
|
||
|
||
let autoRunTimerLaunching = false;
|
||
|
||
async function launchAutoRunTimerPlan(trigger = 'alarm', options = {}) {
|
||
const { expectedKinds = [] } = options;
|
||
if (autoRunTimerLaunching) {
|
||
return false;
|
||
}
|
||
|
||
autoRunTimerLaunching = true;
|
||
try {
|
||
const state = await getState();
|
||
const plan = getPendingAutoRunTimerPlan(state);
|
||
if (!plan) {
|
||
return false;
|
||
}
|
||
if (expectedKinds.length && !expectedKinds.includes(plan.kind)) {
|
||
return false;
|
||
}
|
||
if (autoRunActive) {
|
||
return false;
|
||
}
|
||
if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) {
|
||
return false;
|
||
}
|
||
|
||
const resumeOptions = getAutoRunTimerResumeOptions(plan);
|
||
if (!resumeOptions) {
|
||
await clearAutoRunTimerAlarm();
|
||
await broadcastAutoRunStatus('idle', {
|
||
currentRun: 0,
|
||
totalRuns: 1,
|
||
attemptRun: 0,
|
||
}, {
|
||
autoRunRoundSummaries: [],
|
||
autoRunTimerPlan: null,
|
||
scheduledAutoRunPlan: null,
|
||
});
|
||
return false;
|
||
}
|
||
|
||
await clearAutoRunTimerAlarm();
|
||
if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) {
|
||
return false;
|
||
}
|
||
autoRunCurrentRun = resumeOptions.statusPayload.currentRun;
|
||
autoRunTotalRuns = plan.totalRuns;
|
||
autoRunAttemptRun = resumeOptions.statusPayload.attemptRun;
|
||
autoRunSessionId = normalizeAutoRunSessionId(plan.autoRunSessionId);
|
||
if (plan.kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START && trigger !== 'manual' && state.autoRunDelayEnabled) {
|
||
await setAutoRunDelayEnabledState(false);
|
||
}
|
||
await broadcastAutoRunStatus(
|
||
'running',
|
||
resumeOptions.statusPayload,
|
||
{
|
||
autoRunSkipFailures: plan.autoRunSkipFailures,
|
||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(plan.totalRuns, plan.roundSummaries),
|
||
autoRunTimerPlan: null,
|
||
scheduledAutoRunPlan: null,
|
||
}
|
||
);
|
||
|
||
if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) {
|
||
return false;
|
||
}
|
||
clearStopRequest();
|
||
let logMessage = '倒计时结束,自动运行开始执行。';
|
||
if (plan.kind === AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS) {
|
||
logMessage = trigger === 'manual'
|
||
? '已手动跳过线程间隔,自动流程立即开始下一轮。'
|
||
: '线程间隔结束,自动流程开始下一轮。';
|
||
} else if (plan.kind === AUTO_RUN_TIMER_KIND_BEFORE_RETRY) {
|
||
logMessage = trigger === 'manual'
|
||
? `已手动跳过线程间隔,立即开始第 ${plan.currentRun}/${plan.totalRuns} 轮第 ${plan.attemptRun} 次尝试。`
|
||
: `线程间隔结束,开始第 ${plan.currentRun}/${plan.totalRuns} 轮第 ${plan.attemptRun} 次尝试。`;
|
||
} else if (trigger === 'manual') {
|
||
logMessage = '已手动跳过倒计时,自动运行立即开始。';
|
||
}
|
||
await addLog(logMessage, 'info');
|
||
if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) {
|
||
return false;
|
||
}
|
||
|
||
startAutoRunLoop(plan.totalRuns, resumeOptions.loopOptions);
|
||
return true;
|
||
} finally {
|
||
autoRunTimerLaunching = false;
|
||
}
|
||
}
|
||
|
||
async function scheduleAutoRun(totalRuns, options = {}) {
|
||
const state = await getState();
|
||
if (isAutoRunLockedState(state) || isAutoRunPausedState(state) || autoRunActive) {
|
||
throw new Error('自动运行已在进行中,请先停止后再重新计划。');
|
||
}
|
||
if (getPendingAutoRunTimerPlan(state)) {
|
||
throw new Error('已有自动运行倒计时计划,请先取消或立即开始。');
|
||
}
|
||
|
||
const delayMinutes = normalizeAutoRunDelayMinutes(options.delayMinutes);
|
||
const sessionId = createAutoRunSessionId();
|
||
const timerPlan = normalizeAutoRunTimerPlan({
|
||
kind: AUTO_RUN_TIMER_KIND_SCHEDULED_START,
|
||
fireAt: Date.now() + delayMinutes * 60 * 1000,
|
||
totalRuns,
|
||
autoRunSkipFailures: options.autoRunSkipFailures,
|
||
autoRunSessionId: sessionId,
|
||
mode: options.mode,
|
||
});
|
||
|
||
autoRunCurrentRun = 0;
|
||
autoRunTotalRuns = timerPlan.totalRuns;
|
||
autoRunAttemptRun = 0;
|
||
autoRunSessionId = sessionId;
|
||
|
||
await persistAutoRunTimerPlan(timerPlan, {
|
||
autoRunSkipFailures: timerPlan.autoRunSkipFailures,
|
||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(timerPlan.totalRuns, []),
|
||
});
|
||
await addLog(
|
||
`自动运行已计划:${delayMinutes} 分钟后启动(${formatAutoRunScheduleTime(timerPlan.fireAt)}),目标 ${timerPlan.totalRuns} 轮。`,
|
||
'info'
|
||
);
|
||
return { ok: true, scheduledAt: timerPlan.fireAt };
|
||
}
|
||
|
||
async function cancelScheduledAutoRun(options = {}) {
|
||
const state = await getState();
|
||
const plan = getPendingAutoRunTimerPlan(state);
|
||
if (!plan || plan.kind !== AUTO_RUN_TIMER_KIND_SCHEDULED_START) {
|
||
return false;
|
||
}
|
||
|
||
autoRunCurrentRun = 0;
|
||
autoRunTotalRuns = plan.totalRuns;
|
||
autoRunAttemptRun = 0;
|
||
clearCurrentAutoRunSessionId(plan.autoRunSessionId);
|
||
await broadcastAutoRunStatus(
|
||
'idle',
|
||
{
|
||
currentRun: 0,
|
||
totalRuns: plan.totalRuns,
|
||
attemptRun: 0,
|
||
sessionId: 0,
|
||
},
|
||
{
|
||
autoRunSessionId: 0,
|
||
autoRunRoundSummaries: [],
|
||
autoRunTimerPlan: null,
|
||
scheduledAutoRunPlan: null,
|
||
}
|
||
);
|
||
await clearAutoRunTimerAlarm();
|
||
if (options.logMessage !== false) {
|
||
await addLog(options.logMessage || '已取消自动运行倒计时计划。', 'warn');
|
||
}
|
||
return true;
|
||
}
|
||
|
||
async function restoreAutoRunTimerIfNeeded() {
|
||
const state = await getState();
|
||
let plan = getPendingAutoRunTimerPlan(state);
|
||
if (!plan) {
|
||
clearCurrentAutoRunSessionId();
|
||
if (state.autoRunPhase === 'scheduled' || state.autoRunPhase === 'waiting_interval') {
|
||
await clearAutoRunTimerAlarm();
|
||
await broadcastAutoRunStatus('idle', {
|
||
currentRun: 0,
|
||
totalRuns: 1,
|
||
attemptRun: 0,
|
||
sessionId: 0,
|
||
}, {
|
||
autoRunSessionId: 0,
|
||
autoRunRoundSummaries: [],
|
||
autoRunTimerPlan: null,
|
||
scheduledAutoRunPlan: null,
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (!plan.autoRunSessionId) {
|
||
const restoredSessionId = createAutoRunSessionId();
|
||
plan = await persistAutoRunTimerPlan({
|
||
...plan,
|
||
autoRunSessionId: restoredSessionId,
|
||
}, {
|
||
autoRunSkipFailures: plan.autoRunSkipFailures,
|
||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(plan.totalRuns, plan.roundSummaries),
|
||
});
|
||
} else {
|
||
setCurrentAutoRunSessionId(plan.autoRunSessionId);
|
||
}
|
||
|
||
if (plan.fireAt <= Date.now()) {
|
||
await launchAutoRunTimerPlan('restore');
|
||
return;
|
||
}
|
||
|
||
const statusPayload = getAutoRunTimerStatusPayload(plan);
|
||
await broadcastAutoRunStatus(
|
||
statusPayload.phase,
|
||
statusPayload,
|
||
{
|
||
autoRunSessionId: plan.autoRunSessionId,
|
||
autoRunSkipFailures: plan.autoRunSkipFailures,
|
||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(plan.totalRuns, plan.roundSummaries),
|
||
autoRunTimerPlan: plan,
|
||
scheduledAutoRunPlan: null,
|
||
}
|
||
);
|
||
await ensureAutoRunTimerAlarm(plan.fireAt);
|
||
}
|
||
|
||
async function ensureManualInteractionAllowed(actionLabel) {
|
||
const state = await getState();
|
||
|
||
if (isAutoRunLockedState(state)) {
|
||
throw new Error(`自动流程运行中,请先停止后再${actionLabel}。`);
|
||
}
|
||
if (isAutoRunPausedState(state)) {
|
||
throw new Error(`自动流程当前已暂停。请点击“继续”,或先确认接管自动流程后再${actionLabel}。`);
|
||
}
|
||
if (isAutoRunScheduledState(state)) {
|
||
throw new Error(`自动流程已计划启动。请先取消计划,或立即开始后再${actionLabel}。`);
|
||
}
|
||
|
||
return state;
|
||
}
|
||
|
||
async function skipStep(step) {
|
||
const state = await ensureManualInteractionAllowed('跳过步骤');
|
||
const activeStepIds = typeof getStepIdsForState === 'function'
|
||
? getStepIdsForState(state)
|
||
: (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length
|
||
? STEP_IDS
|
||
: Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1));
|
||
|
||
if (!Number.isInteger(step) || !activeStepIds.includes(step)) {
|
||
throw new Error(`无效步骤:${step}`);
|
||
}
|
||
|
||
const statuses = { ...(state.stepStatuses || {}) };
|
||
const currentStatus = statuses[step];
|
||
if (currentStatus === 'running') {
|
||
throw new Error(`步骤 ${step} 正在运行中,不能跳过。`);
|
||
}
|
||
if (isStepDoneStatus(currentStatus)) {
|
||
throw new Error(`步骤 ${step} 已完成,无需再跳过。`);
|
||
}
|
||
|
||
const currentIndex = activeStepIds.indexOf(step);
|
||
if (currentIndex > 0) {
|
||
const prevStep = activeStepIds[currentIndex - 1];
|
||
const prevStatus = statuses[prevStep];
|
||
if (!isStepDoneStatus(prevStatus)) {
|
||
throw new Error(`请先完成步骤 ${prevStep},再跳过步骤 ${step}。`);
|
||
}
|
||
}
|
||
|
||
await setStepStatus(step, 'skipped');
|
||
await addLog(`步骤 ${step} 已跳过`, 'warn');
|
||
|
||
if (step === 1) {
|
||
const latestState = await getState();
|
||
const skippedSteps = [];
|
||
for (let linkedStep = 2; linkedStep <= 5; linkedStep += 1) {
|
||
const linkedStatus = latestState.stepStatuses?.[linkedStep];
|
||
if (!isStepDoneStatus(linkedStatus) && linkedStatus !== 'running') {
|
||
await setStepStatus(linkedStep, 'skipped');
|
||
skippedSteps.push(linkedStep);
|
||
}
|
||
}
|
||
if (skippedSteps.length) {
|
||
await addLog(`步骤 1 已跳过,步骤 ${skippedSteps.join('、')} 也已同时跳过。`, 'warn');
|
||
}
|
||
}
|
||
|
||
return { ok: true, step, status: 'skipped' };
|
||
}
|
||
|
||
function throwIfStopped(error = null) {
|
||
const errorMessage = typeof error === 'string' ? error : error?.message;
|
||
if (errorMessage === STOP_ERROR_MESSAGE) {
|
||
throw error instanceof Error ? error : new Error(STOP_ERROR_MESSAGE);
|
||
}
|
||
if (stopRequested) {
|
||
throw new Error(STOP_ERROR_MESSAGE);
|
||
}
|
||
}
|
||
|
||
async function sleepWithStop(ms) {
|
||
const start = Date.now();
|
||
while (Date.now() - start < ms) {
|
||
throwIfStopped();
|
||
await new Promise(r => setTimeout(r, Math.min(100, ms - (Date.now() - start))));
|
||
}
|
||
}
|
||
|
||
async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY_MAX) {
|
||
const duration = Math.floor(Math.random() * (max - min + 1)) + min;
|
||
await sleepWithStop(duration);
|
||
}
|
||
|
||
async function clickWithDebugger(tabId, rect) {
|
||
throwIfStopped();
|
||
if (!tabId) {
|
||
throw new Error('未找到用于调试点击的认证页面标签页。');
|
||
}
|
||
if (!rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) {
|
||
throw new Error('步骤 9 的调试器兜底点击需要有效的按钮坐标。');
|
||
}
|
||
|
||
const target = { tabId };
|
||
try {
|
||
await chrome.debugger.attach(target, '1.3');
|
||
} catch (err) {
|
||
throw new Error(
|
||
`步骤 9 的调试器兜底点击附加失败:${err.message}。` +
|
||
'如果认证页标签已打开 DevTools,请先关闭后重试。'
|
||
);
|
||
}
|
||
|
||
try {
|
||
throwIfStopped();
|
||
const x = Math.round(rect.centerX);
|
||
const y = Math.round(rect.centerY);
|
||
|
||
await chrome.debugger.sendCommand(target, 'Page.bringToFront');
|
||
throwIfStopped();
|
||
await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
|
||
type: 'mouseMoved',
|
||
x,
|
||
y,
|
||
button: 'none',
|
||
buttons: 0,
|
||
clickCount: 0,
|
||
});
|
||
throwIfStopped();
|
||
await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
|
||
type: 'mousePressed',
|
||
x,
|
||
y,
|
||
button: 'left',
|
||
buttons: 1,
|
||
clickCount: 1,
|
||
});
|
||
throwIfStopped();
|
||
await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
|
||
type: 'mouseReleased',
|
||
x,
|
||
y,
|
||
button: 'left',
|
||
buttons: 0,
|
||
clickCount: 1,
|
||
});
|
||
} finally {
|
||
await chrome.debugger.detach(target).catch(() => { });
|
||
}
|
||
}
|
||
|
||
async function broadcastStopToContentScripts() {
|
||
const registry = await getTabRegistry();
|
||
for (const entry of Object.values(registry)) {
|
||
if (!entry?.tabId) continue;
|
||
try {
|
||
await chrome.tabs.sendMessage(entry.tabId, {
|
||
type: 'STOP_FLOW',
|
||
source: 'background',
|
||
payload: {},
|
||
});
|
||
} catch { }
|
||
}
|
||
}
|
||
|
||
let stopRequested = false;
|
||
|
||
// ============================================================
|
||
// Message Handler (central router)
|
||
// ============================================================
|
||
|
||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||
console.log(LOG_PREFIX, `Received: ${message.type} from ${message.source || 'sidepanel'}`, message);
|
||
|
||
handleMessage(message, sender).then(response => {
|
||
sendResponse(response);
|
||
}).catch(err => {
|
||
console.error(LOG_PREFIX, 'Handler error:', err);
|
||
sendResponse({ error: err.message });
|
||
});
|
||
|
||
return true; // async response
|
||
});
|
||
|
||
async function handleMessage(message, sender) {
|
||
return messageRouter.handleMessage(message, sender);
|
||
}
|
||
|
||
// ============================================================
|
||
// Step Data Handlers
|
||
// ============================================================
|
||
|
||
async function handleStepData(step, payload) {
|
||
if (typeof messageRouter !== 'undefined' && messageRouter?.handleStepData) {
|
||
return messageRouter.handleStepData(step, payload);
|
||
}
|
||
|
||
switch (step) {
|
||
case 1: {
|
||
const updates = {};
|
||
if (payload.oauthUrl) {
|
||
updates.oauthUrl = payload.oauthUrl;
|
||
broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
|
||
}
|
||
if (payload.sub2apiSessionId !== undefined) updates.sub2apiSessionId = payload.sub2apiSessionId || null;
|
||
if (payload.sub2apiOAuthState !== undefined) updates.sub2apiOAuthState = payload.sub2apiOAuthState || null;
|
||
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
|
||
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
|
||
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
|
||
if (payload.cpaOAuthState !== undefined) updates.cpaOAuthState = payload.cpaOAuthState || null;
|
||
if (payload.cpaManagementOrigin !== undefined) updates.cpaManagementOrigin = payload.cpaManagementOrigin || null;
|
||
if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
|
||
if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
|
||
if (Object.keys(updates).length) {
|
||
await setState(updates);
|
||
}
|
||
break;
|
||
}
|
||
case 2:
|
||
if (payload.email) await setEmailState(payload.email);
|
||
if (payload.skippedPasswordStep) {
|
||
const latestState = await getState();
|
||
const step3Status = latestState.stepStatuses?.[3];
|
||
if (step3Status !== 'running' && step3Status !== 'completed' && step3Status !== 'manual_completed') {
|
||
await setStepStatus(3, 'skipped');
|
||
await addLog('步骤 2:提交邮箱后页面直接进入邮箱验证码页,已自动跳过步骤 3。', 'warn');
|
||
}
|
||
}
|
||
break;
|
||
case 3:
|
||
if (payload.email) await setEmailState(payload.email);
|
||
if (payload.signupVerificationRequestedAt) {
|
||
await setState({ signupVerificationRequestedAt: payload.signupVerificationRequestedAt });
|
||
}
|
||
if (payload.loginVerificationRequestedAt) {
|
||
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
|
||
}
|
||
break;
|
||
case 7:
|
||
if (payload.loginVerificationRequestedAt) {
|
||
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
|
||
}
|
||
break;
|
||
case 4:
|
||
await setState({
|
||
lastEmailTimestamp: payload.emailTimestamp || null,
|
||
signupVerificationRequestedAt: null,
|
||
});
|
||
break;
|
||
case 8:
|
||
await setState({
|
||
lastEmailTimestamp: payload.emailTimestamp || null,
|
||
loginVerificationRequestedAt: null,
|
||
});
|
||
break;
|
||
case 9:
|
||
if (payload.localhostUrl) {
|
||
if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) {
|
||
throw new Error('步骤 9 返回了无效的 localhost OAuth 回调地址。');
|
||
}
|
||
await setState({
|
||
localhostUrl: payload.localhostUrl,
|
||
oauthFlowDeadlineAt: null,
|
||
oauthFlowDeadlineSourceUrl: null,
|
||
});
|
||
broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
|
||
}
|
||
break;
|
||
case 10: {
|
||
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 (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);
|
||
const shouldClearCustomPoolEmail = String(latestState?.emailGenerator || '').trim().toLowerCase() === (
|
||
typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
|
||
? CUSTOM_EMAIL_POOL_GENERATOR
|
||
: 'custom-pool'
|
||
);
|
||
if ((shouldUseCustomRegistrationEmail(latestState) || shouldClearCustomPoolEmail) && latestState.email) {
|
||
await setEmailStateSilently(null);
|
||
}
|
||
await finalizePhoneActivationAfterSuccessfulFlow(latestState);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// Step Completion Waiting
|
||
// ============================================================
|
||
|
||
// Map of step -> { resolve, reject } for waiting on step completion
|
||
const stepWaiters = new Map();
|
||
let resumeWaiter = null;
|
||
const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000;
|
||
const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 7, 8, 9]);
|
||
const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 10, 12]);
|
||
const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([
|
||
'open-chatgpt',
|
||
'submit-signup-email',
|
||
'fetch-signup-code',
|
||
'clear-login-cookies',
|
||
'plus-checkout-create',
|
||
'plus-checkout-billing',
|
||
'paypal-approve',
|
||
'plus-checkout-return',
|
||
'oauth-login',
|
||
'fetch-login-code',
|
||
'confirm-oauth',
|
||
]);
|
||
const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([
|
||
'fill-password',
|
||
'fill-profile',
|
||
'gopay-subscription-confirm',
|
||
'platform-verify',
|
||
]);
|
||
const STEP_COMPLETION_SIGNAL_TIMEOUTS_BY_STEP_KEY = new Map([
|
||
['gopay-subscription-confirm', 1800000],
|
||
]);
|
||
const AUTO_RUN_PRE_EXECUTION_DELAYS_BY_STEP_KEY = new Map([
|
||
['plus-checkout-create', 20000],
|
||
]);
|
||
|
||
function waitForStepComplete(step, timeoutMs = 120000) {
|
||
throwIfStopped();
|
||
const normalizedStep = Number(step);
|
||
const existingWaiter = stepWaiters.get(normalizedStep);
|
||
if (existingWaiter?.promise) {
|
||
console.log(LOG_PREFIX, `[waitForStepComplete] reuse existing waiter for step ${normalizedStep}`);
|
||
return existingWaiter.promise;
|
||
}
|
||
|
||
console.log(LOG_PREFIX, `[waitForStepComplete] register step ${normalizedStep}, timeout=${timeoutMs}ms`);
|
||
const waiter = {
|
||
promise: null,
|
||
resolve: null,
|
||
reject: null,
|
||
};
|
||
|
||
waiter.promise = new Promise((resolve, reject) => {
|
||
const timer = setTimeout(() => {
|
||
if (stepWaiters.get(normalizedStep) === waiter) {
|
||
stepWaiters.delete(normalizedStep);
|
||
}
|
||
console.warn(LOG_PREFIX, `[waitForStepComplete] timeout for step ${normalizedStep} after ${timeoutMs}ms`);
|
||
reject(new Error(`步骤 ${normalizedStep} 等待超时(>${timeoutMs / 1000} 秒)`));
|
||
}, timeoutMs);
|
||
|
||
waiter.resolve = (data) => {
|
||
clearTimeout(timer);
|
||
if (stepWaiters.get(normalizedStep) === waiter) {
|
||
stepWaiters.delete(normalizedStep);
|
||
}
|
||
resolve(data);
|
||
};
|
||
waiter.reject = (err) => {
|
||
clearTimeout(timer);
|
||
if (stepWaiters.get(normalizedStep) === waiter) {
|
||
stepWaiters.delete(normalizedStep);
|
||
}
|
||
reject(err);
|
||
};
|
||
});
|
||
|
||
stepWaiters.set(normalizedStep, waiter);
|
||
return waiter.promise;
|
||
}
|
||
|
||
function getStepExecutionKeyForState(step, state = {}) {
|
||
if (typeof getStepDefinitionForState !== 'function') {
|
||
return '';
|
||
}
|
||
return String(getStepDefinitionForState(step, state)?.key || '').trim();
|
||
}
|
||
|
||
function doesStepUseBackgroundCompletion(step, state = {}) {
|
||
const stepKey = getStepExecutionKeyForState(step, state);
|
||
if (stepKey) {
|
||
return AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS.has(stepKey);
|
||
}
|
||
return AUTO_RUN_BACKGROUND_COMPLETED_STEPS.has(step);
|
||
}
|
||
|
||
function doesStepUseCompletionSignal(step, state = {}) {
|
||
const stepKey = getStepExecutionKeyForState(step, state);
|
||
if (stepKey) {
|
||
return STEP_COMPLETION_SIGNAL_STEP_KEYS.has(stepKey);
|
||
}
|
||
return STEP_COMPLETION_SIGNAL_STEPS.has(step);
|
||
}
|
||
|
||
function getAutoRunPreExecutionDelayMs(step, state = {}) {
|
||
const stepKey = getStepExecutionKeyForState(step, state);
|
||
if (stepKey) {
|
||
return AUTO_RUN_PRE_EXECUTION_DELAYS_BY_STEP_KEY.get(stepKey) || 0;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
function getStepCompletionSignalTimeoutMs(step, state = {}) {
|
||
const stepKey = getStepExecutionKeyForState(step, state);
|
||
if (stepKey) {
|
||
return STEP_COMPLETION_SIGNAL_TIMEOUTS_BY_STEP_KEY.get(stepKey) || AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS;
|
||
}
|
||
return AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS;
|
||
}
|
||
|
||
function notifyStepComplete(step, payload) {
|
||
const waiter = stepWaiters.get(step);
|
||
console.log(LOG_PREFIX, `[notifyStepComplete] step ${step}, hasWaiter=${Boolean(waiter)}`);
|
||
if (waiter) waiter.resolve(payload);
|
||
}
|
||
|
||
function notifyStepError(step, error) {
|
||
const waiter = stepWaiters.get(step);
|
||
console.warn(LOG_PREFIX, `[notifyStepError] step ${step}, hasWaiter=${Boolean(waiter)}, error=${error}`);
|
||
if (waiter) waiter.reject(new Error(error));
|
||
}
|
||
|
||
async function runCompletedStepSideEffects(step, payload, completionState, lastStepId) {
|
||
await handleStepData(step, payload);
|
||
if (step === lastStepId) {
|
||
await appendAndBroadcastAccountRunRecord('success', completionState);
|
||
}
|
||
}
|
||
|
||
async function reportCompletedStepSideEffectError(step, error) {
|
||
const message = getErrorMessage(error);
|
||
console.warn(LOG_PREFIX, `[completeStepFromBackground] step ${step} post-completion side effect failed:`, error);
|
||
await addLog(`步骤 ${step} 已完成,但完成后的收尾处理失败:${message}`, 'warn');
|
||
}
|
||
|
||
async function completeStepFromBackground(step, payload = {}) {
|
||
if (stopRequested) {
|
||
await setStepStatus(step, 'stopped');
|
||
await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, null, STOP_ERROR_MESSAGE);
|
||
notifyStepError(step, STOP_ERROR_MESSAGE);
|
||
return;
|
||
}
|
||
|
||
const latestState = await getState();
|
||
const lastStepId = typeof getLastStepIdForState === 'function'
|
||
? getLastStepIdForState(latestState)
|
||
: (typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10);
|
||
const completionState = step === lastStepId ? latestState : null;
|
||
await setStepStatus(step, 'completed');
|
||
await addLog(`步骤 ${step} 已完成`, 'ok');
|
||
|
||
if (step === lastStepId) {
|
||
notifyStepComplete(step, payload);
|
||
void runCompletedStepSideEffects(step, payload, completionState, lastStepId)
|
||
.catch((error) => reportCompletedStepSideEffectError(step, error));
|
||
return;
|
||
}
|
||
|
||
await runCompletedStepSideEffects(step, payload, completionState, lastStepId);
|
||
notifyStepComplete(step, payload);
|
||
}
|
||
|
||
async function appendManualAccountRunRecordIfNeeded(status, stateOverride = null, reason = '') {
|
||
if (!accountRunHistoryHelpers?.appendAccountRunRecord) {
|
||
return null;
|
||
}
|
||
|
||
const state = stateOverride || await getState();
|
||
return appendAndBroadcastAccountRunRecord(status, state, reason);
|
||
}
|
||
|
||
async function finalizeDeferredStepExecutionError(step, error) {
|
||
const latestState = await getState();
|
||
const currentStatus = latestState.stepStatuses?.[step];
|
||
if (currentStatus === 'completed' || currentStatus === 'failed' || currentStatus === 'stopped') {
|
||
return;
|
||
}
|
||
|
||
if (isStopError(error)) {
|
||
await setStepStatus(step, 'stopped');
|
||
await addLog(`步骤 ${step} 已被用户停止`, 'warn');
|
||
await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, latestState, getErrorMessage(error));
|
||
return;
|
||
}
|
||
|
||
await setStepStatus(step, 'failed');
|
||
await addLog(`步骤 ${step} 失败:${getErrorMessage(error)}`, 'error');
|
||
await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, latestState, getErrorMessage(error));
|
||
}
|
||
|
||
async function executeStepViaCompletionSignal(step, timeoutMs = 0) {
|
||
const executionState = await getState();
|
||
const resolvedTimeoutMs = Number(timeoutMs) > 0
|
||
? timeoutMs
|
||
: getStepCompletionSignalTimeoutMs(step, executionState);
|
||
const completionResultPromise = waitForStepComplete(step, resolvedTimeoutMs).then(
|
||
payload => ({ ok: true, payload }),
|
||
error => ({ ok: false, error }),
|
||
);
|
||
|
||
let executeError = null;
|
||
try {
|
||
await executeStep(step, { deferRetryableTransportError: true });
|
||
} catch (err) {
|
||
executeError = err;
|
||
if (isStopError(err) || !isRetryableContentScriptTransportError(err)) {
|
||
notifyStepError(step, getErrorMessage(err));
|
||
}
|
||
}
|
||
|
||
const completionResult = await completionResultPromise;
|
||
if (completionResult.ok) {
|
||
if (executeError) {
|
||
console.warn(
|
||
LOG_PREFIX,
|
||
`[executeStepViaCompletionSignal] step ${step} completed after deferred execute error: ${getErrorMessage(executeError)}`
|
||
);
|
||
}
|
||
return completionResult.payload;
|
||
}
|
||
|
||
if (executeError && isRetryableContentScriptTransportError(executeError)) {
|
||
const completionMessage = getErrorMessage(completionResult.error);
|
||
if (/等待超时/.test(completionMessage)) {
|
||
await finalizeDeferredStepExecutionError(step, executeError);
|
||
throw executeError;
|
||
}
|
||
throw completionResult.error;
|
||
}
|
||
|
||
if (executeError) {
|
||
throw executeError;
|
||
}
|
||
|
||
throw completionResult.error;
|
||
}
|
||
|
||
async function waitForRunningStepsToFinish(payload = {}) {
|
||
let currentState = await getState();
|
||
let runningSteps = getRunningSteps(currentState.stepStatuses, currentState);
|
||
if (!runningSteps.length) {
|
||
return currentState;
|
||
}
|
||
|
||
await addLog(`自动继续:检测到步骤 ${runningSteps.join(', ')} 正在运行,等待完成后再继续自动流程...`, 'info');
|
||
await broadcastAutoRunStatus('waiting_step', payload);
|
||
|
||
while (runningSteps.length) {
|
||
await sleepWithStop(250);
|
||
currentState = await getState();
|
||
runningSteps = getRunningSteps(currentState.stepStatuses, currentState);
|
||
}
|
||
|
||
await addLog('自动继续:当前运行步骤已结束,准备按最新进度继续自动流程...', 'info');
|
||
return currentState;
|
||
}
|
||
|
||
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10, 11, 12, 13]);
|
||
const AUTH_CHAIN_STEP_KEYS = new Set([
|
||
'oauth-login',
|
||
'fetch-login-code',
|
||
'confirm-oauth',
|
||
'platform-verify',
|
||
]);
|
||
let activeTopLevelAuthChainExecution = null;
|
||
|
||
function isAuthChainStep(step, state = {}) {
|
||
const stepKey = typeof getStepDefinitionForState === 'function'
|
||
? String(getStepDefinitionForState(step, state)?.key || '').trim()
|
||
: '';
|
||
if (stepKey && typeof AUTH_CHAIN_STEP_KEYS !== 'undefined') {
|
||
return AUTH_CHAIN_STEP_KEYS.has(stepKey);
|
||
}
|
||
return AUTH_CHAIN_STEP_IDS.has(Number(step));
|
||
}
|
||
|
||
async function acquireTopLevelAuthChainExecution(step, state = {}) {
|
||
const normalizedStep = Number(step);
|
||
if (!isAuthChainStep(normalizedStep, state)) {
|
||
return {
|
||
joined: false,
|
||
release() {},
|
||
};
|
||
}
|
||
|
||
if (activeTopLevelAuthChainExecution) {
|
||
const activeExecution = activeTopLevelAuthChainExecution;
|
||
await addLog(
|
||
`步骤 ${normalizedStep}:检测到步骤 ${activeExecution.step} 正在运行,本次请求将复用当前授权链,不再重复启动。`,
|
||
'warn'
|
||
);
|
||
const result = await activeExecution.promise;
|
||
if (result?.error) {
|
||
throw result.error;
|
||
}
|
||
return {
|
||
joined: true,
|
||
release() {},
|
||
};
|
||
}
|
||
|
||
let settleExecution = () => {};
|
||
const promise = new Promise((resolve) => {
|
||
settleExecution = (error = null) => resolve({ error });
|
||
});
|
||
const execution = {
|
||
step: normalizedStep,
|
||
promise,
|
||
};
|
||
activeTopLevelAuthChainExecution = execution;
|
||
|
||
return {
|
||
joined: false,
|
||
release(error = null) {
|
||
if (activeTopLevelAuthChainExecution === execution) {
|
||
activeTopLevelAuthChainExecution = null;
|
||
}
|
||
settleExecution(error);
|
||
},
|
||
};
|
||
}
|
||
|
||
async function markRunningStepsStopped() {
|
||
const state = await getState();
|
||
const runningSteps = getRunningSteps(state.stepStatuses, state);
|
||
|
||
for (const step of runningSteps) {
|
||
await setStepStatus(step, 'stopped');
|
||
}
|
||
}
|
||
|
||
async function requestStop(options = {}) {
|
||
const { logMessage = '已收到停止请求,正在取消当前操作...' } = options;
|
||
const state = await getState();
|
||
const runningSteps = getRunningSteps(state.stepStatuses, state);
|
||
const inferredStopStep = inferStoppedRecordStep(state);
|
||
const timerPlan = getPendingAutoRunTimerPlan(state);
|
||
|
||
if (timerPlan?.kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START && !autoRunActive) {
|
||
await cancelScheduledAutoRun({
|
||
logMessage: options.logMessage === false
|
||
? false
|
||
: (options.logMessage || '已取消自动运行倒计时计划。'),
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (timerPlan && !autoRunActive) {
|
||
autoRunCurrentRun = timerPlan.currentRun;
|
||
autoRunTotalRuns = timerPlan.totalRuns;
|
||
autoRunAttemptRun = timerPlan.attemptRun;
|
||
clearCurrentAutoRunSessionId(timerPlan.autoRunSessionId);
|
||
if (options.logMessage !== false) {
|
||
await addLog(options.logMessage || '已停止等待中的自动流程。', 'warn');
|
||
}
|
||
await broadcastAutoRunStatus('stopped', {
|
||
currentRun: timerPlan.currentRun,
|
||
totalRuns: timerPlan.totalRuns,
|
||
attemptRun: timerPlan.attemptRun,
|
||
sessionId: 0,
|
||
}, {
|
||
autoRunSessionId: 0,
|
||
autoRunSkipFailures: timerPlan.autoRunSkipFailures,
|
||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(timerPlan.totalRuns, timerPlan.roundSummaries),
|
||
autoRunTimerPlan: null,
|
||
scheduledAutoRunPlan: null,
|
||
});
|
||
await clearAutoRunTimerAlarm();
|
||
clearStopRequest();
|
||
return;
|
||
}
|
||
|
||
if (stopRequested) return;
|
||
|
||
stopRequested = true;
|
||
clearCurrentAutoRunSessionId();
|
||
cancelPendingCommands();
|
||
abortActiveIcloudRequests();
|
||
cleanupStep8NavigationListeners();
|
||
rejectPendingStep8(new Error(STOP_ERROR_MESSAGE));
|
||
|
||
await addLog(logMessage, 'warn');
|
||
await broadcastStopToContentScripts();
|
||
|
||
if (!runningSteps.length && Number.isInteger(inferredStopStep) && inferredStopStep > 0) {
|
||
await appendAndBroadcastAccountRunRecord('stopped', state, STOP_ERROR_MESSAGE);
|
||
}
|
||
|
||
for (const waiter of stepWaiters.values()) {
|
||
waiter.reject(new Error(STOP_ERROR_MESSAGE));
|
||
}
|
||
stepWaiters.clear();
|
||
|
||
if (state.plusManualConfirmationPending) {
|
||
const clearManualConfirmationState = {
|
||
plusManualConfirmationPending: false,
|
||
plusManualConfirmationRequestId: '',
|
||
plusManualConfirmationStep: 0,
|
||
plusManualConfirmationMethod: '',
|
||
plusManualConfirmationTitle: '',
|
||
plusManualConfirmationMessage: '',
|
||
};
|
||
await setState(clearManualConfirmationState);
|
||
broadcastDataUpdate(clearManualConfirmationState);
|
||
}
|
||
|
||
if (resumeWaiter) {
|
||
resumeWaiter.reject(new Error(STOP_ERROR_MESSAGE));
|
||
resumeWaiter = null;
|
||
}
|
||
|
||
await markRunningStepsStopped();
|
||
autoRunActive = false;
|
||
await broadcastAutoRunStatus('stopped', {
|
||
currentRun: autoRunCurrentRun,
|
||
totalRuns: autoRunTotalRuns,
|
||
attemptRun: autoRunAttemptRun,
|
||
sessionId: 0,
|
||
}, {
|
||
autoRunSessionId: 0,
|
||
autoRunTimerPlan: null,
|
||
scheduledAutoRunPlan: null,
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// Step Execution
|
||
// ============================================================
|
||
|
||
async function executeStep(step, options = {}) {
|
||
const { deferRetryableTransportError = false } = options;
|
||
console.log(LOG_PREFIX, `Executing step ${step}`);
|
||
let state = await getState();
|
||
const authChainClaim = await acquireTopLevelAuthChainExecution(step, state);
|
||
if (authChainClaim.joined) {
|
||
return;
|
||
}
|
||
|
||
let executionError = null;
|
||
throwIfStopped();
|
||
try {
|
||
await setStepStatus(step, 'running');
|
||
await addLog(`步骤 ${step} 开始执行`);
|
||
await humanStepDelay();
|
||
|
||
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),
|
||
});
|
||
} catch (err) {
|
||
executionError = err;
|
||
const errorState = await getState();
|
||
if (isStopError(err)) {
|
||
await setStepStatus(step, 'stopped');
|
||
await addLog(`步骤 ${step} 已被用户停止`, 'warn');
|
||
await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, errorState, getErrorMessage(err));
|
||
throw err;
|
||
}
|
||
if (isTerminalSecurityBlockedError(err)) {
|
||
await handleCloudflareSecurityBlocked(err);
|
||
throw new Error(STOP_ERROR_MESSAGE);
|
||
}
|
||
if (isBrowserSwitchRequiredError(err)) {
|
||
await handleBrowserSwitchRequired(err);
|
||
throw new Error(STOP_ERROR_MESSAGE);
|
||
}
|
||
if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step, errorState) && isRetryableContentScriptTransportError(err))) {
|
||
await setStepStatus(step, 'failed');
|
||
await addLog(`步骤 ${step} 失败:${err.message}`, 'error');
|
||
await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, errorState, getErrorMessage(err));
|
||
} else {
|
||
console.warn(
|
||
LOG_PREFIX,
|
||
`[executeStep] deferring retryable transport error for step ${step}: ${getErrorMessage(err)}`
|
||
);
|
||
}
|
||
throw err;
|
||
} finally {
|
||
authChainClaim.release(executionError);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Execute a step and wait for it to complete before returning.
|
||
* @param {number} step
|
||
* @param {number} delayAfter - ms to wait after completion (for page transitions)
|
||
*/
|
||
async function executeStepAndWait(step, delayAfter = 2000) {
|
||
throwIfStopped();
|
||
|
||
const delaySeconds = normalizeAutoStepDelaySeconds((await getState()).autoStepDelaySeconds, null);
|
||
if (delaySeconds > 0) {
|
||
await addLog(
|
||
`自动运行:步骤 ${step} 执行前额外等待 ${delaySeconds} 秒,避免节奏过快。`,
|
||
'info'
|
||
);
|
||
await sleepWithStop(delaySeconds * 1000);
|
||
}
|
||
|
||
let executionState = await getState();
|
||
const preExecutionDelayMs = getAutoRunPreExecutionDelayMs(step, executionState);
|
||
if (preExecutionDelayMs > 0) {
|
||
await addLog(
|
||
`自动运行:步骤 ${step} 执行前固定等待 ${Math.round(preExecutionDelayMs / 1000)} 秒,确保 Plus Checkout 创建前页面稳定。`,
|
||
'info'
|
||
);
|
||
await sleepWithStop(preExecutionDelayMs);
|
||
executionState = await getState();
|
||
}
|
||
|
||
if (doesStepUseBackgroundCompletion(step, executionState)) {
|
||
await addLog(`自动运行:步骤 ${step} 由后台流程负责收尾,执行函数返回后将直接进入下一步。`, 'info');
|
||
await executeStep(step);
|
||
const latestState = await getState();
|
||
await addLog(`自动运行:步骤 ${step} 已执行返回,当前状态为 ${latestState.stepStatuses?.[step] || 'pending'},准备继续后续步骤。`, 'info');
|
||
} else if (doesStepUseCompletionSignal(step, executionState)) {
|
||
await addLog(`自动运行:步骤 ${step} 已发起,正在等待完成信号(超时 ${AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS / 1000} 秒)。`, 'info');
|
||
await executeStepViaCompletionSignal(step, AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS);
|
||
await addLog(`自动运行:步骤 ${step} 已收到完成信号,准备继续后续步骤。`, 'info');
|
||
} else {
|
||
await executeStep(step);
|
||
}
|
||
|
||
if (step === 5) {
|
||
const signupTabId = await getTabId('signup-page');
|
||
if (signupTabId) {
|
||
await addLog('自动运行:步骤 5 已收到完成信号,正在等待当前页面完成加载并稳定...', 'info');
|
||
await waitForTabStableComplete(signupTabId, {
|
||
timeoutMs: 30000,
|
||
retryDelayMs: 300,
|
||
stableMs: 1000,
|
||
initialDelayMs: 800,
|
||
});
|
||
}
|
||
}
|
||
|
||
// Extra delay for page transitions / DOM updates
|
||
if (delayAfter > 0) {
|
||
await sleepWithStop(delayAfter + Math.floor(Math.random() * 1200));
|
||
}
|
||
}
|
||
|
||
function getEmailGeneratorLabel(generator) {
|
||
const customEmailPoolGenerator = typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
|
||
? CUSTOM_EMAIL_POOL_GENERATOR
|
||
: 'custom-pool';
|
||
const gmailAliasGenerator = typeof GMAIL_ALIAS_GENERATOR === 'string'
|
||
? GMAIL_ALIAS_GENERATOR
|
||
: 'gmail-alias';
|
||
if (generator === 'custom') {
|
||
return '自定义邮箱';
|
||
}
|
||
if (generator === gmailAliasGenerator) {
|
||
return 'Gmail +tag 邮箱';
|
||
}
|
||
if (generator === customEmailPoolGenerator) {
|
||
return '自定义邮箱池';
|
||
}
|
||
if (generator === 'icloud') {
|
||
return 'iCloud 隐私邮箱';
|
||
}
|
||
if (generator === 'cloudflare') return 'Cloudflare 邮箱';
|
||
if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email';
|
||
return 'Duck 邮箱';
|
||
}
|
||
const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMail2925SessionManager({
|
||
addLog,
|
||
broadcastDataUpdate,
|
||
chrome,
|
||
findMail2925Account,
|
||
getMail2925AccountStatus,
|
||
getState,
|
||
isAutoRunLockedState,
|
||
isMail2925AccountAvailable: self.Mail2925Utils?.isMail2925AccountAvailable,
|
||
MAIL2925_LIMIT_COOLDOWN_MS,
|
||
normalizeMail2925Account,
|
||
normalizeMail2925Accounts,
|
||
pickMail2925AccountForRun,
|
||
requestStop,
|
||
ensureContentScriptReadyOnTab,
|
||
reuseOrCreateTab,
|
||
sendToContentScriptResilient,
|
||
sendToMailContentScriptResilient,
|
||
setPersistentSettings,
|
||
setState,
|
||
sleepWithStop,
|
||
throwIfStopped,
|
||
upsertMail2925AccountInList,
|
||
waitForTabComplete,
|
||
waitForTabUrlMatch,
|
||
});
|
||
|
||
async function upsertMail2925Account(input = {}) {
|
||
return mail2925SessionManager.upsertMail2925Account(input);
|
||
}
|
||
|
||
async function deleteMail2925Account(accountId) {
|
||
return mail2925SessionManager.deleteMail2925Account(accountId);
|
||
}
|
||
|
||
async function deleteMail2925Accounts(mode = 'all') {
|
||
return mail2925SessionManager.deleteMail2925Accounts(mode);
|
||
}
|
||
|
||
async function patchMail2925Account(accountId, updates = {}) {
|
||
return mail2925SessionManager.patchMail2925Account(accountId, updates);
|
||
}
|
||
|
||
async function setCurrentMail2925Account(accountId, options = {}) {
|
||
return mail2925SessionManager.setCurrentMail2925Account(accountId, options);
|
||
}
|
||
|
||
function getCurrentMail2925Account(state = null) {
|
||
return mail2925SessionManager.getCurrentMail2925Account(state || {});
|
||
}
|
||
|
||
async function ensureMail2925AccountForFlow(options = {}) {
|
||
return mail2925SessionManager.ensureMail2925AccountForFlow(options);
|
||
}
|
||
|
||
async function ensureMail2925MailboxSession(options = {}) {
|
||
return mail2925SessionManager.ensureMail2925MailboxSession(options);
|
||
}
|
||
|
||
async function handleMail2925LimitReachedError(step, error) {
|
||
return mail2925SessionManager.handleMail2925LimitReachedError(step, error);
|
||
}
|
||
|
||
function isMail2925LimitReachedError(error) {
|
||
if (typeof mail2925SessionManager !== 'undefined' && mail2925SessionManager?.isMail2925LimitReachedError) {
|
||
return mail2925SessionManager.isMail2925LimitReachedError(error);
|
||
}
|
||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||
return /^MAIL2925_LIMIT_REACHED::/.test(message)
|
||
|| /子邮箱.{0,12}已达上限|已达上限邮箱|子邮箱上限|邮箱已达上限/i.test(message);
|
||
}
|
||
|
||
function isMail2925ThreadTerminatedError(error) {
|
||
if (typeof mail2925SessionManager !== 'undefined' && mail2925SessionManager?.isMail2925ThreadTerminatedError) {
|
||
return mail2925SessionManager.isMail2925ThreadTerminatedError(error);
|
||
}
|
||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||
return /^MAIL2925_THREAD_TERMINATED::/.test(message);
|
||
}
|
||
|
||
function isMail2925PoolExhaustedPauseError(error) {
|
||
if (typeof mail2925SessionManager !== 'undefined' && mail2925SessionManager?.isMail2925PoolExhaustedPauseError) {
|
||
return mail2925SessionManager.isMail2925PoolExhaustedPauseError(error);
|
||
}
|
||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||
return /^MAIL2925_POOL_EXHAUSTED_PAUSE::/.test(message);
|
||
}
|
||
|
||
const payPalAccountStore = self.MultiPageBackgroundPayPalAccountStore?.createPayPalAccountStore({
|
||
broadcastDataUpdate,
|
||
findPayPalAccount,
|
||
getState,
|
||
normalizePayPalAccount,
|
||
normalizePayPalAccounts,
|
||
setPersistentSettings,
|
||
setState,
|
||
upsertPayPalAccountInList,
|
||
});
|
||
|
||
async function syncPayPalAccounts(accounts) {
|
||
return payPalAccountStore?.syncPayPalAccounts?.(accounts) || [];
|
||
}
|
||
|
||
async function upsertPayPalAccount(input = {}) {
|
||
if (!payPalAccountStore?.upsertPayPalAccount) {
|
||
throw new Error('PayPal 账号存储能力尚未接入。');
|
||
}
|
||
return payPalAccountStore.upsertPayPalAccount(input);
|
||
}
|
||
|
||
async function setCurrentPayPalAccount(accountId) {
|
||
if (!payPalAccountStore?.setCurrentPayPalAccount) {
|
||
throw new Error('PayPal 账号选择能力尚未接入。');
|
||
}
|
||
return payPalAccountStore.setCurrentPayPalAccount(accountId);
|
||
}
|
||
|
||
function getCurrentPayPalAccount(state = null) {
|
||
return payPalAccountStore?.getCurrentPayPalAccount?.(state || {}) || null;
|
||
}
|
||
|
||
const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGeneratedEmailHelpers({
|
||
addLog,
|
||
buildGeneratedAliasEmail,
|
||
buildCloudflareTempEmailHeaders,
|
||
CLOUDFLARE_TEMP_EMAIL_GENERATOR,
|
||
CUSTOM_EMAIL_POOL_GENERATOR,
|
||
DUCK_AUTOFILL_URL,
|
||
fetch,
|
||
fetchIcloudHideMyEmail,
|
||
getCloudflareTempEmailAddressFromResponse,
|
||
getCloudflareTempEmailConfig,
|
||
getCustomEmailPoolEmail: getCustomEmailPoolEmailForRun,
|
||
getState,
|
||
ensureMail2925AccountForFlow,
|
||
joinCloudflareTempEmailUrl,
|
||
normalizeCloudflareDomain,
|
||
normalizeCloudflareTempEmailAddress,
|
||
normalizeEmailGenerator,
|
||
isGeneratedAliasProvider,
|
||
reuseOrCreateTab,
|
||
sendToContentScript,
|
||
setEmailState,
|
||
throwIfStopped,
|
||
});
|
||
|
||
function generateCloudflareAliasLocalPart() {
|
||
return generatedEmailHelpers.generateCloudflareAliasLocalPart();
|
||
}
|
||
|
||
async function fetchCloudflareEmail(state, options = {}) {
|
||
return generatedEmailHelpers.fetchCloudflareEmail(state, options);
|
||
}
|
||
|
||
function ensureCloudflareTempEmailConfig(state, options = {}) {
|
||
return generatedEmailHelpers.ensureCloudflareTempEmailConfig(state, options);
|
||
}
|
||
|
||
async function requestCloudflareTempEmailJson(config, path, options = {}) {
|
||
return generatedEmailHelpers.requestCloudflareTempEmailJson(config, path, options);
|
||
}
|
||
|
||
async function fetchCloudflareTempEmailAddress(state, options = {}) {
|
||
return generatedEmailHelpers.fetchCloudflareTempEmailAddress(state, options);
|
||
}
|
||
|
||
async function fetchDuckEmail(options = {}) {
|
||
return generatedEmailHelpers.fetchDuckEmail(options);
|
||
}
|
||
|
||
async function fetchGeneratedEmail(state, options = {}) {
|
||
return generatedEmailHelpers.fetchGeneratedEmail(state, options);
|
||
}
|
||
|
||
// ============================================================
|
||
// Auto Run Flow
|
||
// ============================================================
|
||
|
||
let autoRunActive = false;
|
||
let autoRunCurrentRun = 0;
|
||
let autoRunTotalRuns = 1;
|
||
let autoRunAttemptRun = 0;
|
||
let autoRunSessionId = 0;
|
||
let autoRunSessionSeed = 0;
|
||
const EMAIL_FETCH_MAX_ATTEMPTS = 5;
|
||
const VERIFICATION_POLL_MAX_ROUNDS = 5;
|
||
const STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS = 25000;
|
||
const MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15;
|
||
const MAIL_2925_VERIFICATION_INTERVAL_MS = 15000;
|
||
const AUTO_STEP_DELAYS = {
|
||
1: 2000,
|
||
2: 2000,
|
||
3: 3000,
|
||
4: 2000,
|
||
5: 0,
|
||
6: 3000,
|
||
7: 2000,
|
||
8: 2000,
|
||
9: 1000,
|
||
};
|
||
const accountRunHistoryHelpers = self.MultiPageBackgroundAccountRunHistory?.createAccountRunHistoryHelpers({
|
||
ACCOUNT_RUN_HISTORY_STORAGE_KEY,
|
||
addLog,
|
||
buildLocalHelperEndpoint: (baseUrl, path) => buildHotmailLocalEndpoint(baseUrl, path),
|
||
chrome,
|
||
getErrorMessage,
|
||
getState,
|
||
normalizeAccountRunHistoryHelperBaseUrl,
|
||
});
|
||
const contributionOAuthManager = self.MultiPageBackgroundContributionOAuth?.createContributionOAuthManager({
|
||
addLog,
|
||
broadcastDataUpdate,
|
||
chrome,
|
||
closeLocalhostCallbackTabs,
|
||
getState,
|
||
setState,
|
||
});
|
||
contributionOAuthManager?.ensureCallbackListeners?.();
|
||
|
||
async function broadcastAccountRunHistoryUpdate() {
|
||
if (!accountRunHistoryHelpers?.getPersistedAccountRunHistory) {
|
||
return [];
|
||
}
|
||
|
||
const history = await accountRunHistoryHelpers.getPersistedAccountRunHistory();
|
||
broadcastDataUpdate({ accountRunHistory: history });
|
||
return history;
|
||
}
|
||
|
||
async function appendAndBroadcastAccountRunRecord(status, stateOverride = null, reason = '') {
|
||
if (!accountRunHistoryHelpers?.appendAccountRunRecord) {
|
||
return null;
|
||
}
|
||
|
||
const state = stateOverride || await getState();
|
||
const resolvedStatus = resolveAccountRunRecordStatusForStop(status, state);
|
||
const resolvedReason = resolveAccountRunRecordReasonForStop(resolvedStatus, reason);
|
||
const record = await accountRunHistoryHelpers.appendAccountRunRecord(resolvedStatus, state, resolvedReason);
|
||
if (!record) {
|
||
return null;
|
||
}
|
||
|
||
await broadcastAccountRunHistoryUpdate();
|
||
return record;
|
||
}
|
||
|
||
async function clearAndBroadcastAccountRunHistory(stateOverride = null) {
|
||
if (!accountRunHistoryHelpers?.clearAccountRunHistory) {
|
||
return { clearedCount: 0 };
|
||
}
|
||
|
||
const result = await accountRunHistoryHelpers.clearAccountRunHistory(stateOverride);
|
||
await broadcastAccountRunHistoryUpdate();
|
||
return result;
|
||
}
|
||
|
||
async function deleteAndBroadcastAccountRunHistoryRecords(recordIds = [], stateOverride = null) {
|
||
if (!accountRunHistoryHelpers?.deleteAccountRunHistoryRecords) {
|
||
return { deletedCount: 0, remainingCount: 0 };
|
||
}
|
||
|
||
const result = await accountRunHistoryHelpers.deleteAccountRunHistoryRecords(recordIds, stateOverride);
|
||
await broadcastAccountRunHistoryUpdate();
|
||
return result;
|
||
}
|
||
|
||
function resolveIpProxyCandidateCountForAutoSwitch(state = {}, mode = 'account', provider = DEFAULT_IP_PROXY_SERVICE) {
|
||
const normalizedMode = typeof normalizeIpProxyMode === 'function'
|
||
? normalizeIpProxyMode(mode)
|
||
: String(mode || 'account').trim().toLowerCase();
|
||
const normalizedProvider = typeof normalizeIpProxyProviderValue === 'function'
|
||
? normalizeIpProxyProviderValue(provider)
|
||
: String(provider || DEFAULT_IP_PROXY_SERVICE).trim().toLowerCase();
|
||
if (normalizedMode === 'account' && typeof getAccountModeProxyPoolFromState === 'function') {
|
||
const pool = getAccountModeProxyPoolFromState(state, normalizedProvider);
|
||
return Array.isArray(pool) ? pool.length : 0;
|
||
}
|
||
if (typeof getIpProxyRuntimeSnapshot === 'function') {
|
||
const runtime = getIpProxyRuntimeSnapshot(state, normalizedMode, normalizedProvider);
|
||
return Array.isArray(runtime?.pool) ? runtime.pool.length : 0;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
async function maybeSwitchIpProxyAfterAutoRunRoundSuccess(payload = {}) {
|
||
if (typeof switchIpProxy !== 'function') {
|
||
return null;
|
||
}
|
||
const successfulRuns = Number(payload?.successfulRuns) || 0;
|
||
if (successfulRuns <= 0) {
|
||
return null;
|
||
}
|
||
|
||
const state = await getState();
|
||
if (!state?.ipProxyEnabled) {
|
||
return null;
|
||
}
|
||
|
||
const mode = typeof normalizeIpProxyMode === 'function'
|
||
? normalizeIpProxyMode(state?.ipProxyMode)
|
||
: String(state?.ipProxyMode || 'account').trim().toLowerCase();
|
||
const provider = typeof normalizeIpProxyProviderValue === 'function'
|
||
? normalizeIpProxyProviderValue(state?.ipProxyService)
|
||
: String(state?.ipProxyService || DEFAULT_IP_PROXY_SERVICE).trim().toLowerCase();
|
||
const threshold = typeof resolveIpProxyAutoSwitchThreshold === 'function'
|
||
? resolveIpProxyAutoSwitchThreshold(state)
|
||
: Math.max(1, Math.min(500, Number(state?.ipProxyPoolTargetCount) || 20));
|
||
if (successfulRuns % threshold !== 0) {
|
||
return null;
|
||
}
|
||
|
||
const candidateCount = resolveIpProxyCandidateCountForAutoSwitch(state, mode, provider);
|
||
if (candidateCount <= 1) {
|
||
await addLog(
|
||
`任务切换阈值命中(成功 ${successfulRuns} 轮 / 阈值 ${threshold}),但当前仅 ${candidateCount} 条可切换代理,已跳过自动切换。`,
|
||
'info'
|
||
);
|
||
return {
|
||
skipped: true,
|
||
reason: 'insufficient_candidates',
|
||
candidateCount,
|
||
threshold,
|
||
successfulRuns,
|
||
};
|
||
}
|
||
|
||
const switchResult = await switchIpProxy('next', {
|
||
mode,
|
||
state,
|
||
forceRefresh: mode === 'api',
|
||
maxItems: typeof resolveIpProxyPoolTargetCountForMode === 'function'
|
||
? resolveIpProxyPoolTargetCountForMode(state, mode)
|
||
: undefined,
|
||
});
|
||
const display = String(switchResult?.display || '').trim();
|
||
const routingApplied = Boolean(switchResult?.proxyRouting?.applied);
|
||
await addLog(
|
||
routingApplied
|
||
? `任务切换阈值命中(成功 ${successfulRuns} 轮 / 阈值 ${threshold}),已自动切换代理:${display || '已切换到下一条'}。`
|
||
: `任务切换阈值命中(成功 ${successfulRuns} 轮 / 阈值 ${threshold}),已尝试自动切换代理,但连通性仍异常。`,
|
||
routingApplied ? 'ok' : 'warn'
|
||
);
|
||
return switchResult;
|
||
}
|
||
|
||
const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({
|
||
addLog,
|
||
appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args),
|
||
AUTO_RUN_MAX_RETRIES_PER_ROUND,
|
||
AUTO_RUN_RETRY_DELAY_MS,
|
||
AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
|
||
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
|
||
broadcastAutoRunStatus,
|
||
broadcastStopToContentScripts,
|
||
cancelPendingCommands,
|
||
clearStopRequest: () => clearStopRequest(),
|
||
createAutoRunSessionId: () => createAutoRunSessionId(),
|
||
ensureHotmailMailboxReadyForAutoRunRound: (...args) => ensureHotmailMailboxReadyForAutoRunRound(...args),
|
||
getAutoRunStatusPayload,
|
||
getErrorMessage,
|
||
getFirstUnfinishedStep,
|
||
getPendingAutoRunTimerPlan,
|
||
getRunningSteps,
|
||
getState,
|
||
getStopRequested: () => stopRequested,
|
||
hasSavedProgress,
|
||
isAddPhoneAuthFailure,
|
||
isRestartCurrentAttemptError,
|
||
isSignupUserAlreadyExistsFailure,
|
||
isStopError,
|
||
launchAutoRunTimerPlan,
|
||
normalizeAutoRunFallbackThreadIntervalMinutes,
|
||
onAutoRunRoundSuccess: (payload = {}) => maybeSwitchIpProxyAfterAutoRunRoundSuccess(payload),
|
||
persistAutoRunTimerPlan,
|
||
resetState,
|
||
runAutoSequenceFromStep: (...args) => runAutoSequenceFromStep(...args),
|
||
runtime: {
|
||
get: () => ({
|
||
autoRunActive,
|
||
autoRunCurrentRun,
|
||
autoRunTotalRuns,
|
||
autoRunAttemptRun,
|
||
autoRunSessionId,
|
||
}),
|
||
set: (updates = {}) => {
|
||
if (updates.autoRunActive !== undefined) autoRunActive = Boolean(updates.autoRunActive);
|
||
if (updates.autoRunCurrentRun !== undefined) autoRunCurrentRun = Number(updates.autoRunCurrentRun) || 0;
|
||
if (updates.autoRunTotalRuns !== undefined) autoRunTotalRuns = Number(updates.autoRunTotalRuns) || 0;
|
||
if (updates.autoRunAttemptRun !== undefined) autoRunAttemptRun = Number(updates.autoRunAttemptRun) || 0;
|
||
if (updates.autoRunSessionId !== undefined) autoRunSessionId = normalizeAutoRunSessionId(updates.autoRunSessionId);
|
||
},
|
||
},
|
||
setState,
|
||
sleepWithStop,
|
||
throwIfAutoRunSessionStopped: (sessionId) => throwIfAutoRunSessionStopped(sessionId),
|
||
waitForRunningStepsToFinish,
|
||
throwIfStopped: () => throwIfStopped(),
|
||
chrome,
|
||
});
|
||
|
||
async function resumeAutoRunIfWaitingForEmail(options = {}) {
|
||
const { silent = false } = options;
|
||
const state = await getState();
|
||
if (!state.email || !isAutoRunPausedState(state)) {
|
||
return false;
|
||
}
|
||
|
||
if (resumeWaiter) {
|
||
if (!silent) {
|
||
await addLog('邮箱已就绪,自动继续后续步骤...', 'info');
|
||
}
|
||
resumeWaiter.resolve();
|
||
resumeWaiter = null;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function shouldStopIcloudAutoFetchRetries(error) {
|
||
if (!error) {
|
||
return false;
|
||
}
|
||
|
||
if (error.code === 'ICLOUD_TRANSIENT_CONTEXT') {
|
||
return true;
|
||
}
|
||
|
||
const message = getErrorMessage(error).toLowerCase();
|
||
if (message.includes('请先在新打开的 icloud 页面中完成登录')) {
|
||
return true;
|
||
}
|
||
return message.includes('网络/上下文波动')
|
||
|| message.includes('could not validate icloud session')
|
||
|| message.includes('status 421')
|
||
|| message.includes('failed to fetch')
|
||
|| message.includes('network request failed')
|
||
|| message.includes('networkerror')
|
||
|| message.includes('cors')
|
||
|| message.includes('address space')
|
||
|| message.includes('timed out')
|
||
|| message.includes('timeout');
|
||
}
|
||
|
||
function shouldStopEmailAutoFetchRetries(generator, error) {
|
||
if (generator === 'icloud' && shouldStopIcloudAutoFetchRetries(error)) {
|
||
return true;
|
||
}
|
||
const message = String(error?.message || '');
|
||
if (generator === 'cloudflare' && /域名/.test(message)) {
|
||
return true;
|
||
}
|
||
return generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR && /(服务地址|Admin Auth|域名)/.test(message);
|
||
}
|
||
|
||
async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
|
||
const currentState = await getState();
|
||
if (isHotmailProvider(currentState)) {
|
||
const account = await ensureHotmailAccountForFlow({
|
||
allowAllocate: true,
|
||
markUsed: true,
|
||
preferredAccountId: null,
|
||
});
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:已分配 Hotmail 账号 ${account.email}(第 ${attemptRuns} 次尝试)===`, 'ok');
|
||
return account.email;
|
||
}
|
||
|
||
if (isLuckmailProvider(currentState)) {
|
||
const purchase = await ensureLuckmailPurchaseForFlow({ allowReuse: true });
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:LuckMail 邮箱已就绪:${purchase.email_address}(第 ${attemptRuns} 次尝试)===`, 'ok');
|
||
return purchase.email_address;
|
||
}
|
||
|
||
if (isGeneratedAliasProvider(currentState)) {
|
||
if (currentState.mailProvider === GMAIL_PROVIDER) {
|
||
if (!currentState.emailPrefix) {
|
||
throw new Error('Gmail 原邮箱未设置,请先在侧边栏填写。');
|
||
}
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:Gmail +tag 模式已启用,将在步骤 3 自动生成邮箱(第 ${attemptRuns} 次尝试)===`, 'info');
|
||
return null;
|
||
}
|
||
if (!currentState.emailPrefix) {
|
||
throw new Error('2925 邮箱前缀未设置,请先在侧边栏填写。');
|
||
}
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:2925 模式已启用,将在步骤 3 自动生成邮箱(第 ${attemptRuns} 次尝试)===`, 'info');
|
||
return null;
|
||
}
|
||
|
||
if (currentState.email) {
|
||
return currentState.email;
|
||
}
|
||
|
||
if (isCustomMailProvider(currentState)) {
|
||
const poolSize = getCustomMailProviderPool(currentState).length;
|
||
if (poolSize > 0) {
|
||
const queuedEmail = getCustomMailProviderPoolEmailForRun(currentState, targetRun);
|
||
if (!queuedEmail) {
|
||
throw new Error(`自定义邮箱号池第 ${targetRun} 个邮箱不存在,请检查号池数量是否与自动轮数一致。`);
|
||
}
|
||
await setEmailState(queuedEmail);
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:自定义邮箱号池已就绪:${queuedEmail}(第 ${attemptRuns} 次尝试;第 4/8 步仍需手动输入验证码)===`, 'ok');
|
||
return queuedEmail;
|
||
}
|
||
}
|
||
|
||
if (isCustomEmailPoolGenerator(currentState)) {
|
||
const queuedEmail = getCustomEmailPoolEmailForRun(currentState, targetRun);
|
||
if (!queuedEmail) {
|
||
const poolSize = getCustomEmailPool(currentState).length;
|
||
throw new Error(
|
||
poolSize > 0
|
||
? `自定义邮箱池第 ${targetRun} 个邮箱不存在,请检查邮箱池数量是否与自动轮数一致。`
|
||
: '自定义邮箱池为空,请先至少填写 1 个邮箱。'
|
||
);
|
||
}
|
||
await setEmailState(queuedEmail);
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:自定义邮箱池已就绪:${queuedEmail}(第 ${attemptRuns} 次尝试)===`, 'ok');
|
||
return queuedEmail;
|
||
}
|
||
|
||
if (shouldUseCustomRegistrationEmail(currentState)) {
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先填写自定义注册邮箱,然后继续 ===`, 'warn');
|
||
await broadcastAutoRunStatus('waiting_email', {
|
||
currentRun: targetRun,
|
||
totalRuns,
|
||
attemptRun: attemptRuns,
|
||
});
|
||
|
||
await waitForResume();
|
||
|
||
const resumedState = await getState();
|
||
if (!resumedState.email) {
|
||
throw new Error('无法继续:当前没有注册邮箱。');
|
||
}
|
||
return resumedState.email;
|
||
}
|
||
|
||
const generator = normalizeEmailGenerator(currentState.emailGenerator);
|
||
const generatorLabel = getEmailGeneratorLabel(generator);
|
||
let lastError = null;
|
||
let attemptedFetches = 0;
|
||
for (let attempt = 1; attempt <= EMAIL_FETCH_MAX_ATTEMPTS; attempt++) {
|
||
attemptedFetches = attempt;
|
||
try {
|
||
if (attempt > 1) {
|
||
await addLog(`${generatorLabel}:正在进行第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次自动获取重试...`, 'warn');
|
||
}
|
||
const generatedEmail = await fetchGeneratedEmail(currentState, {
|
||
generateNew: generator !== 'icloud' || normalizeIcloudFetchMode(currentState.icloudFetchMode) === 'always_new',
|
||
generator,
|
||
});
|
||
await addLog(
|
||
`=== 目标 ${targetRun}/${totalRuns} 轮:${generatorLabel}已就绪:${generatedEmail}(第 ${attemptRuns} 次尝试,第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次获取)===`,
|
||
'ok'
|
||
);
|
||
return generatedEmail;
|
||
} catch (err) {
|
||
lastError = err;
|
||
await addLog(`${generatorLabel}自动获取失败(${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS}):${err.message}`, 'warn');
|
||
if (generator === 'icloud' && shouldStopIcloudAutoFetchRetries(err)) {
|
||
await addLog('iCloud:检测到会话/网络异常,本轮将停止重复重试。请先确认 iCloud 页面已登录,再点击“我已登录”或手动粘贴邮箱继续。', 'warn');
|
||
}
|
||
if (shouldStopEmailAutoFetchRetries(generator, err)) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
const totalAttempts = Math.max(1, attemptedFetches);
|
||
await addLog(`${generatorLabel}自动获取已连续失败 ${totalAttempts} 次:${lastError?.message || '未知错误'}`, 'error');
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先自动获取邮箱或手动粘贴邮箱,然后继续 ===`, 'warn');
|
||
await broadcastAutoRunStatus('waiting_email', {
|
||
currentRun: targetRun,
|
||
totalRuns,
|
||
attemptRun: attemptRuns,
|
||
});
|
||
|
||
await waitForResume();
|
||
|
||
const resumedState = await getState();
|
||
if (!resumedState.email) {
|
||
throw new Error('无法继续:当前没有邮箱地址。');
|
||
}
|
||
return resumedState.email;
|
||
}
|
||
|
||
async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
|
||
const currentState = await getState();
|
||
if (isHotmailProvider(currentState)) {
|
||
const account = await ensureHotmailAccountForFlow({
|
||
allowAllocate: true,
|
||
markUsed: true,
|
||
preferredAccountId: null,
|
||
});
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:已分配 Hotmail 账号 ${account.email}(第 ${attemptRuns} 次尝试)===`, 'ok');
|
||
return account.email;
|
||
}
|
||
|
||
if (isLuckmailProvider(currentState)) {
|
||
const purchase = await ensureLuckmailPurchaseForFlow({ allowReuse: true });
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:LuckMail 邮箱已就绪:${purchase.email_address}(第 ${attemptRuns} 次尝试)===`, 'ok');
|
||
return purchase.email_address;
|
||
}
|
||
|
||
if (isGeneratedAliasProvider(currentState)) {
|
||
if (isReusableGeneratedAliasEmail(currentState)) {
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:当前已复用 ${currentState.email},将直接继续执行(第 ${attemptRuns} 次尝试)===`, 'info');
|
||
return currentState.email;
|
||
}
|
||
|
||
let managedAliasState = currentState;
|
||
if (
|
||
String(currentState.mailProvider || '').trim().toLowerCase() === '2925'
|
||
&& Boolean(currentState.mail2925UseAccountPool)
|
||
) {
|
||
const account = await ensureMail2925AccountForFlow({
|
||
allowAllocate: true,
|
||
preferredAccountId: currentState.currentMail2925AccountId || null,
|
||
markUsed: true,
|
||
});
|
||
managedAliasState = {
|
||
...(await getState()),
|
||
currentMail2925AccountId: account.id,
|
||
};
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:已分配 2925 账号 ${account.email}(第 ${attemptRuns} 次尝试)===`, 'ok');
|
||
}
|
||
|
||
const baseEmail = getManagedAliasBaseEmail(managedAliasState);
|
||
if (!baseEmail && !managedAliasState.email) {
|
||
const baseLabel = currentState.mailProvider === GMAIL_PROVIDER ? 'Gmail 原邮箱' : '2925 基邮箱';
|
||
throw new Error(`${baseLabel}未设置,请先填写,或直接在“注册邮箱”中手动填写完整邮箱。`);
|
||
}
|
||
|
||
await addLog(
|
||
`=== 目标 ${targetRun}/${totalRuns} 轮:${currentState.mailProvider === GMAIL_PROVIDER ? 'Gmail +tag' : '2925'} 模式已启用,将在步骤 3 自动生成邮箱(第 ${attemptRuns} 次尝试)===`,
|
||
'info'
|
||
);
|
||
return null;
|
||
}
|
||
|
||
if (currentState.email) {
|
||
return currentState.email;
|
||
}
|
||
|
||
if (isCustomMailProvider(currentState)) {
|
||
const poolSize = getCustomMailProviderPool(currentState).length;
|
||
if (poolSize > 0) {
|
||
const queuedEmail = getCustomMailProviderPoolEmailForRun(currentState, targetRun);
|
||
if (!queuedEmail) {
|
||
throw new Error(`自定义邮箱号池第 ${targetRun} 个邮箱不存在,请检查号池数量是否与自动轮数一致。`);
|
||
}
|
||
await setEmailState(queuedEmail);
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:自定义邮箱号池已就绪:${queuedEmail}(第 ${attemptRuns} 次尝试;第 4/8 步仍需手动输入验证码)===`, 'ok');
|
||
return queuedEmail;
|
||
}
|
||
}
|
||
|
||
if (isCustomEmailPoolGenerator(currentState)) {
|
||
const queuedEmail = getCustomEmailPoolEmailForRun(currentState, targetRun);
|
||
if (!queuedEmail) {
|
||
const poolSize = getCustomEmailPool(currentState).length;
|
||
throw new Error(
|
||
poolSize > 0
|
||
? `自定义邮箱池第 ${targetRun} 个邮箱不存在,请检查邮箱池数量是否与自动轮数一致。`
|
||
: '自定义邮箱池为空,请先至少填写 1 个邮箱。'
|
||
);
|
||
}
|
||
await setEmailState(queuedEmail);
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:自定义邮箱池已就绪:${queuedEmail}(第 ${attemptRuns} 次尝试)===`, 'ok');
|
||
return queuedEmail;
|
||
}
|
||
|
||
if (shouldUseCustomRegistrationEmail(currentState)) {
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先填写自定义注册邮箱,然后继续 ===`, 'warn');
|
||
await broadcastAutoRunStatus('waiting_email', {
|
||
currentRun: targetRun,
|
||
totalRuns,
|
||
attemptRun: attemptRuns,
|
||
});
|
||
|
||
await waitForResume();
|
||
|
||
const resumedState = await getState();
|
||
if (!resumedState.email) {
|
||
throw new Error('无法继续:当前没有注册邮箱。');
|
||
}
|
||
return resumedState.email;
|
||
}
|
||
|
||
const generator = normalizeEmailGenerator(currentState.emailGenerator);
|
||
const generatorLabel = getEmailGeneratorLabel(generator);
|
||
let lastError = null;
|
||
let attemptedFetches = 0;
|
||
for (let attempt = 1; attempt <= EMAIL_FETCH_MAX_ATTEMPTS; attempt++) {
|
||
attemptedFetches = attempt;
|
||
try {
|
||
if (attempt > 1) {
|
||
await addLog(`${generatorLabel}:正在进行第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次自动获取重试...`, 'warn');
|
||
}
|
||
const generatedEmail = await fetchGeneratedEmail(currentState, {
|
||
generateNew: generator !== 'icloud' || normalizeIcloudFetchMode(currentState.icloudFetchMode) === 'always_new',
|
||
generator,
|
||
});
|
||
await addLog(
|
||
`=== 目标 ${targetRun}/${totalRuns} 轮:${generatorLabel}已就绪:${generatedEmail}(第 ${attemptRuns} 次尝试,第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次获取)===`,
|
||
'ok'
|
||
);
|
||
return generatedEmail;
|
||
} catch (err) {
|
||
lastError = err;
|
||
await addLog(`${generatorLabel}自动获取失败(${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS}):${err.message}`, 'warn');
|
||
if (generator === 'icloud' && shouldStopIcloudAutoFetchRetries(err)) {
|
||
await addLog('iCloud:检测到会话/网络异常,本轮将停止重复重试。请先确认 iCloud 页面已登录,再点击“我已登录”或手动粘贴邮箱继续。', 'warn');
|
||
}
|
||
if (shouldStopEmailAutoFetchRetries(generator, err)) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
const totalAttempts = Math.max(1, attemptedFetches);
|
||
await addLog(`${generatorLabel}自动获取已连续失败 ${totalAttempts} 次:${lastError?.message || '未知错误'}`, 'error');
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先自动获取邮箱或手动粘贴邮箱,然后继续 ===`, 'warn');
|
||
await broadcastAutoRunStatus('waiting_email', {
|
||
currentRun: targetRun,
|
||
totalRuns,
|
||
attemptRun: attemptRuns,
|
||
});
|
||
|
||
await waitForResume();
|
||
|
||
const resumedState = await getState();
|
||
if (!resumedState.email) {
|
||
throw new Error('无法继续:当前没有邮箱地址。');
|
||
}
|
||
return resumedState.email;
|
||
}
|
||
|
||
async function runAutoSequenceFromStep(startStep, context = {}) {
|
||
const { targetRun, totalRuns, attemptRuns, continued = false } = context;
|
||
let postStep7RestartCount = 0;
|
||
let step4RestartCount = 0;
|
||
let currentStartStep = startStep;
|
||
let continueCurrentAttempt = continued;
|
||
|
||
while (true) {
|
||
|
||
if (continueCurrentAttempt) {
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续当前进度,从步骤 ${startStep} 开始(第 ${attemptRuns} 次尝试)===`, 'info');
|
||
} else {
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,打开官网并进入密码页 ===`, 'info');
|
||
}
|
||
|
||
if (currentStartStep <= 1) {
|
||
await executeStepAndWait(1, AUTO_STEP_DELAYS[1]);
|
||
}
|
||
|
||
if (currentStartStep <= 2) {
|
||
await ensureAutoEmailReady(targetRun, totalRuns, attemptRuns);
|
||
await executeStepAndWait(2, AUTO_STEP_DELAYS[2]);
|
||
}
|
||
|
||
if (currentStartStep <= 3) {
|
||
const latestState = await getState();
|
||
const step3Status = latestState.stepStatuses?.[3] || 'pending';
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,填写密码、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info');
|
||
await broadcastAutoRunStatus('running', {
|
||
currentRun: targetRun,
|
||
totalRuns,
|
||
attemptRun: attemptRuns,
|
||
});
|
||
if (isStepDoneStatus(step3Status)) {
|
||
await addLog(`自动运行:步骤 3 当前状态为 ${step3Status},将直接继续后续流程。`, 'info');
|
||
} else {
|
||
await executeStepAndWait(3, AUTO_STEP_DELAYS[3]);
|
||
}
|
||
} else {
|
||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续执行剩余流程(第 ${attemptRuns} 次尝试)===`, 'info');
|
||
}
|
||
|
||
const signupTabId = await getTabId('signup-page');
|
||
if (signupTabId) {
|
||
await chrome.tabs.update(signupTabId, { active: true });
|
||
}
|
||
|
||
let restartFromStep1WithCurrentEmail = false;
|
||
let step = Math.max(currentStartStep, 4);
|
||
while (step <= (typeof getLastStepIdForState === 'function'
|
||
? getLastStepIdForState(await getState())
|
||
: (typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10))) {
|
||
const latestState = await getState();
|
||
if (typeof getStepDefinitionForState === 'function' && !getStepDefinitionForState(step, latestState)) {
|
||
step += 1;
|
||
continue;
|
||
}
|
||
const currentStatus = latestState.stepStatuses?.[step] || 'pending';
|
||
if (isStepDoneStatus(currentStatus)) {
|
||
await addLog(`自动运行:步骤 ${step} 当前状态为 ${currentStatus},将直接继续后续流程。`, 'info');
|
||
step += 1;
|
||
continue;
|
||
}
|
||
try {
|
||
await executeStepAndWait(step, AUTO_STEP_DELAYS[step]);
|
||
step += 1;
|
||
} catch (err) {
|
||
if (isStopError(err)) {
|
||
throw err;
|
||
}
|
||
|
||
if (step === 4) {
|
||
if (isSignupUserAlreadyExistsFailure(err)) {
|
||
throw err;
|
||
}
|
||
if (isMail2925ThreadTerminatedError(err)) {
|
||
await addLog(`步骤 4:2925 已切换账号并要求结束当前尝试:${getErrorMessage(err)}`, 'warn');
|
||
throw err;
|
||
}
|
||
step4RestartCount += 1;
|
||
const preservedState = await getState();
|
||
const preservedEmail = String(preservedState.email || '').trim();
|
||
const preservedPassword = String(preservedState.password || '').trim();
|
||
const emailSuffix = preservedEmail ? `当前邮箱:${preservedEmail};` : '';
|
||
await addLog(
|
||
`步骤 4:执行失败,准备沿用当前邮箱回到步骤 1 重新开始(第 ${step4RestartCount} 次重开)。${emailSuffix}原因:${getErrorMessage(err)}`,
|
||
'warn'
|
||
);
|
||
await invalidateDownstreamAfterStepRestart(1, {
|
||
logLabel: `步骤 4 报错后准备回到步骤 1 沿用当前邮箱重试(第 ${step4RestartCount} 次重开)`,
|
||
});
|
||
const restorePayload = {};
|
||
if (preservedEmail) restorePayload.email = preservedEmail;
|
||
if (preservedPassword) restorePayload.password = preservedPassword;
|
||
if (Object.keys(restorePayload).length) {
|
||
await setState(restorePayload);
|
||
}
|
||
currentStartStep = 1;
|
||
continueCurrentAttempt = true;
|
||
restartFromStep1WithCurrentEmail = true;
|
||
break;
|
||
}
|
||
|
||
const restartDecision = await getPostStep6AutoRestartDecision(step, err);
|
||
if (restartDecision.shouldRestart) {
|
||
postStep7RestartCount += 1;
|
||
const restartStep = restartDecision.restartStep
|
||
|| (typeof getAuthChainStartStepId === 'function'
|
||
? getAuthChainStartStepId(await getState())
|
||
: FINAL_OAUTH_CHAIN_START_STEP);
|
||
const resetAfterStep = Math.max(1, restartStep - 1);
|
||
const authState = restartDecision.authState;
|
||
const authStateLabel = authState?.state ? getLoginAuthStateLabel(authState.state) : '未知页面';
|
||
const authStateSuffix = authState?.url
|
||
? `当前认证页:${authStateLabel}(${authState.url})`
|
||
: authState?.state
|
||
? `当前认证页:${authStateLabel}`
|
||
: '未获取到认证页状态';
|
||
await addLog(
|
||
`步骤 ${step}:检测到报错且当前未进入 add-phone,正在回到步骤 ${restartStep} 重新开始授权流程(第 ${postStep7RestartCount} 次重开)。${authStateSuffix};原因:${restartDecision.errorMessage || '未知错误'}`,
|
||
'warn'
|
||
);
|
||
await invalidateDownstreamAfterStepRestart(resetAfterStep, {
|
||
logLabel: `步骤 ${step} 报错后准备回到步骤 ${restartStep} 重试(第 ${postStep7RestartCount} 次重开)`,
|
||
});
|
||
step = restartStep;
|
||
continue;
|
||
}
|
||
|
||
if (restartDecision.blockedByAddPhone) {
|
||
const addPhoneUrl = restartDecision.authState?.url || 'https://auth.openai.com/add-phone';
|
||
const authChainStartStep = typeof getAuthChainStartStepId === 'function'
|
||
? getAuthChainStartStepId(await getState())
|
||
: FINAL_OAUTH_CHAIN_START_STEP;
|
||
await addLog(`步骤 ${step}:检测到认证流程进入 add-phone(${addPhoneUrl}),停止自动回到步骤 ${authChainStartStep} 重开。`, 'warn');
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
if (restartFromStep1WithCurrentEmail) {
|
||
continue;
|
||
}
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
async function waitForResume() {
|
||
throwIfStopped();
|
||
const state = await getState();
|
||
if (state.email) {
|
||
await addLog('邮箱已就绪,自动继续后续步骤...', 'info');
|
||
return;
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
resumeWaiter = { resolve, reject };
|
||
});
|
||
}
|
||
|
||
function createAutoRunRoundSummary(round) {
|
||
return autoRunController.createAutoRunRoundSummary(round);
|
||
}
|
||
|
||
function normalizeAutoRunRoundSummary(summary, round) {
|
||
return autoRunController.normalizeAutoRunRoundSummary(summary, round);
|
||
}
|
||
|
||
function buildAutoRunRoundSummaries(totalRuns, rawSummaries = []) {
|
||
return autoRunController.buildAutoRunRoundSummaries(totalRuns, rawSummaries);
|
||
}
|
||
|
||
function serializeAutoRunRoundSummaries(totalRuns, roundSummaries = []) {
|
||
return autoRunController.serializeAutoRunRoundSummaries(totalRuns, roundSummaries);
|
||
}
|
||
|
||
function getAutoRunRoundRetryCount(summary) {
|
||
return autoRunController.getAutoRunRoundRetryCount(summary);
|
||
}
|
||
|
||
function formatAutoRunFailureReasons(reasons = []) {
|
||
return autoRunController.formatAutoRunFailureReasons(reasons);
|
||
}
|
||
|
||
async function logAutoRunFinalSummary(totalRuns, roundSummaries = []) {
|
||
return autoRunController.logAutoRunFinalSummary(totalRuns, roundSummaries);
|
||
}
|
||
|
||
async function skipAutoRunCountdown() {
|
||
return autoRunController.skipAutoRunCountdown();
|
||
}
|
||
|
||
async function waitBetweenAutoRunRounds(targetRun, totalRuns, roundSummary, options = {}) {
|
||
return autoRunController.waitBetweenAutoRunRounds(targetRun, totalRuns, roundSummary, options);
|
||
}
|
||
|
||
async function waitBeforeAutoRunRetry(targetRun, totalRuns, nextAttemptRun, options = {}) {
|
||
return autoRunController.waitBeforeAutoRunRetry(targetRun, totalRuns, nextAttemptRun, options);
|
||
}
|
||
|
||
async function handleAutoRunLoopUnhandledError(error) {
|
||
return autoRunController.handleAutoRunLoopUnhandledError(error);
|
||
}
|
||
|
||
function startAutoRunLoop(totalRuns, options = {}) {
|
||
return autoRunController.startAutoRunLoop(totalRuns, options);
|
||
}
|
||
|
||
async function autoRunLoop(totalRuns, options = {}) {
|
||
return autoRunController.autoRunLoop(totalRuns, options);
|
||
}
|
||
|
||
async function resumeAutoRun() {
|
||
throwIfStopped();
|
||
const state = await getState();
|
||
if (!state.email) {
|
||
await addLog('无法继续:当前没有邮箱地址,请先在侧边栏填写邮箱。', 'error');
|
||
return false;
|
||
}
|
||
|
||
const resumedInMemory = await resumeAutoRunIfWaitingForEmail({ silent: true });
|
||
if (resumedInMemory) {
|
||
return true;
|
||
}
|
||
|
||
if (!isAutoRunPausedState(state)) {
|
||
return false;
|
||
}
|
||
|
||
if (autoRunActive) {
|
||
return false;
|
||
}
|
||
|
||
const totalRuns = state.autoRunTotalRuns || 1;
|
||
const currentRun = state.autoRunCurrentRun || 1;
|
||
const attemptRun = state.autoRunAttemptRun || 1;
|
||
|
||
await addLog('检测到自动流程暂停上下文已丢失,正在从当前进度恢复自动运行...', 'warn');
|
||
startAutoRunLoop(totalRuns, {
|
||
autoRunSessionId: normalizeAutoRunSessionId(state.autoRunSessionId),
|
||
autoRunSkipFailures: Boolean(state.autoRunSkipFailures),
|
||
mode: 'continue',
|
||
resumeCurrentRun: currentRun,
|
||
resumeAttemptRun: attemptRun,
|
||
resumeRoundSummaries: state.autoRunRoundSummaries,
|
||
});
|
||
return true;
|
||
}
|
||
|
||
// ============================================================
|
||
// Signup / OAuth Helpers
|
||
// ============================================================
|
||
|
||
const SIGNUP_ENTRY_URL = 'https://chatgpt.com/';
|
||
const SIGNUP_PAGE_INJECT_FILES = ['content/utils.js', 'content/auth-page-recovery.js', 'content/phone-auth.js', 'content/signup-page.js'];
|
||
const panelBridge = self.MultiPageBackgroundPanelBridge?.createPanelBridge({
|
||
chrome,
|
||
addLog,
|
||
closeConflictingTabsForSource,
|
||
ensureContentScriptReadyOnTab,
|
||
getPanelMode,
|
||
normalizeCodex2ApiUrl,
|
||
normalizeSub2ApiUrl,
|
||
rememberSourceLastUrl,
|
||
sendToContentScript,
|
||
sendToContentScriptResilient,
|
||
waitForTabUrlFamily,
|
||
DEFAULT_SUB2API_GROUP_NAME,
|
||
SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
||
});
|
||
const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpers({
|
||
addLog,
|
||
buildGeneratedAliasEmail,
|
||
chrome,
|
||
ensureContentScriptReadyOnTab,
|
||
ensureHotmailAccountForFlow,
|
||
ensureMail2925AccountForFlow,
|
||
ensureLuckmailPurchaseForFlow,
|
||
getTabId,
|
||
isGeneratedAliasProvider,
|
||
isReusableGeneratedAliasEmail,
|
||
isSignupEmailVerificationPageUrl,
|
||
isRetryableContentScriptTransportError,
|
||
isHotmailProvider,
|
||
isLuckmailProvider,
|
||
isSignupPasswordPageUrl,
|
||
isTabAlive,
|
||
reuseOrCreateTab,
|
||
sendToContentScriptResilient,
|
||
setEmailState,
|
||
SIGNUP_ENTRY_URL,
|
||
SIGNUP_PAGE_INJECT_FILES,
|
||
waitForTabUrlMatch,
|
||
});
|
||
const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.createVerificationFlowHelpers({
|
||
addLog,
|
||
chrome,
|
||
closeConflictingTabsForSource,
|
||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||
completeStepFromBackground,
|
||
confirmCustomVerificationStepBypassRequest: (step) => chrome.runtime.sendMessage({
|
||
type: 'REQUEST_CUSTOM_VERIFICATION_BYPASS_CONFIRMATION',
|
||
payload: { step },
|
||
}),
|
||
getHotmailVerificationPollConfig,
|
||
getHotmailVerificationRequestTimestamp,
|
||
handleMail2925LimitReachedError,
|
||
getState,
|
||
getTabId,
|
||
HOTMAIL_PROVIDER,
|
||
isMail2925LimitReachedError,
|
||
isRetryableContentScriptTransportError,
|
||
isStopError,
|
||
LUCKMAIL_PROVIDER,
|
||
MAIL_2925_VERIFICATION_INTERVAL_MS,
|
||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
|
||
pollCloudflareTempEmailVerificationCode,
|
||
pollHotmailVerificationCode,
|
||
pollLuckmailVerificationCode,
|
||
sendToContentScript,
|
||
sendToContentScriptResilient,
|
||
sendToMailContentScriptResilient,
|
||
setState,
|
||
setStepStatus,
|
||
sleepWithStop,
|
||
throwIfStopped,
|
||
VERIFICATION_POLL_MAX_ROUNDS,
|
||
});
|
||
const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.createPhoneVerificationHelpers({
|
||
addLog,
|
||
DEFAULT_HERO_SMS_BASE_URL,
|
||
DEFAULT_HERO_SMS_REUSE_ENABLED,
|
||
DEFAULT_PHONE_CODE_WAIT_SECONDS,
|
||
DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS,
|
||
DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS,
|
||
DEFAULT_PHONE_CODE_POLL_ROUNDS,
|
||
ensureStep8SignupPageReady,
|
||
getOAuthFlowStepTimeoutMs,
|
||
getState,
|
||
HERO_SMS_COUNTRY_ID,
|
||
HERO_SMS_COUNTRY_LABEL,
|
||
HERO_SMS_SERVICE_CODE,
|
||
HERO_SMS_SERVICE_LABEL,
|
||
sendToContentScriptResilient,
|
||
setState,
|
||
sleepWithStop,
|
||
throwIfStopped,
|
||
});
|
||
const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({
|
||
addLog,
|
||
completeStepFromBackground,
|
||
openSignupEntryTab,
|
||
});
|
||
const step2Executor = self.MultiPageBackgroundStep2?.createStep2Executor({
|
||
addLog,
|
||
chrome,
|
||
completeStepFromBackground,
|
||
ensureContentScriptReadyOnTab,
|
||
ensureSignupAuthEntryPageReady,
|
||
ensureSignupEntryPageReady,
|
||
ensureSignupPostEmailPageReadyInTab,
|
||
getTabId,
|
||
isTabAlive,
|
||
resolveSignupEmailForFlow,
|
||
sendToContentScriptResilient,
|
||
SIGNUP_PAGE_INJECT_FILES,
|
||
});
|
||
const step3Executor = self.MultiPageBackgroundStep3?.createStep3Executor({
|
||
addLog,
|
||
chrome,
|
||
ensureContentScriptReadyOnTab,
|
||
generatePassword,
|
||
getTabId,
|
||
isTabAlive,
|
||
sendToContentScript,
|
||
setPasswordState,
|
||
setState,
|
||
SIGNUP_PAGE_INJECT_FILES,
|
||
});
|
||
|
||
async function ensureIcloudMailSessionForVerification(options = {}) {
|
||
const flowState = options?.state || await getState().catch(() => ({}));
|
||
const hostPreference = getConfiguredIcloudHostPreference(flowState)
|
||
|| normalizeIcloudHost(flowState?.preferredIcloudHost);
|
||
return checkIcloudSession({
|
||
...(hostPreference ? { hostPreference } : {}),
|
||
actionLabel: options?.actionLabel || '检查 iCloud 会话',
|
||
});
|
||
}
|
||
|
||
const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({
|
||
addLog,
|
||
chrome,
|
||
completeStepFromBackground,
|
||
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
|
||
ensureMail2925MailboxSession,
|
||
ensureIcloudMailSession: ensureIcloudMailSessionForVerification,
|
||
getMailConfig,
|
||
getTabId,
|
||
HOTMAIL_PROVIDER,
|
||
isTabAlive,
|
||
LUCKMAIL_PROVIDER,
|
||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||
resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep,
|
||
reuseOrCreateTab,
|
||
sendToContentScriptResilient,
|
||
shouldUseCustomRegistrationEmail,
|
||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||
throwIfStopped,
|
||
});
|
||
const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({
|
||
addLog,
|
||
generateRandomBirthday,
|
||
generateRandomName,
|
||
sendToContentScript,
|
||
});
|
||
const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({
|
||
completeStepFromBackground,
|
||
runPreStep6CookieCleanup,
|
||
});
|
||
const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({
|
||
addLog,
|
||
completeStepFromBackground,
|
||
getErrorMessage,
|
||
getLoginAuthStateLabel,
|
||
getOAuthFlowStepTimeoutMs,
|
||
getState,
|
||
isAddPhoneAuthFailure,
|
||
isStep6RecoverableResult,
|
||
isStep6SuccessResult,
|
||
refreshOAuthUrlBeforeStep6,
|
||
reuseOrCreateTab,
|
||
sendToContentScriptResilient,
|
||
startOAuthFlowTimeoutWindow,
|
||
STEP6_MAX_ATTEMPTS,
|
||
throwIfStopped,
|
||
});
|
||
const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
|
||
addLog,
|
||
chrome,
|
||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||
completeStepFromBackground,
|
||
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
|
||
ensureMail2925MailboxSession,
|
||
ensureIcloudMailSession: ensureIcloudMailSessionForVerification,
|
||
ensureStep8VerificationPageReady,
|
||
getOAuthFlowRemainingMs,
|
||
getOAuthFlowStepTimeoutMs,
|
||
getPanelMode,
|
||
getMailConfig,
|
||
getState,
|
||
getTabId,
|
||
HOTMAIL_PROVIDER,
|
||
isTabAlive,
|
||
isVerificationMailPollingError,
|
||
LUCKMAIL_PROVIDER,
|
||
resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep,
|
||
rerunStep7ForStep8Recovery: (...args) => rerunStep7ForStep8Recovery(...args),
|
||
reuseOrCreateTab,
|
||
setState,
|
||
shouldUseCustomRegistrationEmail,
|
||
sleepWithStop,
|
||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
|
||
throwIfStopped,
|
||
});
|
||
const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.createPlusCheckoutCreateExecutor({
|
||
addLog,
|
||
chrome,
|
||
completeStepFromBackground,
|
||
ensureContentScriptReadyOnTabUntilStopped,
|
||
registerTab,
|
||
sendTabMessageUntilStopped,
|
||
setState,
|
||
sleepWithStop,
|
||
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 goPayManualConfirmExecutor = self.MultiPageBackgroundGoPayManualConfirm?.createGoPayManualConfirmExecutor({
|
||
addLog,
|
||
broadcastDataUpdate,
|
||
chrome,
|
||
getTabId,
|
||
isTabAlive,
|
||
registerTab,
|
||
setState,
|
||
});
|
||
const payPalApproveExecutor = self.MultiPageBackgroundPayPalApprove?.createPayPalApproveExecutor({
|
||
addLog,
|
||
chrome,
|
||
completeStepFromBackground,
|
||
ensureContentScriptReadyOnTabUntilStopped,
|
||
getTabId,
|
||
isTabAlive,
|
||
sendTabMessageUntilStopped,
|
||
setState,
|
||
sleepWithStop,
|
||
waitForTabCompleteUntilStopped,
|
||
waitForTabUrlMatchUntilStopped,
|
||
});
|
||
const plusReturnConfirmExecutor = self.MultiPageBackgroundPlusReturnConfirm?.createPlusReturnConfirmExecutor({
|
||
addLog,
|
||
completeStepFromBackground,
|
||
getTabId,
|
||
isTabAlive,
|
||
setState,
|
||
sleepWithStop,
|
||
waitForTabUrlMatchUntilStopped,
|
||
});
|
||
const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({
|
||
addLog,
|
||
chrome,
|
||
closeConflictingTabsForSource,
|
||
completeStepFromBackground,
|
||
ensureContentScriptReadyOnTab,
|
||
getPanelMode,
|
||
getTabId,
|
||
isLocalhostOAuthCallbackUrl,
|
||
isTabAlive,
|
||
normalizeCodex2ApiUrl,
|
||
normalizeSub2ApiUrl,
|
||
rememberSourceLastUrl,
|
||
reuseOrCreateTab,
|
||
sendToContentScript,
|
||
sendToContentScriptResilient,
|
||
shouldBypassStep9ForLocalCpa,
|
||
SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||
});
|
||
const stepExecutorsByKey = {
|
||
'open-chatgpt': () => step1Executor.executeStep1(),
|
||
'submit-signup-email': (state) => step2Executor.executeStep2(state),
|
||
'fill-password': (state) => step3Executor.executeStep3(state),
|
||
'fetch-signup-code': (state) => step4Executor.executeStep4(state),
|
||
'fill-profile': (state) => step5Executor.executeStep5(state),
|
||
'clear-login-cookies': () => step6Executor.executeStep6(),
|
||
'plus-checkout-create': (state) => plusCheckoutCreateExecutor.executePlusCheckoutCreate(state),
|
||
'plus-checkout-billing': (state) => plusCheckoutBillingExecutor.executePlusCheckoutBilling(state),
|
||
'gopay-subscription-confirm': (state) => goPayManualConfirmExecutor.executeGoPayManualConfirm(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),
|
||
'platform-verify': (state) => executeStep10(state),
|
||
};
|
||
const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({
|
||
addLog,
|
||
appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args),
|
||
batchUpdateLuckmailPurchases,
|
||
buildLocalhostCleanupPrefix,
|
||
buildLuckmailSessionSettingsPayload,
|
||
buildPersistentSettingsPayload,
|
||
broadcastDataUpdate,
|
||
applyIpProxySettingsFromState,
|
||
cancelScheduledAutoRun,
|
||
checkIcloudSession,
|
||
clearAccountRunHistory: (...args) => clearAndBroadcastAccountRunHistory(...args),
|
||
deleteAccountRunHistoryRecords: (...args) => deleteAndBroadcastAccountRunHistoryRecords(...args),
|
||
clearAutoRunTimerAlarm,
|
||
clearLuckmailRuntimeState,
|
||
clearStopRequest,
|
||
closeLocalhostCallbackTabs,
|
||
closeTabsByUrlPrefix,
|
||
completeStepFromBackground,
|
||
deleteHotmailAccount,
|
||
deleteHotmailAccounts,
|
||
deleteIcloudAlias,
|
||
deleteUsedIcloudAliases,
|
||
findPayPalAccount,
|
||
disableUsedLuckmailPurchases,
|
||
doesStepUseCompletionSignal,
|
||
ensureMail2925MailboxSession,
|
||
ensureManualInteractionAllowed,
|
||
executeStep,
|
||
executeStepViaCompletionSignal,
|
||
exportSettingsBundle,
|
||
fetchGeneratedEmail,
|
||
finalizePhoneActivationAfterSuccessfulFlow,
|
||
finalizeStep3Completion: async () => {
|
||
const currentState = await getState();
|
||
const signupTabId = await getTabId('signup-page');
|
||
return signupFlowHelpers.finalizeSignupPasswordSubmitInTab(
|
||
signupTabId,
|
||
currentState.password || currentState.customPassword || '',
|
||
3
|
||
);
|
||
},
|
||
finalizeIcloudAliasAfterSuccessfulFlow,
|
||
findHotmailAccount,
|
||
flushCommand,
|
||
getCurrentLuckmailPurchase,
|
||
getPendingAutoRunTimerPlan,
|
||
getSourceLabel,
|
||
getState,
|
||
getStepDefinitionForState,
|
||
getStepIdsForState,
|
||
getLastStepIdForState,
|
||
getTabId,
|
||
getStopRequested: () => stopRequested,
|
||
handleCloudflareSecurityBlocked,
|
||
handleAutoRunLoopUnhandledError,
|
||
importSettingsBundle,
|
||
invalidateDownstreamAfterStepRestart,
|
||
isCloudflareSecurityBlockedError: isTerminalSecurityBlockedError,
|
||
isAutoRunLockedState,
|
||
isHotmailProvider,
|
||
isLocalhostOAuthCallbackUrl,
|
||
isLuckmailProvider,
|
||
isStopError,
|
||
isTabAlive,
|
||
launchAutoRunTimerPlan,
|
||
listIcloudAliases,
|
||
listLuckmailPurchasesForManagement,
|
||
refreshIpProxyPool,
|
||
getCurrentPayPalAccount,
|
||
getCurrentMail2925Account,
|
||
normalizeHotmailAccounts,
|
||
normalizeMail2925Accounts,
|
||
normalizePayPalAccounts,
|
||
normalizeRunCount,
|
||
AUTO_RUN_TIMER_KIND_SCHEDULED_START,
|
||
notifyStepComplete,
|
||
notifyStepError,
|
||
patchHotmailAccount,
|
||
patchMail2925Account,
|
||
registerTab,
|
||
requestStop,
|
||
probeIpProxyExit,
|
||
resetState,
|
||
resumeAutoRun,
|
||
scheduleAutoRun,
|
||
selectLuckmailPurchase,
|
||
switchIpProxy,
|
||
changeIpProxyExit,
|
||
setCurrentPayPalAccount,
|
||
setCurrentHotmailAccount,
|
||
setCurrentMail2925Account,
|
||
setContributionMode,
|
||
setEmailState,
|
||
setEmailStateSilently,
|
||
setIcloudAliasPreservedState,
|
||
setIcloudAliasUsedState,
|
||
setLuckmailPurchaseDisabledState,
|
||
setLuckmailPurchasePreservedState,
|
||
setLuckmailPurchaseUsedState,
|
||
setPersistentSettings,
|
||
setState,
|
||
setStepStatus,
|
||
skipAutoRunCountdown,
|
||
skipStep,
|
||
startContributionFlow: (...args) => contributionOAuthManager?.startContributionFlow?.(...args),
|
||
startAutoRunLoop,
|
||
pollContributionStatus: (...args) => contributionOAuthManager?.pollContributionStatus?.(...args),
|
||
syncHotmailAccounts,
|
||
syncPayPalAccounts,
|
||
deleteMail2925Account,
|
||
deleteMail2925Accounts,
|
||
testHotmailAccountMailAccess,
|
||
upsertPayPalAccount,
|
||
upsertMail2925Account,
|
||
upsertHotmailAccount,
|
||
verifyHotmailAccount,
|
||
});
|
||
|
||
function buildStepRegistry(definitions = []) {
|
||
return self.MultiPageBackgroundStepRegistry?.createStepRegistry(
|
||
definitions.map((definition) => ({
|
||
...definition,
|
||
execute: stepExecutorsByKey[definition.key],
|
||
}))
|
||
);
|
||
}
|
||
|
||
const normalStepRegistry = buildStepRegistry(NORMAL_STEP_DEFINITIONS);
|
||
const plusPayPalStepRegistry = buildStepRegistry(PLUS_PAYPAL_STEP_DEFINITIONS);
|
||
const plusGoPayStepRegistry = buildStepRegistry(PLUS_GOPAY_STEP_DEFINITIONS);
|
||
|
||
function getStepRegistryForState(state = {}) {
|
||
if (!isPlusModeState(state)) {
|
||
return normalStepRegistry;
|
||
}
|
||
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay'
|
||
? plusGoPayStepRegistry
|
||
: plusPayPalStepRegistry;
|
||
}
|
||
|
||
async function requestOAuthUrlFromPanel(state, options = {}) {
|
||
return panelBridge.requestOAuthUrlFromPanel(state, options);
|
||
}
|
||
|
||
async function requestCpaOAuthUrl(state, options = {}) {
|
||
return panelBridge.requestCpaOAuthUrl(state, options);
|
||
}
|
||
|
||
async function requestSub2ApiOAuthUrl(state, options = {}) {
|
||
return panelBridge.requestSub2ApiOAuthUrl(state, options);
|
||
}
|
||
|
||
async function openSignupEntryTab(step = 1) {
|
||
return signupFlowHelpers.openSignupEntryTab(step);
|
||
}
|
||
|
||
async function ensureSignupEntryPageReady(step = 1) {
|
||
return signupFlowHelpers.ensureSignupEntryPageReady(step);
|
||
}
|
||
|
||
async function ensureSignupAuthEntryPageReady(step = 1) {
|
||
return signupFlowHelpers.ensureSignupEntryPageReady(step);
|
||
}
|
||
|
||
async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) {
|
||
return signupFlowHelpers.ensureSignupPasswordPageReadyInTab(tabId, step, options);
|
||
}
|
||
|
||
async function ensureSignupPostEmailPageReadyInTab(tabId, step = 2, options = {}) {
|
||
return signupFlowHelpers.ensureSignupPostEmailPageReadyInTab(tabId, step, options);
|
||
}
|
||
|
||
async function resolveSignupEmailForFlow(state) {
|
||
return signupFlowHelpers.resolveSignupEmailForFlow(state);
|
||
}
|
||
|
||
// ============================================================
|
||
// Step 1: Open ChatGPT homepage
|
||
// ============================================================
|
||
|
||
async function executeStep1() {
|
||
return step1Executor.executeStep1();
|
||
}
|
||
|
||
// ============================================================
|
||
// Step 2: Click signup, fill email, continue to password page
|
||
// ============================================================
|
||
|
||
async function executeStep2(state) {
|
||
return step2Executor.executeStep2(state);
|
||
}
|
||
|
||
// ============================================================
|
||
// Step 3: Fill Password (via signup-page.js)
|
||
// ============================================================
|
||
|
||
async function executeStep3(state) {
|
||
return step3Executor.executeStep3(state);
|
||
}
|
||
|
||
// ============================================================
|
||
// Step 4: Get Signup Verification Code (qq-mail.js polls, then fills in signup-page.js)
|
||
// ============================================================
|
||
|
||
function getMailConfig(state) {
|
||
const provider = state.mailProvider || 'qq';
|
||
if (provider === 'custom') {
|
||
return { provider: 'custom', label: '自定义邮箱' };
|
||
}
|
||
if (provider === HOTMAIL_PROVIDER) {
|
||
return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(API对接/本地助手)' };
|
||
}
|
||
if (provider === ICLOUD_PROVIDER) {
|
||
const configuredHost = getConfiguredIcloudHostPreference(state)
|
||
|| normalizeIcloudHost(state?.preferredIcloudHost)
|
||
|| 'icloud.com';
|
||
const targetMailboxType = normalizeIcloudTargetMailboxType(state?.icloudTargetMailboxType);
|
||
const useForwardMailbox = targetMailboxType === 'forward-mailbox';
|
||
if (useForwardMailbox) {
|
||
const forwardProvider = normalizeIcloudForwardMailProvider(state?.icloudForwardMailProvider);
|
||
const forwardConfig = getSharedIcloudForwardMailConfig(forwardProvider);
|
||
return {
|
||
...forwardConfig,
|
||
label: `iCloud 转发(${forwardConfig.label})`,
|
||
icloudForwarding: true,
|
||
};
|
||
}
|
||
const loginUrl = getIcloudLoginUrlForHost(configuredHost) || 'https://www.icloud.com/';
|
||
const mailUrl = getIcloudMailUrlForHost(configuredHost) || loginUrl;
|
||
return {
|
||
source: 'icloud-mail',
|
||
url: mailUrl,
|
||
label: 'iCloud 邮箱',
|
||
navigateOnReuse: true,
|
||
};
|
||
}
|
||
if (provider === GMAIL_PROVIDER) {
|
||
return {
|
||
source: 'gmail-mail',
|
||
url: 'https://mail.google.com/mail/u/0/#inbox',
|
||
label: 'Gmail 邮箱',
|
||
inject: ['content/activation-utils.js', 'content/utils.js', 'content/gmail-mail.js'],
|
||
injectSource: 'gmail-mail',
|
||
};
|
||
}
|
||
if (provider === LUCKMAIL_PROVIDER) {
|
||
return { provider: LUCKMAIL_PROVIDER, label: 'LuckMail(API 购邮)' };
|
||
}
|
||
if (provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
|
||
return { provider: CLOUDFLARE_TEMP_EMAIL_PROVIDER, label: 'Cloudflare Temp Email' };
|
||
}
|
||
if (provider === '163') {
|
||
return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 邮箱' };
|
||
}
|
||
if (provider === '163-vip') {
|
||
return { source: 'mail-163', url: 'https://webmail.vip.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 VIP 邮箱' };
|
||
}
|
||
if (provider === '126') {
|
||
return { source: 'mail-163', url: 'https://mail.126.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '126 邮箱' };
|
||
}
|
||
if (provider === 'inbucket') {
|
||
const host = normalizeInbucketOrigin(state.inbucketHost);
|
||
const mailbox = (state.inbucketMailbox || '').trim();
|
||
if (!host) {
|
||
return { error: 'Inbucket 主机地址为空或无效。' };
|
||
}
|
||
if (!mailbox) {
|
||
return { error: 'Inbucket 邮箱名称为空。' };
|
||
}
|
||
return {
|
||
source: 'inbucket-mail',
|
||
url: `${host}/m/${encodeURIComponent(mailbox)}/`,
|
||
label: `Inbucket 邮箱(${mailbox})`,
|
||
navigateOnReuse: true,
|
||
inject: ['content/activation-utils.js', 'content/utils.js', 'content/inbucket-mail.js'],
|
||
injectSource: 'inbucket-mail',
|
||
};
|
||
}
|
||
if (provider === '2925') {
|
||
return {
|
||
provider: '2925',
|
||
source: 'mail-2925',
|
||
url: 'https://2925.com/#/mailList',
|
||
label: '2925 邮箱',
|
||
inject: ['content/utils.js', 'content/mail-2925.js'],
|
||
injectSource: 'mail-2925',
|
||
};
|
||
}
|
||
return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ 邮箱' };
|
||
}
|
||
|
||
function normalizeInbucketOrigin(rawValue) {
|
||
const value = (rawValue || '').trim();
|
||
if (!value) return '';
|
||
|
||
const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`;
|
||
|
||
try {
|
||
const parsed = new URL(candidate);
|
||
return parsed.origin;
|
||
} catch {
|
||
return '';
|
||
}
|
||
}
|
||
|
||
function getVerificationCodeStateKey(step) {
|
||
return verificationFlowHelpers.getVerificationCodeStateKey(step);
|
||
}
|
||
|
||
function getVerificationCodeLabel(step) {
|
||
return verificationFlowHelpers.getVerificationCodeLabel(step);
|
||
}
|
||
|
||
async function confirmCustomVerificationStepBypass(step) {
|
||
return verificationFlowHelpers.confirmCustomVerificationStepBypass(step);
|
||
}
|
||
|
||
function getVerificationPollPayload(step, state, overrides = {}) {
|
||
return verificationFlowHelpers.getVerificationPollPayload(step, state, overrides);
|
||
}
|
||
|
||
async function requestVerificationCodeResend(step) {
|
||
return verificationFlowHelpers.requestVerificationCodeResend(step);
|
||
}
|
||
|
||
async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) {
|
||
return verificationFlowHelpers.pollFreshVerificationCode(step, state, mail, pollOverrides);
|
||
}
|
||
|
||
async function pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides = {}) {
|
||
return verificationFlowHelpers.pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides);
|
||
}
|
||
|
||
async function submitVerificationCode(step, code) {
|
||
return verificationFlowHelpers.submitVerificationCode(step, code);
|
||
}
|
||
|
||
async function resolveVerificationStep(step, state, mail, options = {}) {
|
||
return verificationFlowHelpers.resolveVerificationStep(step, state, mail, options);
|
||
}
|
||
|
||
async function executeStep4(state) {
|
||
return step4Executor.executeStep4(state);
|
||
}
|
||
|
||
// ============================================================
|
||
// Step 5: Fill Name & Birthday (via signup-page.js)
|
||
// ============================================================
|
||
|
||
async function executeStep5(state) {
|
||
return step5Executor.executeStep5(state);
|
||
}
|
||
|
||
// ============================================================
|
||
// Step 6 Cookie Cleanup
|
||
// ============================================================
|
||
|
||
function normalizeCookieDomainForMatch(domain) {
|
||
return String(domain || '').trim().replace(/^\.+/, '').toLowerCase();
|
||
}
|
||
|
||
function shouldClearPreLoginCookie(cookie) {
|
||
const domain = normalizeCookieDomainForMatch(cookie?.domain);
|
||
if (!domain) return false;
|
||
return PRE_LOGIN_COOKIE_CLEAR_DOMAINS.some((target) => (
|
||
domain === target || domain.endsWith(`.${target}`)
|
||
));
|
||
}
|
||
|
||
function buildCookieRemovalUrl(cookie) {
|
||
const host = normalizeCookieDomainForMatch(cookie?.domain);
|
||
const path = String(cookie?.path || '/').startsWith('/')
|
||
? String(cookie?.path || '/')
|
||
: `/${String(cookie?.path || '')}`;
|
||
return `https://${host}${path}`;
|
||
}
|
||
|
||
async function collectCookiesForPreLoginCleanup() {
|
||
if (!chrome.cookies?.getAll) {
|
||
return [];
|
||
}
|
||
|
||
const stores = chrome.cookies.getAllCookieStores
|
||
? await chrome.cookies.getAllCookieStores()
|
||
: [{ id: undefined }];
|
||
const cookies = [];
|
||
const seen = new Set();
|
||
|
||
for (const store of stores) {
|
||
const storeId = store?.id;
|
||
const batch = await chrome.cookies.getAll(storeId ? { storeId } : {});
|
||
for (const cookie of batch || []) {
|
||
if (!shouldClearPreLoginCookie(cookie)) continue;
|
||
const key = [
|
||
cookie.storeId || storeId || '',
|
||
cookie.domain || '',
|
||
cookie.path || '',
|
||
cookie.name || '',
|
||
cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
|
||
].join('|');
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
cookies.push(cookie);
|
||
}
|
||
}
|
||
|
||
return cookies;
|
||
}
|
||
|
||
async function removeCookieDirectly(cookie) {
|
||
const details = {
|
||
url: buildCookieRemovalUrl(cookie),
|
||
name: cookie.name,
|
||
};
|
||
|
||
if (cookie.storeId) {
|
||
details.storeId = cookie.storeId;
|
||
}
|
||
if (cookie.partitionKey) {
|
||
details.partitionKey = cookie.partitionKey;
|
||
}
|
||
|
||
try {
|
||
const result = await chrome.cookies.remove(details);
|
||
return Boolean(result);
|
||
} catch (err) {
|
||
console.warn(LOG_PREFIX, '[removeCookieDirectly] failed', {
|
||
domain: cookie?.domain,
|
||
name: cookie?.name,
|
||
message: getErrorMessage(err),
|
||
});
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function runPreStep6CookieCleanup() {
|
||
await addLog(
|
||
`步骤 6:开始前等待 ${Math.round(STEP6_PRE_LOGIN_COOKIE_CLEAR_DELAY_MS / 1000)} 秒,然后直接删除 ChatGPT / OpenAI cookies...`,
|
||
'info'
|
||
);
|
||
|
||
await sleepWithStop(STEP6_PRE_LOGIN_COOKIE_CLEAR_DELAY_MS);
|
||
|
||
if (!chrome.cookies?.getAll || !chrome.cookies?.remove) {
|
||
await addLog('步骤 6:当前浏览器不支持 cookies API,无法直接删除 cookies。', 'warn');
|
||
return;
|
||
}
|
||
|
||
const cookies = await collectCookiesForPreLoginCleanup();
|
||
let removedCount = 0;
|
||
|
||
for (const cookie of cookies) {
|
||
throwIfStopped();
|
||
if (await removeCookieDirectly(cookie)) {
|
||
removedCount += 1;
|
||
}
|
||
}
|
||
|
||
if (chrome.browsingData?.removeCookies) {
|
||
try {
|
||
await chrome.browsingData.removeCookies({
|
||
since: 0,
|
||
origins: PRE_LOGIN_COOKIE_CLEAR_ORIGINS,
|
||
});
|
||
} catch (err) {
|
||
await addLog(`步骤 6:browsingData 补扫 cookies 失败:${getErrorMessage(err)}`, 'warn');
|
||
}
|
||
}
|
||
|
||
await addLog(`步骤 6:已直接删除 ${removedCount} 个 ChatGPT / OpenAI cookies,准备继续获取链接并登录。`, 'ok');
|
||
}
|
||
|
||
// ============================================================
|
||
// Step 7: Login and ensure the auth page reaches the login verification page
|
||
// ============================================================
|
||
|
||
async function refreshOAuthUrlBeforeStep6(state, options = {}) {
|
||
const visibleStep = Number(options.visibleStep) || Number(state?.visibleStep) || 7;
|
||
if (state?.contributionModeExpected && !state?.contributionMode) {
|
||
throw new Error(`步骤 ${visibleStep}:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入贡献模式后再点击自动。`);
|
||
}
|
||
if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) {
|
||
await addLog(`步骤 ${visibleStep}:contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...`, 'info');
|
||
const contributionState = await contributionOAuthManager.startContributionFlow({
|
||
nickname: state.contributionNickname || '',
|
||
openAuthTab: false,
|
||
stateOverride: state,
|
||
});
|
||
const oauthUrl = String(contributionState?.contributionAuthUrl || '').trim();
|
||
if (!oauthUrl) {
|
||
throw new Error('贡献模式未返回可用的登录地址,请稍后重试。');
|
||
}
|
||
await handleStepData(1, { oauthUrl });
|
||
return oauthUrl;
|
||
}
|
||
await addLog(`步骤 ${visibleStep}:contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info');
|
||
console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel');
|
||
const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: `步骤 ${visibleStep}` });
|
||
await handleStepData(1, refreshResult);
|
||
|
||
if (!refreshResult?.oauthUrl) {
|
||
throw new Error('刷新 OAuth 链接后仍未拿到可用链接。');
|
||
}
|
||
|
||
return refreshResult.oauthUrl;
|
||
}
|
||
|
||
function buildOAuthFlowTimeoutError(step, actionLabel = '后续授权流程', state = {}) {
|
||
const restartStep = typeof getAuthChainStartStepId === 'function'
|
||
? getAuthChainStartStepId(state)
|
||
: FINAL_OAUTH_CHAIN_START_STEP;
|
||
return new Error(
|
||
`步骤 ${step}:从拿到 OAuth 登录地址开始,${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟内未完成${actionLabel},结束当前链路,准备从步骤 ${restartStep} 重新开始。`
|
||
);
|
||
}
|
||
|
||
function normalizeOAuthFlowDeadlineAt(value) {
|
||
const numeric = Number(value);
|
||
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||
return null;
|
||
}
|
||
return Math.floor(numeric);
|
||
}
|
||
|
||
function normalizeOAuthFlowSourceUrl(value) {
|
||
const normalized = String(value || '').trim();
|
||
return normalized || null;
|
||
}
|
||
|
||
async function startOAuthFlowTimeoutWindow(options = {}) {
|
||
const step = Number(options.step) || 7;
|
||
const deadlineAt = Date.now() + OAUTH_FLOW_TIMEOUT_MS;
|
||
await setState({
|
||
oauthFlowDeadlineAt: deadlineAt,
|
||
oauthFlowDeadlineSourceUrl: normalizeOAuthFlowSourceUrl(options.oauthUrl),
|
||
});
|
||
await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,开始 ${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟倒计时。`, 'info');
|
||
return deadlineAt;
|
||
}
|
||
|
||
async function getOAuthFlowRemainingMs(options = {}) {
|
||
const step = Number(options.step) || 7;
|
||
const actionLabel = String(options.actionLabel || '后续授权流程').trim() || '后续授权流程';
|
||
const state = options.state || await getState();
|
||
const deadlineAt = normalizeOAuthFlowDeadlineAt(state?.oauthFlowDeadlineAt);
|
||
const deadlineSourceUrl = normalizeOAuthFlowSourceUrl(state?.oauthFlowDeadlineSourceUrl);
|
||
const currentOauthUrl = normalizeOAuthFlowSourceUrl(options.oauthUrl !== undefined ? options.oauthUrl : state?.oauthUrl);
|
||
if (!deadlineAt) {
|
||
return null;
|
||
}
|
||
|
||
if (deadlineSourceUrl && currentOauthUrl && deadlineSourceUrl !== currentOauthUrl) {
|
||
console.warn(LOG_PREFIX, '[oauth-flow] ignoring stale deadline due to oauth url mismatch', {
|
||
step,
|
||
actionLabel,
|
||
deadlineSourceUrl,
|
||
currentOauthUrl,
|
||
});
|
||
return null;
|
||
}
|
||
|
||
const remainingMs = deadlineAt - Date.now();
|
||
if (remainingMs <= 0) {
|
||
throw buildOAuthFlowTimeoutError(step, actionLabel, state);
|
||
}
|
||
|
||
return remainingMs;
|
||
}
|
||
|
||
async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) {
|
||
const normalizedDefault = Math.max(1000, Number(defaultTimeoutMs) || 1000);
|
||
const reserveMs = Math.max(0, Number(options.reserveMs) || 0);
|
||
const remainingMs = await getOAuthFlowRemainingMs(options);
|
||
if (remainingMs === null) {
|
||
return normalizedDefault;
|
||
}
|
||
|
||
const budgetMs = remainingMs - reserveMs;
|
||
if (budgetMs <= 0) {
|
||
const stateForError = options.state || await getState();
|
||
throw buildOAuthFlowTimeoutError(
|
||
Number(options.step) || 7,
|
||
String(options.actionLabel || '后续授权流程').trim() || '后续授权流程',
|
||
stateForError
|
||
);
|
||
}
|
||
|
||
return Math.max(1000, Math.min(normalizedDefault, budgetMs));
|
||
}
|
||
|
||
function isStep6SuccessResult(result) {
|
||
return result?.step6Outcome === 'success';
|
||
}
|
||
|
||
function isStep6RecoverableResult(result) {
|
||
return result?.step6Outcome === 'recoverable';
|
||
}
|
||
|
||
function isAddPhoneAuthUrl(url) {
|
||
return /https:\/\/auth\.openai\.com\/(?:add-phone|phone-verification)(?:[/?#]|$)/i.test(String(url || '').trim());
|
||
}
|
||
|
||
function isAddPhoneAuthState(authState = {}) {
|
||
return authState?.state === 'add_phone_page'
|
||
|| authState?.state === 'phone_verification_page'
|
||
|| Boolean(authState?.addPhonePage)
|
||
|| Boolean(authState?.phoneVerificationPage)
|
||
|| isAddPhoneAuthUrl(authState?.url);
|
||
}
|
||
|
||
async function getPostStep6AutoRestartDecision(step, error) {
|
||
const resolveStepKey = (stepId, state) => {
|
||
if (typeof getStepExecutionKeyForState === 'function') {
|
||
return getStepExecutionKeyForState(stepId, state);
|
||
}
|
||
return String(
|
||
typeof getStepDefinitionForState === 'function'
|
||
? (getStepDefinitionForState(stepId, state)?.key || '')
|
||
: ''
|
||
).trim();
|
||
};
|
||
const findStepIdByKeyForState = (targetKey, state = {}) => {
|
||
const normalizedKey = String(targetKey || '').trim();
|
||
if (!normalizedKey) {
|
||
return null;
|
||
}
|
||
const stepIds = typeof getStepIdsForState === 'function'
|
||
? getStepIdsForState(state)
|
||
: [];
|
||
for (const stepId of stepIds) {
|
||
if (resolveStepKey(stepId, state) === normalizedKey) {
|
||
return Number(stepId);
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
const isPlatformVerifyTransientRetryError = (errorMessage = '') => {
|
||
const normalizedMessage = String(errorMessage || '');
|
||
const mentionsTokenExchange = /auth\.openai\.com\/oauth\/token/i.test(normalizedMessage);
|
||
const hasTransientNetworkSignal = /connect:\s*connection refused|failed to fetch|i\/o timeout|context deadline exceeded|eof|connection reset by peer/i.test(normalizedMessage);
|
||
return mentionsTokenExchange && hasTransientNetworkSignal;
|
||
};
|
||
const isPhoneVerificationLocalFailure = (errorMessage = '') => {
|
||
const normalizedMessage = String(errorMessage || '');
|
||
return /HeroSMS|phone verification did not succeed|number replacements|sms_timeout_after_resend|phone number is already linked|add-phone keeps rejecting current number|接码|手机号|手机验证码|步骤\s*9.*(?:手机号|验证码)|Step\s*9.*phone verification/i.test(normalizedMessage);
|
||
};
|
||
|
||
const normalizedStep = Number(step);
|
||
const errorMessage = getErrorMessage(error);
|
||
const shouldForceRestartFromStep7 = /restart step 7 with a new number/i.test(errorMessage);
|
||
const latestState = await getState();
|
||
const authChainStartStep = typeof getAuthChainStartStepId === 'function'
|
||
? getAuthChainStartStepId(latestState)
|
||
: FINAL_OAUTH_CHAIN_START_STEP;
|
||
const lastStepId = typeof getLastStepIdForState === 'function'
|
||
? getLastStepIdForState(latestState)
|
||
: (typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10);
|
||
const currentStepKey = resolveStepKey(normalizedStep, latestState);
|
||
const confirmOauthStep = findStepIdByKeyForState('confirm-oauth', latestState);
|
||
const shouldRetryFromConfirmStep = currentStepKey === 'platform-verify'
|
||
&& Number.isFinite(confirmOauthStep)
|
||
&& confirmOauthStep > 0
|
||
&& confirmOauthStep < normalizedStep
|
||
&& isPlatformVerifyTransientRetryError(errorMessage);
|
||
const restartAnchorStep = shouldRetryFromConfirmStep ? confirmOauthStep : authChainStartStep;
|
||
if (!Number.isFinite(normalizedStep) || normalizedStep < authChainStartStep || normalizedStep > lastStepId) {
|
||
return {
|
||
shouldRestart: false,
|
||
blockedByAddPhone: false,
|
||
forcedByPhoneVerificationTimeout: false,
|
||
restartStep: authChainStartStep,
|
||
errorMessage,
|
||
authState: null,
|
||
};
|
||
}
|
||
|
||
if (isPhoneVerificationLocalFailure(errorMessage)) {
|
||
return {
|
||
shouldRestart: false,
|
||
blockedByAddPhone: true,
|
||
forcedByPhoneVerificationTimeout: false,
|
||
restartStep: authChainStartStep,
|
||
errorMessage,
|
||
authState: null,
|
||
};
|
||
}
|
||
|
||
if (shouldForceRestartFromStep7) {
|
||
return {
|
||
shouldRestart: true,
|
||
blockedByAddPhone: false,
|
||
forcedByPhoneVerificationTimeout: true,
|
||
restartStep: authChainStartStep,
|
||
errorMessage,
|
||
authState: null,
|
||
};
|
||
}
|
||
|
||
if (isAddPhoneAuthFailure(error) || isAddPhoneAuthUrl(errorMessage)) {
|
||
return {
|
||
shouldRestart: false,
|
||
blockedByAddPhone: true,
|
||
forcedByPhoneVerificationTimeout: false,
|
||
restartStep: authChainStartStep,
|
||
errorMessage,
|
||
authState: null,
|
||
};
|
||
}
|
||
|
||
let authState = null;
|
||
try {
|
||
authState = await getLoginAuthStateFromContent({
|
||
logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 ${restartAnchorStep} 重开...`,
|
||
});
|
||
} catch (inspectError) {
|
||
console.warn(LOG_PREFIX, '[AutoRun] failed to inspect login auth state after post-step6 error', {
|
||
step: normalizedStep,
|
||
sourceError: errorMessage,
|
||
inspectError: inspectError?.message || inspectError,
|
||
});
|
||
}
|
||
|
||
if (isAddPhoneAuthState(authState)) {
|
||
return {
|
||
shouldRestart: false,
|
||
blockedByAddPhone: true,
|
||
forcedByPhoneVerificationTimeout: false,
|
||
restartStep: authChainStartStep,
|
||
errorMessage,
|
||
authState,
|
||
};
|
||
}
|
||
|
||
return {
|
||
shouldRestart: true,
|
||
blockedByAddPhone: false,
|
||
forcedByPhoneVerificationTimeout: false,
|
||
restartStep: restartAnchorStep,
|
||
errorMessage,
|
||
authState,
|
||
};
|
||
}
|
||
|
||
async function getLoginAuthStateFromContent(options = {}) {
|
||
const { logMessage = '步骤 8:认证页正在切换,等待页面重新就绪后继续确认验证码页状态...' } = options;
|
||
const result = await sendToContentScriptResilient(
|
||
'signup-page',
|
||
{
|
||
type: 'GET_LOGIN_AUTH_STATE',
|
||
source: 'background',
|
||
payload: {},
|
||
},
|
||
{
|
||
timeoutMs: options.timeoutMs ?? 15000,
|
||
retryDelayMs: options.retryDelayMs ?? 600,
|
||
responseTimeoutMs: options.responseTimeoutMs ?? (options.timeoutMs ?? 15000),
|
||
logMessage,
|
||
}
|
||
);
|
||
|
||
if (result?.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
|
||
return result || {};
|
||
}
|
||
|
||
async function ensureStep8VerificationPageReady(options = {}) {
|
||
const visibleStep = Number(options.visibleStep) || 8;
|
||
const authLoginStep = Number(options.authLoginStep) || (visibleStep >= 11 ? 10 : 7);
|
||
const inspectState = async (overrides = {}) => getLoginAuthStateFromContent({
|
||
...options,
|
||
...overrides,
|
||
});
|
||
let pageState = await inspectState();
|
||
if (pageState.state === 'verification_page' || pageState.state === 'oauth_consent_page') {
|
||
return pageState;
|
||
}
|
||
|
||
if (pageState.maxCheckAttemptsBlocked) {
|
||
throw new Error(`${CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX}${CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE}`);
|
||
}
|
||
|
||
if (pageState.state === 'login_timeout_error_page') {
|
||
let recovered = false;
|
||
try {
|
||
const recoverPayload = {
|
||
flow: 'login',
|
||
logLabel: `步骤 ${visibleStep}:检测到登录超时报错,正在点击“重试”恢复当前页面`,
|
||
step: visibleStep,
|
||
timeoutMs: 12000,
|
||
};
|
||
const recoverMessage = {
|
||
type: 'RECOVER_AUTH_RETRY_PAGE',
|
||
source: 'background',
|
||
payload: recoverPayload,
|
||
};
|
||
let recoverResult = null;
|
||
const recoverTimeoutMs = 15000;
|
||
if (typeof sendToContentScriptResilient === 'function') {
|
||
recoverResult = await sendToContentScriptResilient(
|
||
'signup-page',
|
||
recoverMessage,
|
||
{
|
||
timeoutMs: recoverTimeoutMs,
|
||
responseTimeoutMs: recoverTimeoutMs,
|
||
retryDelayMs: 700,
|
||
logMessage: `步骤 ${visibleStep}:认证页进入重试/超时报错状态,正在尝试点击“重试”恢复...`,
|
||
}
|
||
);
|
||
} else if (typeof sendToContentScript === 'function') {
|
||
recoverResult = await sendToContentScript('signup-page', recoverMessage, {
|
||
responseTimeoutMs: recoverTimeoutMs,
|
||
});
|
||
}
|
||
|
||
if (recoverResult?.error) {
|
||
throw new Error(recoverResult.error);
|
||
}
|
||
recovered = Boolean(recoverResult?.recovered || Number(recoverResult?.clickCount) > 0);
|
||
if (recovered && typeof addLog === 'function') {
|
||
await addLog(`步骤 ${visibleStep}:认证页已点击“重试”,正在重新确认验证码页状态...`, 'warn');
|
||
}
|
||
} catch (recoverError) {
|
||
const recoverMessage = getErrorMessage(recoverError);
|
||
if (/^CF_SECURITY_BLOCKED::/i.test(recoverMessage)) {
|
||
throw recoverError;
|
||
}
|
||
if (typeof addLog === 'function') {
|
||
await addLog(`步骤 ${visibleStep}:认证页“重试”恢复失败:${recoverMessage}`, 'warn');
|
||
}
|
||
}
|
||
|
||
if (recovered) {
|
||
pageState = await inspectState({
|
||
timeoutMs: 10000,
|
||
responseTimeoutMs: 10000,
|
||
retryDelayMs: 500,
|
||
logMessage: `步骤 ${visibleStep}:认证页恢复后,正在确认验证码页是否可继续...`,
|
||
});
|
||
if (pageState.state === 'verification_page' || pageState.state === 'oauth_consent_page') {
|
||
return pageState;
|
||
}
|
||
if (pageState.maxCheckAttemptsBlocked) {
|
||
throw new Error(`${CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX}${CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE}`);
|
||
}
|
||
if (pageState.state === 'add_phone_page' || pageState.state === 'phone_verification_page') {
|
||
const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
|
||
throw new Error(`步骤 ${visibleStep}:当前认证页进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim());
|
||
}
|
||
}
|
||
|
||
const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
|
||
throw new Error(`STEP8_RESTART_STEP7::步骤 ${visibleStep}:当前认证页进入登录超时报错页,请回到步骤 ${authLoginStep} 重新开始。${urlPart}`.trim());
|
||
}
|
||
|
||
if (pageState.state === 'add_phone_page' || pageState.state === 'phone_verification_page') {
|
||
const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
|
||
throw new Error(`步骤 ${visibleStep}:当前认证页进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim());
|
||
}
|
||
|
||
const stateLabel = getLoginAuthStateLabel(pageState.state);
|
||
const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
|
||
throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 ${authLoginStep}。当前状态:${stateLabel}.${urlPart}`.trim());
|
||
}
|
||
|
||
async function rerunStep7ForStep8Recovery(options = {}) {
|
||
const {
|
||
logMessage = '步骤 8:正在回到步骤 7,重新发起登录验证码流程...',
|
||
postStepDelayMs = 3000,
|
||
} = options;
|
||
|
||
throwIfStopped();
|
||
const initialState = await getState();
|
||
const authLoginStep = typeof getAuthChainStartStepId === 'function'
|
||
? getAuthChainStartStepId(initialState)
|
||
: FINAL_OAUTH_CHAIN_START_STEP;
|
||
await addLog(logMessage, 'warn');
|
||
await setStepStatus(authLoginStep, 'running');
|
||
await addLog(`步骤 ${authLoginStep} 开始执行`);
|
||
|
||
try {
|
||
await step7Executor.executeStep7({
|
||
...initialState,
|
||
visibleStep: authLoginStep,
|
||
});
|
||
} catch (err) {
|
||
const latestState = await getState();
|
||
if (isStopError(err)) {
|
||
await setStepStatus(authLoginStep, 'stopped');
|
||
await addLog(`步骤 ${authLoginStep} 已被用户停止`, 'warn');
|
||
await appendManualAccountRunRecordIfNeeded(`step${authLoginStep}_stopped`, latestState, getErrorMessage(err));
|
||
throw err;
|
||
}
|
||
if (isTerminalSecurityBlockedError(err)) {
|
||
await handleCloudflareSecurityBlocked(err);
|
||
throw new Error(STOP_ERROR_MESSAGE);
|
||
}
|
||
await setStepStatus(authLoginStep, 'failed');
|
||
await addLog(`步骤 ${authLoginStep} 失败:${getErrorMessage(err)}`, 'error');
|
||
await appendManualAccountRunRecordIfNeeded(`step${authLoginStep}_failed`, latestState, getErrorMessage(err));
|
||
throw err;
|
||
}
|
||
|
||
if (postStepDelayMs > 0) {
|
||
await sleepWithStop(postStepDelayMs);
|
||
}
|
||
}
|
||
|
||
async function executeStep6() {
|
||
return step6Executor.executeStep6();
|
||
}
|
||
|
||
// ============================================================
|
||
// Step 7: Refresh OAuth and log in
|
||
// ============================================================
|
||
|
||
async function executeStep7(state) {
|
||
return step7Executor.executeStep7(state);
|
||
}
|
||
|
||
// ============================================================
|
||
// Step 8: Poll login verification mail and submit the login code
|
||
// ============================================================
|
||
|
||
async function executeStep8(state) {
|
||
return step8Executor.executeStep8(state);
|
||
}
|
||
|
||
// ============================================================
|
||
// Step 9: 完成 OAuth(自动点击 + localhost 回调监听)
|
||
// ============================================================
|
||
|
||
let webNavListener = null;
|
||
let webNavCommittedListener = null;
|
||
let step8TabUpdatedListener = null;
|
||
let step8PendingReject = null;
|
||
const STEP8_CLICK_EFFECT_TIMEOUT_MS = 15000;
|
||
const STEP8_CLICK_RETRY_DELAY_MS = 500;
|
||
const STEP8_READY_WAIT_TIMEOUT_MS = 180000;
|
||
const STEP8_MAX_ROUNDS = 5;
|
||
const STEP8_STRATEGIES = [
|
||
{ mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
|
||
{ mode: 'debugger', label: 'debugger click' },
|
||
{ mode: 'content', strategy: 'nativeClick', label: 'element.click' },
|
||
{ mode: 'content', strategy: 'dispatchClick', label: 'dispatch click' },
|
||
{ mode: 'debugger', label: 'debugger click retry' },
|
||
];
|
||
|
||
function setWebNavListener(listener) {
|
||
webNavListener = listener;
|
||
}
|
||
|
||
function getWebNavListener() {
|
||
return webNavListener;
|
||
}
|
||
|
||
function setWebNavCommittedListener(listener) {
|
||
webNavCommittedListener = listener;
|
||
}
|
||
|
||
function getWebNavCommittedListener() {
|
||
return webNavCommittedListener;
|
||
}
|
||
|
||
function setStep8TabUpdatedListener(listener) {
|
||
step8TabUpdatedListener = listener;
|
||
}
|
||
|
||
function getStep8TabUpdatedListener() {
|
||
return step8TabUpdatedListener;
|
||
}
|
||
|
||
function setStep8PendingReject(handler) {
|
||
step8PendingReject = handler;
|
||
}
|
||
|
||
function cleanupStep8NavigationListeners() {
|
||
if (webNavListener) {
|
||
chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
|
||
webNavListener = null;
|
||
}
|
||
if (webNavCommittedListener) {
|
||
chrome.webNavigation.onCommitted.removeListener(webNavCommittedListener);
|
||
webNavCommittedListener = null;
|
||
}
|
||
if (step8TabUpdatedListener) {
|
||
chrome.tabs.onUpdated.removeListener(step8TabUpdatedListener);
|
||
step8TabUpdatedListener = null;
|
||
}
|
||
}
|
||
|
||
function rejectPendingStep8(error) {
|
||
if (!step8PendingReject) return;
|
||
const reject = step8PendingReject;
|
||
step8PendingReject = null;
|
||
reject(error);
|
||
}
|
||
|
||
function throwIfStep8SettledOrStopped(isSettled = false) {
|
||
if (isSettled || stopRequested) {
|
||
throw new Error(STOP_ERROR_MESSAGE);
|
||
}
|
||
}
|
||
|
||
async function ensureStep8SignupPageReady(tabId, options = {}) {
|
||
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
||
inject: SIGNUP_PAGE_INJECT_FILES,
|
||
injectSource: 'signup-page',
|
||
timeoutMs: options.timeoutMs ?? 15000,
|
||
retryDelayMs: options.retryDelayMs ?? 600,
|
||
logMessage: options.logMessage || '',
|
||
});
|
||
}
|
||
|
||
async function getStep8PageState(tabId, responseTimeoutMs = 1500) {
|
||
try {
|
||
const result = await sendTabMessageWithTimeout(tabId, 'signup-page', {
|
||
type: 'STEP8_GET_STATE',
|
||
source: 'background',
|
||
payload: {},
|
||
}, responseTimeoutMs);
|
||
if (result?.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
return result;
|
||
} catch (err) {
|
||
if (isRetryableContentScriptTransportError(err)) {
|
||
return null;
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) {
|
||
const start = Date.now();
|
||
let recovered = false;
|
||
|
||
while (Date.now() - start < timeoutMs) {
|
||
throwIfStopped();
|
||
const pageState = await getStep8PageState(tabId);
|
||
if (pageState?.maxCheckAttemptsBlocked) {
|
||
throw new Error(`${CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX}${CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE}`);
|
||
}
|
||
if (pageState?.addPhonePage || pageState?.phoneVerificationPage) {
|
||
const latestState = await getState();
|
||
if (!Boolean(latestState?.phoneVerificationEnabled)) {
|
||
const urlPart = pageState?.url ? ` URL: ${pageState.url}` : '';
|
||
throw new Error(
|
||
pageState?.phoneVerificationPage
|
||
? `步骤 9:当前认证页进入手机验证码页,但未开启接码功能,无法继续自动授权。${urlPart}`.trim()
|
||
: `步骤 9:当前认证页进入手机号页面,但未开启接码功能,无法继续自动授权。${urlPart}`.trim()
|
||
);
|
||
}
|
||
await phoneVerificationHelpers.completePhoneVerificationFlow(tabId, pageState);
|
||
recovered = false;
|
||
await sleepWithStop(250);
|
||
continue;
|
||
}
|
||
if (pageState?.addPhonePage) {
|
||
throw new Error('步骤 9:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。');
|
||
}
|
||
if (pageState?.retryPage) {
|
||
const retryUrl = String(pageState?.url || '').trim();
|
||
const consentLikeRetry = Boolean(
|
||
pageState?.consentReady
|
||
|| pageState?.consentPage
|
||
|| /\/sign-in-with-chatgpt\/[^/?#]+\/consent(?:[/?#]|$)/i.test(retryUrl)
|
||
);
|
||
if (!consentLikeRetry) {
|
||
throw new Error(`步骤 9:当前认证页已进入重试页,当前流程将直接报错。URL: ${pageState.url || 'unknown'}`);
|
||
}
|
||
}
|
||
if (pageState?.consentReady) {
|
||
return pageState;
|
||
}
|
||
if (pageState === null && !recovered) {
|
||
recovered = true;
|
||
await ensureStep8SignupPageReady(tabId, {
|
||
timeoutMs: Math.min(10000, timeoutMs),
|
||
logMessage: '步骤 9:认证页内容脚本已失联,正在等待页面重新就绪...',
|
||
});
|
||
continue;
|
||
}
|
||
recovered = false;
|
||
await sleepWithStop(250);
|
||
}
|
||
|
||
throw new Error('步骤 9:长时间未进入 OAuth 同意页,无法定位“继续”按钮。');
|
||
}
|
||
|
||
async function prepareStep8DebuggerClick(tabId, options = {}) {
|
||
const timeoutMs = options.timeoutMs ?? 15000;
|
||
const responseTimeoutMs = options.responseTimeoutMs ?? timeoutMs;
|
||
await ensureStep8SignupPageReady(tabId, {
|
||
timeoutMs,
|
||
logMessage: '步骤 9:认证页内容脚本已失联,正在恢复后继续定位按钮...',
|
||
});
|
||
const result = await sendToContentScriptResilient('signup-page', {
|
||
type: 'STEP8_FIND_AND_CLICK',
|
||
source: 'background',
|
||
payload: {},
|
||
}, {
|
||
timeoutMs,
|
||
responseTimeoutMs,
|
||
retryDelayMs: 600,
|
||
logMessage: '步骤 9:认证页正在切换,等待 OAuth 同意页按钮重新就绪...',
|
||
});
|
||
|
||
if (result?.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
async function triggerStep8ContentStrategy(tabId, strategy, options = {}) {
|
||
const timeoutMs = options.timeoutMs ?? 15000;
|
||
const responseTimeoutMs = options.responseTimeoutMs ?? timeoutMs;
|
||
await ensureStep8SignupPageReady(tabId, {
|
||
timeoutMs,
|
||
logMessage: '步骤 9:认证页内容脚本已失联,正在恢复后继续点击“继续”按钮...',
|
||
});
|
||
const result = await sendToContentScriptResilient('signup-page', {
|
||
type: 'STEP8_TRIGGER_CONTINUE',
|
||
source: 'background',
|
||
payload: {
|
||
strategy,
|
||
findTimeoutMs: 4000,
|
||
enabledTimeoutMs: 3000,
|
||
},
|
||
}, {
|
||
timeoutMs,
|
||
responseTimeoutMs,
|
||
retryDelayMs: 600,
|
||
logMessage: '步骤 9:认证页正在切换,等待“继续”按钮重新就绪...',
|
||
});
|
||
|
||
if (result?.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
async function recoverAuthRetryPageOnTab(tabId, payload = {}, options = {}) {
|
||
const readyTimeoutMs = options.readyTimeoutMs ?? 15000;
|
||
const timeoutMs = options.timeoutMs ?? 15000;
|
||
const responseTimeoutMs = options.responseTimeoutMs ?? timeoutMs;
|
||
await ensureStep8SignupPageReady(tabId, {
|
||
timeoutMs: readyTimeoutMs,
|
||
retryDelayMs: options.retryDelayMs ?? 600,
|
||
logMessage: options.readyLogMessage || '步骤 9:认证页内容脚本已失联,正在恢复后继续处理重试页...',
|
||
});
|
||
const result = await sendToContentScriptResilient('signup-page', {
|
||
type: 'RECOVER_AUTH_RETRY_PAGE',
|
||
source: 'background',
|
||
payload,
|
||
}, {
|
||
timeoutMs,
|
||
responseTimeoutMs,
|
||
retryDelayMs: options.retryDelayMs ?? 600,
|
||
logMessage: options.logMessage || '步骤 9:认证页正在切换,等待“重试”按钮重新就绪...',
|
||
});
|
||
|
||
if (result?.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
async function reloadStep8ConsentPage(tabId, timeoutMs = 30000) {
|
||
if (!Number.isInteger(tabId)) {
|
||
throw new Error('步骤 9:缺少有效的认证页标签页,无法刷新后重试。');
|
||
}
|
||
|
||
await chrome.tabs.update(tabId, { active: true }).catch(() => { });
|
||
|
||
await new Promise((resolve, reject) => {
|
||
let settled = false;
|
||
const timer = setTimeout(() => {
|
||
if (settled) return;
|
||
settled = true;
|
||
chrome.tabs.onUpdated.removeListener(listener);
|
||
reject(new Error('步骤 9:刷新认证页后等待页面完成加载超时。'));
|
||
}, timeoutMs);
|
||
|
||
const listener = (updatedTabId, changeInfo) => {
|
||
if (updatedTabId !== tabId) return;
|
||
if (changeInfo.status !== 'complete') return;
|
||
if (settled) return;
|
||
settled = true;
|
||
clearTimeout(timer);
|
||
chrome.tabs.onUpdated.removeListener(listener);
|
||
resolve();
|
||
};
|
||
|
||
chrome.tabs.onUpdated.addListener(listener);
|
||
chrome.tabs.reload(tabId, { bypassCache: false }).catch((err) => {
|
||
if (settled) return;
|
||
settled = true;
|
||
clearTimeout(timer);
|
||
chrome.tabs.onUpdated.removeListener(listener);
|
||
reject(err);
|
||
});
|
||
});
|
||
|
||
await ensureStep8SignupPageReady(tabId, {
|
||
timeoutMs: Math.min(15000, timeoutMs),
|
||
logMessage: '步骤 9:认证页刷新后内容脚本尚未就绪,正在等待页面恢复...',
|
||
});
|
||
}
|
||
|
||
async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLICK_EFFECT_TIMEOUT_MS) {
|
||
const start = Date.now();
|
||
let recovered = false;
|
||
|
||
while (Date.now() - start < timeoutMs) {
|
||
throwIfStopped();
|
||
|
||
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||
if (!tab) {
|
||
throw new Error('步骤 9:认证页面标签页已关闭,无法继续自动授权。');
|
||
}
|
||
|
||
if (baselineUrl && typeof tab.url === 'string' && tab.url !== baselineUrl) {
|
||
return { progressed: true, reason: 'url_changed', url: tab.url };
|
||
}
|
||
|
||
const pageState = await getStep8PageState(tabId);
|
||
if (pageState?.maxCheckAttemptsBlocked) {
|
||
throw new Error(`${CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX}${CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE}`);
|
||
}
|
||
if (pageState?.addPhonePage) {
|
||
throw new Error('步骤 9:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。');
|
||
}
|
||
if (pageState?.retryPage) {
|
||
const retryUrl = String(pageState?.url || baselineUrl || '').trim();
|
||
const consentLikeRetry = Boolean(
|
||
pageState?.consentReady
|
||
|| pageState?.consentPage
|
||
|| /\/sign-in-with-chatgpt\/[^/?#]+\/consent(?:[/?#]|$)/i.test(retryUrl)
|
||
);
|
||
if (!consentLikeRetry) {
|
||
throw new Error(`步骤 9:点击“继续”后页面进入认证页重试页,当前流程将直接报错。URL: ${pageState.url || baselineUrl || 'unknown'}`);
|
||
}
|
||
}
|
||
if (pageState === null) {
|
||
if (!recovered) {
|
||
recovered = true;
|
||
await ensureStep8SignupPageReady(tabId, {
|
||
timeoutMs: Math.max(1000, Math.min(8000, timeoutMs)),
|
||
logMessage: '步骤 9:点击后认证页正在重载,正在等待内容脚本重新就绪...',
|
||
}).catch(() => null);
|
||
continue;
|
||
}
|
||
await sleepWithStop(200);
|
||
continue;
|
||
}
|
||
recovered = false;
|
||
|
||
if (pageState?.consentPage === false && !pageState?.verificationPage) {
|
||
return {
|
||
progressed: true,
|
||
reason: 'left_consent_page',
|
||
url: pageState.url || baselineUrl || '',
|
||
};
|
||
}
|
||
|
||
await sleepWithStop(200);
|
||
}
|
||
|
||
return { progressed: false, reason: 'no_effect' };
|
||
}
|
||
|
||
function getStep8EffectLabel(effect) {
|
||
switch (effect?.reason) {
|
||
case 'url_changed':
|
||
return `URL 已变化:${effect.url}`;
|
||
case 'page_reloading':
|
||
return '页面正在跳转或重载';
|
||
case 'left_consent_page':
|
||
return `页面已离开 OAuth 同意页:${effect.url || 'unknown'}`;
|
||
default:
|
||
return '页面仍停留在 OAuth 同意页';
|
||
}
|
||
}
|
||
|
||
function isStep9OAuthLocalhostTimeoutError(error, visibleStep = 9) {
|
||
const message = getErrorMessage(error);
|
||
if (!message) {
|
||
return false;
|
||
}
|
||
if (!/从拿到 OAuth 登录地址开始/.test(message)) {
|
||
return false;
|
||
}
|
||
if (!/localhost 回调|OAuth localhost 回调/i.test(message)) {
|
||
return false;
|
||
}
|
||
const normalizedStep = Number(visibleStep);
|
||
if (Number.isFinite(normalizedStep) && normalizedStep > 0) {
|
||
const stepPrefix = new RegExp(`步骤\\s*${normalizedStep}\\s*:`);
|
||
if (!stepPrefix.test(message)) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
async function recoverOAuthLocalhostTimeout(details = {}) {
|
||
const {
|
||
error,
|
||
state,
|
||
visibleStep = 9,
|
||
} = details;
|
||
|
||
if (!isStep9OAuthLocalhostTimeoutError(error, visibleStep)) {
|
||
return null;
|
||
}
|
||
|
||
const authLoginStep = typeof getAuthChainStartStepId === 'function'
|
||
? getAuthChainStartStepId(state || {})
|
||
: FINAL_OAUTH_CHAIN_START_STEP;
|
||
const loginCodeStep = Number(visibleStep) >= 12 ? 11 : 8;
|
||
|
||
await addLog(
|
||
`步骤 ${visibleStep}:检测到 OAuth localhost 回调等待窗口已过期,正在复核认证页并回到步骤 ${authLoginStep} 重拉授权链路。`,
|
||
'warn'
|
||
);
|
||
|
||
let authState = null;
|
||
try {
|
||
authState = await getLoginAuthStateFromContent({
|
||
timeoutMs: 10000,
|
||
responseTimeoutMs: 10000,
|
||
logMessage: `步骤 ${visibleStep}:正在复核认证页状态,确认是否可自动恢复 localhost 回调链路...`,
|
||
});
|
||
} catch (inspectError) {
|
||
await addLog(
|
||
`步骤 ${visibleStep}:复核认证页状态失败(${getErrorMessage(inspectError)}),将先尝试按步骤 ${loginCodeStep} 收尾恢复。`,
|
||
'warn'
|
||
);
|
||
}
|
||
|
||
if (isAddPhoneAuthState(authState)) {
|
||
const stateLabel = getLoginAuthStateLabel(authState.state);
|
||
await addLog(
|
||
`步骤 ${visibleStep}:当前认证页为 ${stateLabel},将直接回到步骤 ${authLoginStep} 重新拉起授权链路,避免步骤 8/9 恢复冲突。`,
|
||
'warn'
|
||
);
|
||
} else if (authState && authState.state && !['verification_page', 'oauth_consent_page'].includes(authState.state)) {
|
||
const stateLabel = getLoginAuthStateLabel(authState.state);
|
||
await addLog(
|
||
`步骤 ${visibleStep}:当前认证页为 ${stateLabel},不满足快速恢复条件,将回到步骤 ${authLoginStep} 重开授权链路。`,
|
||
'warn'
|
||
);
|
||
}
|
||
|
||
const latestState = await getState();
|
||
if (!step7Executor?.executeStep7 || !step8Executor?.executeStep8) {
|
||
return null;
|
||
}
|
||
|
||
await addLog(
|
||
`步骤 ${visibleStep}:正在自动重开步骤 ${authLoginStep} -> ${loginCodeStep},恢复到可继续捕获 localhost 回调的状态。`,
|
||
'warn'
|
||
);
|
||
await step7Executor.executeStep7({
|
||
...latestState,
|
||
visibleStep: authLoginStep,
|
||
});
|
||
|
||
const stateAfterStep7 = await getState();
|
||
await step8Executor.executeStep8({
|
||
...stateAfterStep7,
|
||
visibleStep: loginCodeStep,
|
||
});
|
||
|
||
const recoveredState = await getState();
|
||
const oauthUrl = String(recoveredState?.oauthUrl || state?.oauthUrl || '').trim();
|
||
if (oauthUrl && typeof startOAuthFlowTimeoutWindow === 'function') {
|
||
await startOAuthFlowTimeoutWindow({
|
||
step: Number(visibleStep) || 9,
|
||
oauthUrl,
|
||
});
|
||
}
|
||
|
||
await setState({
|
||
localhostUrl: null,
|
||
});
|
||
|
||
await addLog(
|
||
`步骤 ${visibleStep}:已恢复到步骤 ${authLoginStep} -> ${loginCodeStep} 收尾状态,并刷新 OAuth localhost 回调等待窗口,准备重试当前步骤。`,
|
||
'warn'
|
||
);
|
||
return await getState();
|
||
}
|
||
|
||
const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
|
||
addLog,
|
||
chrome,
|
||
cleanupStep8NavigationListeners,
|
||
clickWithDebugger,
|
||
completeStepFromBackground,
|
||
ensureStep8SignupPageReady,
|
||
getOAuthFlowStepTimeoutMs,
|
||
getStep8CallbackUrlFromNavigation,
|
||
getStep8CallbackUrlFromTabUpdate,
|
||
getStep8EffectLabel,
|
||
getTabId,
|
||
getWebNavCommittedListener,
|
||
getWebNavListener,
|
||
getStep8TabUpdatedListener,
|
||
isTabAlive,
|
||
prepareStep8DebuggerClick,
|
||
recoverOAuthLocalhostTimeout,
|
||
reloadStep8ConsentPage,
|
||
reuseOrCreateTab,
|
||
setStep8PendingReject,
|
||
setStep8TabUpdatedListener,
|
||
setWebNavCommittedListener,
|
||
setWebNavListener,
|
||
sleepWithStop,
|
||
STEP8_CLICK_RETRY_DELAY_MS,
|
||
STEP8_MAX_ROUNDS,
|
||
STEP8_READY_WAIT_TIMEOUT_MS,
|
||
STEP8_STRATEGIES,
|
||
throwIfStep8SettledOrStopped,
|
||
triggerStep8ContentStrategy,
|
||
waitForStep8ClickEffect,
|
||
waitForStep8Ready,
|
||
});
|
||
|
||
async function executeStep9(state) {
|
||
return step9Executor.executeStep9(state);
|
||
}
|
||
|
||
// ============================================================
|
||
// Step 10: 平台回调验证
|
||
// ============================================================
|
||
|
||
async function executeContributionStep10(state) {
|
||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
||
}
|
||
if (!state.localhostUrl) {
|
||
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
|
||
}
|
||
if (!state.contributionSessionId) {
|
||
throw new Error('缺少贡献会话信息,请重新从步骤 7 开始。');
|
||
}
|
||
if (!contributionOAuthManager?.pollContributionStatus) {
|
||
throw new Error('贡献 OAuth 流程尚未接入,无法完成贡献模式的步骤 10。');
|
||
}
|
||
|
||
await addLog('步骤 10:贡献模式正在提交回调并等待最终结果...');
|
||
|
||
let latestState = await getState();
|
||
const callbackUrl = latestState.localhostUrl || state.localhostUrl;
|
||
|
||
if (!latestState.contributionCallbackUrl && contributionOAuthManager?.handleCapturedCallback) {
|
||
latestState = await contributionOAuthManager.handleCapturedCallback(callbackUrl, {
|
||
source: 'step10',
|
||
});
|
||
} else {
|
||
latestState = await contributionOAuthManager.pollContributionStatus({
|
||
reason: 'step10_initial',
|
||
stateOverride: latestState,
|
||
});
|
||
}
|
||
|
||
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||
? await getOAuthFlowStepTimeoutMs(120000, {
|
||
step: 10,
|
||
actionLabel: '贡献流程最终结果',
|
||
})
|
||
: 120000;
|
||
const startedAt = Date.now();
|
||
|
||
while (Date.now() - startedAt < timeoutMs) {
|
||
const status = String(latestState.contributionStatus || '').trim().toLowerCase();
|
||
if (contributionOAuthManager?.isContributionFinalStatus?.(status)) {
|
||
if (status === 'auto_approved') {
|
||
await addLog(`步骤 10:贡献流程已结束,最终状态:${latestState.contributionStatusMessage || status}`, 'ok');
|
||
await completeStepFromBackground(10, {
|
||
contributionStatus: status,
|
||
contributionStatusMessage: latestState.contributionStatusMessage || '',
|
||
localhostUrl: callbackUrl,
|
||
});
|
||
return;
|
||
}
|
||
throw new Error(latestState.contributionStatusMessage || '贡献流程失败。');
|
||
}
|
||
|
||
await sleepWithStop(2500);
|
||
latestState = await contributionOAuthManager.pollContributionStatus({
|
||
reason: 'step10_wait_final',
|
||
stateOverride: latestState,
|
||
});
|
||
}
|
||
|
||
throw new Error('步骤 10:等待贡献流程最终结果超时。');
|
||
}
|
||
|
||
async function executeStep10(state) {
|
||
if (state?.contributionModeExpected && !state?.contributionMode) {
|
||
throw new Error('步骤 10:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 提交。请重新进入贡献模式后再点击自动。');
|
||
}
|
||
if (state?.contributionMode) {
|
||
return executeContributionStep10(state);
|
||
}
|
||
return step10Executor.executeStep10(state);
|
||
}
|
||
|
||
// ============================================================
|
||
// Open Side Panel on extension icon click
|
||
// ============================================================
|
||
|
||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
|
||
|
||
chrome.alarms.onAlarm.addListener((alarm) => {
|
||
if (alarm.name !== AUTO_RUN_TIMER_ALARM_NAME) {
|
||
return;
|
||
}
|
||
launchAutoRunTimerPlan('alarm').catch((err) => {
|
||
console.error(LOG_PREFIX, 'Failed to resume auto run from timer alarm:', err);
|
||
});
|
||
});
|
||
|
||
chrome.runtime.onStartup.addListener(() => {
|
||
restoreAutoRunTimerIfNeeded().catch((err) => {
|
||
console.error(LOG_PREFIX, 'Failed to restore auto run timer on startup:', err);
|
||
});
|
||
if (IP_PROXY_INIT_AUTO_APPLY) {
|
||
ensureIpProxySettingsAppliedFromCurrentState({
|
||
skipExitProbe: !IP_PROXY_INIT_ENABLE_EXIT_PROBE,
|
||
suppressAuthRebind: IP_PROXY_INIT_SUPPRESS_AUTH_REBIND,
|
||
}).catch((err) => {
|
||
console.error(LOG_PREFIX, 'Failed to restore IP proxy settings on startup:', err);
|
||
});
|
||
}
|
||
});
|
||
|
||
chrome.runtime.onInstalled.addListener(() => {
|
||
restoreAutoRunTimerIfNeeded().catch((err) => {
|
||
console.error(LOG_PREFIX, 'Failed to restore auto run timer on install/update:', err);
|
||
});
|
||
if (IP_PROXY_INIT_AUTO_APPLY) {
|
||
ensureIpProxySettingsAppliedFromCurrentState({
|
||
skipExitProbe: !IP_PROXY_INIT_ENABLE_EXIT_PROBE,
|
||
suppressAuthRebind: IP_PROXY_INIT_SUPPRESS_AUTH_REBIND,
|
||
}).catch((err) => {
|
||
console.error(LOG_PREFIX, 'Failed to restore IP proxy settings on install/update:', err);
|
||
});
|
||
}
|
||
});
|
||
|
||
restoreAutoRunTimerIfNeeded().catch((err) => {
|
||
console.error(LOG_PREFIX, 'Failed to restore auto run timer:', err);
|
||
});
|
||
if (IP_PROXY_INIT_AUTO_APPLY) {
|
||
ensureIpProxySettingsAppliedFromCurrentState({
|
||
skipExitProbe: !IP_PROXY_INIT_ENABLE_EXIT_PROBE,
|
||
suppressAuthRebind: IP_PROXY_INIT_SUPPRESS_AUTH_REBIND,
|
||
}).catch((err) => {
|
||
console.error(LOG_PREFIX, 'Failed to restore IP proxy settings:', err);
|
||
});
|
||
}
|