Files
FlowPilot/background.js
T

7899 lines
271 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// background.js — Service Worker: orchestration, state, tab management, message routing
importScripts(
'managed-alias-utils.js',
'mail2925-utils.js',
'background/phone-verification-flow.js',
'background/account-run-history.js',
'background/contribution-oauth.js',
'background/mail-2925-session.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',
'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/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',
'content/activation-utils.js'
);
const SHARED_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.() || [];
const STEP_IDS = SHARED_STEP_DEFINITIONS
.map((definition) => Number(definition?.id))
.filter(Number.isFinite)
.sort((left, right) => left - right);
const LAST_STEP_ID = STEP_IDS[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,
normalizeIcloudHost,
pickReusableIcloudAlias,
toNormalizedEmailSet,
} = self.IcloudUtils;
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_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 DEFAULT_SUB2API_REDIRECT_URI = 'http://localhost:1455/auth/callback';
const AUTO_RUN_TIMER_ALARM_NAME = 'auto-run-timer';
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
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 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 DISPLAY_TIMEZONE = 'Asia/Shanghai';
const MICROSOFT_TOKEN_DNR_RULE_ID = 1001;
const PERSISTENT_ALIAS_STATE_KEYS = ['manualAliasUsage', 'preservedAliases'];
const ACCOUNT_RUN_HISTORY_STORAGE_KEY = 'accountRunHistory';
const CONTRIBUTION_RUNTIME_DEFAULTS = self.MultiPageBackgroundContributionOAuth?.RUNTIME_DEFAULTS || {
contributionMode: false,
contributionModeExpected: false,
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);
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,
codex2apiUrl: DEFAULT_CODEX2API_URL,
codex2apiAdminKey: '',
customPassword: '',
autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
mailProvider: '163',
mail2925Mode: DEFAULT_MAIL_2925_MODE,
mail2925UseAccountPool: false,
emailGenerator: 'duck',
customMailProviderPool: [],
customEmailPool: [],
autoDeleteUsedIcloudAlias: false,
icloudHostPreference: 'auto',
icloudFetchMode: 'reuse_existing',
accountRunHistoryTextEnabled: false,
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,
cloudflareDomain: '',
cloudflareDomains: [],
cloudflareTempEmailBaseUrl: '',
cloudflareTempEmailAdminAuth: '',
cloudflareTempEmailCustomAuth: '',
cloudflareTempEmailReceiveMailbox: '',
cloudflareTempEmailUseRandomSubdomain: false,
cloudflareTempEmailDomain: '',
cloudflareTempEmailDomains: [],
hotmailAccounts: [],
mail2925Accounts: [],
heroSmsApiKey: '',
heroSmsCountryId: HERO_SMS_COUNTRY_ID,
heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL,
};
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: {},
lastEmailTimestamp: null, // 最近一次获取到邮箱数据的运行时时间戳。
lastSignupCode: null, // 注册验证码,运行时由程序自动读取并写入。
lastLoginCode: null, // 登录验证码,运行时由程序自动读取并写入。
localhostUrl: null, // 运行时捕获到的 localhost 回调地址,不要手动预填。
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。
flowStartTime: null, // 当前流程开始时间。
tabRegistry: {}, // 程序维护的标签页注册表。
sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。
logs: [], // 侧边栏展示的运行日志。
...PERSISTED_SETTING_DEFAULTS, // 合并 chrome.storage.local 中持久化保存的用户配置。
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,
reusablePhoneActivation: 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,
currentHotmailAccountId: null,
currentMail2925AccountId: null,
preferredIcloudHost: '',
};
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 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 '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);
if (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 'codex2apiUrl':
return normalizeCodex2ApiUrl(value);
case 'codex2apiAdminKey':
return String(value || '').trim();
case 'customPassword':
return String(value || '');
case 'autoRunSkipFailures':
case 'autoRunDelayEnabled':
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 '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 '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 '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 'heroSmsApiKey':
return String(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;
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;
}
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);
return {
manualAliasUsage: normalizeBooleanMap(stored.manualAliasUsage),
preservedAliases: normalizeBooleanMap(stored.preservedAliases),
};
} catch (err) {
console.warn(LOG_PREFIX, 'Failed to read persisted iCloud alias state:', err?.message || err);
return {
manualAliasUsage: {},
preservedAliases: {},
};
}
}
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, accountRunHistory, ...state };
}
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.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) {
return {
...currentContributionState,
contributionMode: true,
contributionModeExpected: true,
panelMode: 'cpa',
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(),
]);
if (normalizedEnabled) {
await setPersistentSettings({ panelMode: 'cpa' });
}
const updates = buildContributionModeState(normalizedEnabled, {
...persistedSettings,
...(normalizedEnabled ? { panelMode: 'cpa' } : {}),
}, 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));
}
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',
'luckmailApiKey',
'luckmailBaseUrl',
'luckmailEmailType',
'luckmailDomain',
'luckmailUsedPurchases',
'luckmailPreserveTagId',
'luckmailPreserveTagName',
'preferredIcloudHost',
...CONTRIBUTION_RUNTIME_KEYS,
]),
getPersistedSettings(),
getPersistedAliasState(),
]);
const contributionModeState = buildContributionModeState(Boolean(prev.contributionMode), {
...persistedSettings,
...(prev.contributionMode ? { panelMode: 'cpa' } : {}),
}, prev);
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,
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 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;
}
async function ensureHotmailAccountForFlow(options = {}) {
const { allowAllocate = true, markUsed = false, preferredAccountId = null } = options;
const state = await getState();
const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
const isAccountAllocatable = (candidate) => Boolean(candidate)
&& candidate.status === 'authorized'
&& !candidate.used
&& Boolean(candidate.refreshToken);
let account = null;
if (preferredAccountId) {
account = findHotmailAccount(accounts, preferredAccountId);
}
if (!account && state.currentHotmailAccountId) {
account = findHotmailAccount(accounts, state.currentHotmailAccountId);
}
if ((!account || !isAccountAllocatable(account)) && allowAllocate) {
account = pickHotmailAccountForRun(accounts, {});
}
if (!account) {
throw new Error('没有可用的 Hotmail 账号。请先在侧边栏添加至少一个带刷新令牌(refresh token)的账号。');
}
if (!isAccountAllocatable(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 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 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) || 5);
const intervalMs = Math.max(1000, Number(pollPayload.intervalMs) || 3000);
const filters = {
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
senderFilters: pollPayload.senderFilters || [],
subjectFilters: pollPayload.subjectFilters || [],
excludeCodes: pollPayload.excludeCodes || [],
};
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 邮箱中找到新的匹配验证码。`);
}
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: [
'https://www.icloud.com/*',
'https://www.icloud.com.cn/*',
],
});
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 messageHint = getIcloudHostHintFromMessage(getErrorMessage(error));
if (messageHint) {
return getIcloudLoginUrlForHost(messageHint);
}
const savedHost = normalizeIcloudHost(currentState?.preferredIcloudHost);
if (savedHost) {
return getIcloudLoginUrlForHost(savedHost);
}
const openHost = await getOpenIcloudHostPreference();
if (openHost) {
return getIcloudLoginUrlForHost(openHost);
}
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();
return message.includes('could not validate icloud session')
|| message.includes('hide my email service was unavailable')
|| /\bstatus (401|403|409|421)\b/.test(message);
}
let lastIcloudLoginPromptAt = 0;
async function openIcloudLoginPage(preferredUrl) {
const tabs = await chrome.tabs.query({
url: [
'https://www.icloud.com/*',
'https://www.icloud.com.cn/*',
],
});
const preferredHost = new URL(preferredUrl).host;
const existing = tabs.find((tab) => {
try {
return new URL(tab.url).host === preferredHost;
} catch {
return false;
}
});
if (existing?.id) {
await chrome.tabs.update(existing.id, { active: true });
if (existing.url !== preferredUrl) {
await chrome.tabs.update(existing.id, { url: preferredUrl });
}
return existing.id;
}
const created = await chrome.tabs.create({ url: preferredUrl, active: true });
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) {
try {
return await action();
} catch (err) {
if (isIcloudLoginRequiredError(err)) {
await promptIcloudLogin(err, actionLabel);
throw new Error('请先在新打开的 iCloud 页面中完成登录,再回来点击“我已登录”。');
}
throw err;
}
}
async function icloudRequest(method, url, options = {}) {
const { data } = options;
let response;
try {
response = await fetch(url, {
method,
credentials: 'include',
headers: data !== undefined ? { 'Content-Type': 'application/json' } : undefined,
body: data !== undefined ? JSON.stringify(data) : undefined,
});
} catch (err) {
throw new Error(`iCloud 请求失败:${method} ${url}${err.message}`);
}
if (!response.ok) {
throw new Error(`iCloud 请求失败:${method} ${url}status ${response.status}`);
}
try {
return await response.json();
} catch (err) {
throw new Error(`iCloud 返回的 JSON 无法解析:${method} ${url}${err.message}`);
}
}
async function validateIcloudSession(setupUrl) {
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;
}
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: String(data.webservices.premiummailsettings.url || '').replace(/\/$/, ''),
};
} catch (err) {
errors.push(`${new URL(setupUrl).host}: ${getErrorMessage(err)}`);
}
}
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 listIcloudAliases(options = {}) {
return withIcloudLoginHelp('加载 iCloud 隐私邮箱列表', async () => {
const { serviceUrl } = await resolveIcloudPremiumMailService(options);
const response = await icloudRequest('GET', `${serviceUrl}/v2/hme/list`);
const state = await getState();
return normalizeIcloudAliasList(response, {
usedEmails: getEffectiveUsedEmails(state),
preservedEmails: getPreservedAliasMap(state),
});
});
}
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 别名列表。`);
}
const { serviceUrl } = await resolveIcloudPremiumMailService();
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');
const existingAliasesResponse = await icloudRequest('GET', `${serviceUrl}/v2/hme/list`);
const state = await getState();
const existingAliases = normalizeIcloudAliasList(existingAliasesResponse, {
usedEmails: getEffectiveUsedEmails(state),
preservedEmails: getPreservedAliasMap(state),
});
if (!generateNew) {
const reusableAlias = pickReusableIcloudAlias(existingAliases);
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');
const generated = await icloudRequest('POST', `${serviceUrl}/v1/hme/generate`);
if (!generated?.success || !generated?.result?.hme) {
throw new Error(generated?.error?.errorMessage || 'iCloud 隐私邮箱生成失败。');
}
const reserved = await icloudRequest('POST', `${serviceUrl}/v1/hme/reserve`, {
data: {
hme: generated.result.hme,
label: getIcloudAliasLabel(),
note: 'Generated through Multi-Page Automation',
},
});
if (!reserved?.success || !reserved?.result?.hme?.hme) {
throw new Error(reserved?.error?.errorMessage || 'iCloud 隐私邮箱保留失败。');
}
const alias = String(reserved.result.hme.hme || '').trim().toLowerCase();
await setEmailState(alias);
await addLog(`iCloud:已创建并保留新别名 ${alias}`, 'ok');
broadcastIcloudAliasesChanged({ reason: 'created', email: alias });
return alias;
});
}
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) {
await addLog(`iCloud:自动删除 ${email} 失败:${getErrorMessage(err)}`, 'warn');
return { handled: true, deleted: false };
}
}
// ============================================================
// 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 === '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 waitForTabComplete(tabId, options = {}) {
return tabRuntime.waitForTabComplete(tabId, options);
}
async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
return tabRuntime.ensureContentScriptReadyOnTab(source, tabId, options);
}
// ============================================================
// 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);
}
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': 'HotmailAPI对接/本地助手)',
'luckmail-api': 'LuckMailAPI 购邮)',
'cloudflare-temp-email': 'Cloudflare Temp Email',
};
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);
}
state = state === 'oauth_consent_page' ? 'unknown' : 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 = {}) {
if (typeof loggingStatus !== 'undefined' && loggingStatus?.getFirstUnfinishedStep) {
return loggingStatus.getFirstUnfinishedStep(statuses);
}
for (const step of STEP_IDS) {
if (!isStepDoneStatus(statuses[step] || 'pending')) return step;
}
return null;
}
function hasSavedProgress(statuses = {}) {
if (typeof loggingStatus !== 'undefined' && loggingStatus?.hasSavedProgress) {
return loggingStatus.hasSavedProgress(statuses);
}
return Object.values({ ...DEFAULT_STATE.stepStatuses, ...statuses }).some((status) => status !== 'pending');
}
function getDownstreamStateResets(step) {
if (step <= 1) {
return {
oauthUrl: 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,
lastSignupCode: null,
lastLoginCode: null,
localhostUrl: null,
};
}
if (step === 2) {
return {
password: null,
lastEmailTimestamp: null,
signupVerificationRequestedAt: null,
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
lastSignupCode: null,
lastLoginCode: null,
localhostUrl: null,
};
}
if (step === 3 || step === 4) {
return {
lastEmailTimestamp: null,
signupVerificationRequestedAt: null,
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
lastSignupCode: null,
lastLoginCode: null,
localhostUrl: null,
};
}
if (step === 5 || step === 6 || step === 7 || step === 8) {
return {
lastLoginCode: null,
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
localhostUrl: null,
};
}
if (step === 9) {
return {
localhostUrl: null,
};
}
return {};
}
async function invalidateDownstreamAfterStepRestart(step, options = {}) {
const { logLabel = `步骤 ${step} 重新执行` } = options;
const state = await getState();
const statuses = { ...(state.stepStatuses || {}) };
const changedSteps = [];
for (let downstream = step + 1; downstream <= LAST_STEP_ID; downstream++) {
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);
if (Object.keys(resets).length) {
await setState(resets);
broadcastDataUpdate(resets);
}
}
function clearStopRequest() {
stopRequested = false;
}
function getRunningSteps(statuses = {}) {
if (typeof loggingStatus !== 'undefined' && loggingStatus?.getRunningSteps) {
return loggingStatus.getRunningSteps(statuses);
}
return Object.entries({ ...DEFAULT_STATE.stepStatuses, ...statuses })
.filter(([, status]) => status === 'running')
.map(([step]) => Number(step))
.sort((a, b) => a - b);
}
function inferStoppedRecordStep(state = {}) {
const statuses = { ...DEFAULT_STATE.stepStatuses, ...(state?.stepStatuses || {}) };
const stepIds = Object.keys(statuses)
.map((step) => Number(step))
.filter(Number.isFinite)
.sort((left, right) => left - right);
const runningSteps = stepIds.filter((step) => statuses[step] === 'running');
if (runningSteps.length) {
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('跳过步骤');
if (!Number.isInteger(step) || !STEP_IDS.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} 已完成,无需再跳过。`);
}
if (step > 1) {
const prevStatus = statuses[step - 1];
if (!isStepDoneStatus(prevStatus)) {
throw new Error(`请先完成步骤 ${step - 1},再跳过步骤 ${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.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);
}
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]);
function waitForStepComplete(step, timeoutMs = 120000) {
return new Promise((resolve, reject) => {
throwIfStopped();
if (stepWaiters.has(step)) {
console.warn(LOG_PREFIX, `[waitForStepComplete] replacing existing waiter for step ${step}`);
}
console.log(LOG_PREFIX, `[waitForStepComplete] register step ${step}, timeout=${timeoutMs}ms`);
const timer = setTimeout(() => {
stepWaiters.delete(step);
console.warn(LOG_PREFIX, `[waitForStepComplete] timeout for step ${step} after ${timeoutMs}ms`);
reject(new Error(`步骤 ${step} 等待超时(>${timeoutMs / 1000} 秒)`));
}, timeoutMs);
stepWaiters.set(step, {
resolve: (data) => { clearTimeout(timer); stepWaiters.delete(step); resolve(data); },
reject: (err) => { clearTimeout(timer); stepWaiters.delete(step); reject(err); },
});
});
}
function doesStepUseCompletionSignal(step) {
return STEP_COMPLETION_SIGNAL_STEPS.has(step);
}
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 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 completionState = step === LAST_STEP_ID ? await getState() : null;
await setStepStatus(step, 'completed');
await addLog(`步骤 ${step} 已完成`, 'ok');
await handleStepData(step, payload);
if (step === LAST_STEP_ID) {
await appendAndBroadcastAccountRunRecord('success', completionState);
}
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 = AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS) {
const completionResultPromise = waitForStepComplete(step, timeoutMs).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);
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);
}
await addLog('自动继续:当前运行步骤已结束,准备按最新进度继续自动流程...', 'info');
return currentState;
}
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
let activeTopLevelAuthChainExecution = null;
function isAuthChainStep(step) {
return AUTH_CHAIN_STEP_IDS.has(Number(step));
}
async function acquireTopLevelAuthChainExecution(step) {
const normalizedStep = Number(step);
if (!isAuthChainStep(normalizedStep)) {
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);
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);
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();
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 (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}`);
const authChainClaim = await acquireTopLevelAuthChainExecution(step);
if (authChainClaim.joined) {
return;
}
let executionError = null;
throwIfStopped();
try {
await setStepStatus(step, 'running');
await addLog(`步骤 ${step} 开始执行`);
await humanStepDelay();
const state = await getState();
// Set flow start time on first step
if (step === 1 && !state.flowStartTime) {
await setState({ flowStartTime: Date.now() });
}
await stepRegistry.executeStep(step, state);
} catch (err) {
executionError = err;
const state = await getState();
if (isStopError(err)) {
await setStepStatus(step, 'stopped');
await addLog(`步骤 ${step} 已被用户停止`, 'warn');
await appendManualAccountRunRecordIfNeeded(`step${step}_stopped`, state, 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) && isRetryableContentScriptTransportError(err))) {
await setStepStatus(step, 'failed');
await addLog(`步骤 ${step} 失败:${err.message}`, 'error');
await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, state, 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);
}
if (AUTO_RUN_BACKGROUND_COMPLETED_STEPS.has(step)) {
await addLog(`自动运行:步骤 ${step} 由后台流程负责收尾,执行函数返回后将直接进入下一步。`, 'info');
await executeStep(step);
const latestState = await getState();
await addLog(`自动运行:步骤 ${step} 已执行返回,当前状态为 ${latestState.stepStatuses?.[step] || 'pending'},准备继续后续步骤。`, 'info');
} else if (doesStepUseCompletionSignal(step)) {
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 waitForTabComplete(signupTabId, {
timeoutMs: 15000,
retryDelayMs: 300,
});
}
}
// 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 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;
}
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(),
getAutoRunStatusPayload,
getErrorMessage,
getFirstUnfinishedStep,
getPendingAutoRunTimerPlan,
getRunningSteps,
getState,
getStopRequested: () => stopRequested,
hasSavedProgress,
isAddPhoneAuthFailure,
isRestartCurrentAttemptError,
isSignupUserAlreadyExistsFailure,
isStopError,
launchAutoRunTimerPlan,
normalizeAutoRunFallbackThreadIntervalMinutes,
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;
}
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;
for (let attempt = 1; attempt <= EMAIL_FETCH_MAX_ATTEMPTS; 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 === 'cloudflare' && /域名/.test(String(err.message || '')))
|| (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR && /(服务地址|Admin Auth|域名)/.test(String(err.message || '')))
) {
break;
}
}
}
await addLog(`${generatorLabel}自动获取已连续失败 ${EMAIL_FETCH_MAX_ATTEMPTS} 次:${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;
for (let attempt = 1; attempt <= EMAIL_FETCH_MAX_ATTEMPTS; 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 === 'cloudflare' && /域名/.test(String(err.message || '')))
|| (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR && /(服务地址|Admin Auth|域名)/.test(String(err.message || '')))
) {
break;
}
}
}
await addLog(`${generatorLabel}自动获取已连续失败 ${EMAIL_FETCH_MAX_ATTEMPTS} 次:${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 <= LAST_STEP_ID) {
const latestState = await getState();
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 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,正在回到步骤 7 重新开始授权流程(第 ${postStep7RestartCount} 次重开)。${authStateSuffix};原因:${restartDecision.errorMessage || '未知错误'}`,
'warn'
);
await invalidateDownstreamAfterStepRestart(6, {
logLabel: `步骤 ${step} 报错后准备回到步骤 7 重试(第 ${postStep7RestartCount} 次重开)`,
});
step = 7;
continue;
}
if (restartDecision.blockedByAddPhone) {
const addPhoneUrl = restartDecision.authState?.url || 'https://auth.openai.com/add-phone';
await addLog(`步骤 ${step}:检测到认证流程进入 add-phone(${addPhoneUrl}),停止自动回到步骤 7 重开。`, '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,
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,
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 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 stepDefinitions = SHARED_STEP_DEFINITIONS;
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(),
'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,
cancelScheduledAutoRun,
checkIcloudSession,
clearAccountRunHistory: (...args) => clearAndBroadcastAccountRunHistory(...args),
deleteAccountRunHistoryRecords: (...args) => deleteAndBroadcastAccountRunHistoryRecords(...args),
clearAutoRunTimerAlarm,
clearLuckmailRuntimeState,
clearStopRequest,
closeLocalhostCallbackTabs,
closeTabsByUrlPrefix,
deleteHotmailAccount,
deleteHotmailAccounts,
deleteIcloudAlias,
deleteUsedIcloudAliases,
disableUsedLuckmailPurchases,
doesStepUseCompletionSignal,
ensureMail2925MailboxSession,
ensureManualInteractionAllowed,
executeStep,
executeStepViaCompletionSignal,
exportSettingsBundle,
fetchGeneratedEmail,
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,
getTabId,
getStopRequested: () => stopRequested,
handleCloudflareSecurityBlocked,
handleAutoRunLoopUnhandledError,
importSettingsBundle,
invalidateDownstreamAfterStepRestart,
isCloudflareSecurityBlockedError: isTerminalSecurityBlockedError,
isAutoRunLockedState,
isHotmailProvider,
isLocalhostOAuthCallbackUrl,
isLuckmailProvider,
isStopError,
isTabAlive,
launchAutoRunTimerPlan,
listIcloudAliases,
listLuckmailPurchasesForManagement,
getCurrentMail2925Account,
normalizeHotmailAccounts,
normalizeMail2925Accounts,
normalizeRunCount,
AUTO_RUN_TIMER_KIND_SCHEDULED_START,
notifyStepComplete,
notifyStepError,
patchHotmailAccount,
patchMail2925Account,
registerTab,
requestStop,
resetState,
resumeAutoRun,
scheduleAutoRun,
selectLuckmailPurchase,
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,
deleteMail2925Account,
deleteMail2925Accounts,
testHotmailAccountMailAccess,
upsertMail2925Account,
upsertHotmailAccount,
verifyHotmailAccount,
});
const stepRegistry = self.MultiPageBackgroundStepRegistry?.createStepRegistry(
stepDefinitions.map((definition) => ({
...definition,
execute: stepExecutorsByKey[definition.key],
}))
);
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: 'HotmailAPI对接/本地助手)' };
}
if (provider === ICLOUD_PROVIDER) {
const configuredHost = getConfiguredIcloudHostPreference(state)
|| normalizeIcloudHost(state?.preferredIcloudHost)
|| 'icloud.com';
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: 'LuckMailAPI 购邮)' };
}
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 === '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(`步骤 6browsingData 补扫 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) {
if (state?.contributionModeExpected && !state?.contributionMode) {
throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入贡献模式后再点击自动。');
}
if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) {
await addLog('步骤 7contributionMode=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(`步骤 7contributionMode=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: '步骤 7' });
await handleStepData(1, refreshResult);
if (!refreshResult?.oauthUrl) {
throw new Error('刷新 OAuth 链接后仍未拿到可用链接。');
}
return refreshResult.oauthUrl;
}
function buildOAuthFlowTimeoutError(step, actionLabel = '后续授权流程') {
return new Error(
`步骤 ${step}:从拿到 OAuth 登录地址开始,${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟内未完成${actionLabel},结束当前链路,准备从步骤 7 重新开始。`
);
}
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);
}
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) {
throw buildOAuthFlowTimeoutError(
Number(options.step) || 7,
String(options.actionLabel || '后续授权流程').trim() || '后续授权流程'
);
}
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(?:[/?#]|$)/i.test(String(url || '').trim());
}
function isAddPhoneAuthState(authState = {}) {
return authState?.state === 'add_phone_page'
|| Boolean(authState?.addPhonePage)
|| isAddPhoneAuthUrl(authState?.url);
}
async function getPostStep6AutoRestartDecision(step, error) {
const normalizedStep = Number(step);
const errorMessage = getErrorMessage(error);
const shouldForceRestartFromStep7 = /restart step 7 with a new number/i.test(errorMessage);
if (!Number.isFinite(normalizedStep) || normalizedStep < 7 || normalizedStep > LAST_STEP_ID) {
return {
shouldRestart: false,
blockedByAddPhone: false,
forcedByPhoneVerificationTimeout: false,
errorMessage,
authState: null,
};
}
if (shouldForceRestartFromStep7) {
return {
shouldRestart: true,
blockedByAddPhone: false,
forcedByPhoneVerificationTimeout: true,
errorMessage,
authState: null,
};
}
if (isAddPhoneAuthFailure(error) || isAddPhoneAuthUrl(errorMessage)) {
return {
shouldRestart: false,
blockedByAddPhone: true,
forcedByPhoneVerificationTimeout: false,
errorMessage,
authState: null,
};
}
let authState = null;
try {
authState = await getLoginAuthStateFromContent({
logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 7 重开...`,
});
} 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,
errorMessage,
authState,
};
}
return {
shouldRestart: true,
blockedByAddPhone: false,
forcedByPhoneVerificationTimeout: false,
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 pageState = await getLoginAuthStateFromContent(options);
if (pageState.state === 'verification_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') {
const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
throw new Error(`STEP8_RESTART_STEP7::步骤 8:当前认证页进入登录超时报错页,请回到步骤 7 重新开始。${urlPart}`.trim());
}
if (pageState.state === 'add_phone_page') {
const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
throw new Error(`步骤 8:当前认证页进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim());
}
const stateLabel = getLoginAuthStateLabel(pageState.state);
const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 7。当前状态:${stateLabel}.${urlPart}`.trim());
}
async function rerunStep7ForStep8Recovery(options = {}) {
const {
logMessage = '步骤 8:正在回到步骤 7,重新发起登录验证码流程...',
postStepDelayMs = 3000,
} = options;
throwIfStopped();
const initialState = await getState();
await addLog(logMessage, 'warn');
await setStepStatus(7, 'running');
await addLog('步骤 7 开始执行');
try {
await step7Executor.executeStep7(initialState);
} catch (err) {
const latestState = await getState();
if (isStopError(err)) {
await setStepStatus(7, 'stopped');
await addLog('步骤 7 已被用户停止', 'warn');
await appendManualAccountRunRecordIfNeeded('step7_stopped', latestState, getErrorMessage(err));
throw err;
}
if (isTerminalSecurityBlockedError(err)) {
await handleCloudflareSecurityBlocked(err);
throw new Error(STOP_ERROR_MESSAGE);
}
await setStepStatus(7, 'failed');
await addLog(`步骤 7 失败:${getErrorMessage(err)}`, 'error');
await appendManualAccountRunRecordIfNeeded('step7_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) {
await phoneVerificationHelpers.completePhoneVerificationFlow(tabId, pageState);
recovered = false;
await sleepWithStop(250);
continue;
}
if (pageState?.addPhonePage) {
throw new Error('步骤 9:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。');
}
if (pageState?.retryPage) {
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) {
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 同意页';
}
}
const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
addLog,
chrome,
cleanupStep8NavigationListeners,
clickWithDebugger,
completeStepFromBackground,
ensureStep8SignupPageReady,
getOAuthFlowStepTimeoutMs,
getStep8CallbackUrlFromNavigation,
getStep8CallbackUrlFromTabUpdate,
getStep8EffectLabel,
getTabId,
getWebNavCommittedListener,
getWebNavListener,
getStep8TabUpdatedListener,
isTabAlive,
prepareStep8DebuggerClick,
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' || status === 'manual_review_required') {
await addLog(`步骤 10:贡献流程已结束,最终状态:${latestState.contributionStatusMessage || status}`, status === 'auto_approved' ? 'ok' : 'warn');
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);
});
});
chrome.runtime.onInstalled.addListener(() => {
restoreAutoRunTimerIfNeeded().catch((err) => {
console.error(LOG_PREFIX, 'Failed to restore auto run timer on install/update:', err);
});
});
restoreAutoRunTimerIfNeeded().catch((err) => {
console.error(LOG_PREFIX, 'Failed to restore auto run timer:', err);
});