fix: handle signup phone entry mode before email submission

- 同步最新 dev 到 PR #113,并保留 dev 上更完整的 163 邮箱实现\n- 补充 Step 2 的手机号入口切邮箱逻辑与本地化邮箱输入识别\n- 避免把 Step 2 的 phone entry 提示误判成 auth add-phone 致命错误
This commit is contained in:
QLHazyCoder
2026-04-25 14:53:13 +08:00
105 changed files with 18673 additions and 1530 deletions
+14 -2
View File
@@ -96,6 +96,12 @@
.join('');
}
function shouldKeepCustomMailProviderPoolEmail(state = {}) {
return String(state?.mailProvider || '').trim().toLowerCase() === 'custom'
&& Array.isArray(state?.customMailProviderPool)
&& state.customMailProviderPool.length > 0;
}
async function logAutoRunFinalSummary(totalRuns, roundSummaries = []) {
const summaries = buildAutoRunRoundSummaries(totalRuns, roundSummaries);
const successRounds = summaries.filter((item) => item.status === 'success');
@@ -328,8 +334,10 @@
const resumingCurrentRound = continueCurrentOnFirstAttempt && targetRun === resumeCurrentRun;
let attemptRun = resumingCurrentRound ? resumeAttemptRun : 1;
let reuseExistingProgress = resumingCurrentRound;
const currentRoundState = await getState();
const keepSameEmailUntilAddPhone = autoRunSkipFailures && shouldKeepCustomMailProviderPoolEmail(currentRoundState);
const maxAttemptsForRound = autoRunSkipFailures
? AUTO_RUN_MAX_RETRIES_PER_ROUND + 1
? (keepSameEmailUntilAddPhone ? Number.MAX_SAFE_INTEGER : AUTO_RUN_MAX_RETRIES_PER_ROUND + 1)
: Math.max(1, attemptRun);
while (attemptRun <= maxAttemptsForRound) {
@@ -374,6 +382,7 @@
emailGenerator: prevState.emailGenerator,
gmailBaseEmail: prevState.gmailBaseEmail,
mail2925BaseEmail: prevState.mail2925BaseEmail,
currentMail2925AccountId: prevState.currentMail2925AccountId,
emailPrefix: prevState.emailPrefix,
inbucketHost: prevState.inbucketHost,
inbucketMailbox: prevState.inbucketMailbox,
@@ -460,6 +469,7 @@
roundSummary.failureReasons.push(reason);
const blockedByAddPhone = typeof isAddPhoneAuthFailure === 'function' && isAddPhoneAuthFailure(err);
const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function'
&& !keepSameEmailUntilAddPhone
&& isSignupUserAlreadyExistsFailure(err);
const canRetry = !blockedByAddPhone && !blockedBySignupUserAlreadyExists && autoRunSkipFailures && attemptRun < maxAttemptsForRound;
@@ -554,7 +564,9 @@
});
forceFreshTabsNextRun = true;
await addLog(
`自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试(第 ${retryIndex}/${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试)。`,
keepSameEmailUntilAddPhone
? `自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后继续使用当前邮箱,开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试。`
: `自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试(第 ${retryIndex}/${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试)。`,
'warn'
);
try {
+2 -2
View File
@@ -247,9 +247,8 @@
function buildNickname(state = {}, preferredNickname = '') {
const nickname = normalizeString(preferredNickname)
|| normalizeString(state.email)
|| normalizeString(state.contributionNickname);
return nickname || 'codex-extension-user';
return nickname || '';
}
function buildContributionQq(state = {}, preferredQq = '') {
@@ -600,6 +599,7 @@
body: {
nickname: buildNickname(currentState, options.nickname),
qq: buildContributionQq(currentState, options.qq),
email: normalizeString(currentState.email),
source: 'cpa',
channel: 'codex-extension',
},
+76 -7
View File
@@ -7,12 +7,15 @@
buildGeneratedAliasEmail,
buildCloudflareTempEmailHeaders,
CLOUDFLARE_TEMP_EMAIL_GENERATOR,
CUSTOM_EMAIL_POOL_GENERATOR,
DUCK_AUTOFILL_URL,
fetch,
fetchIcloudHideMyEmail,
getCloudflareTempEmailAddressFromResponse,
getCloudflareTempEmailConfig,
getCustomEmailPoolEmail,
getState,
ensureMail2925AccountForFlow,
joinCloudflareTempEmailUrl,
normalizeCloudflareDomain,
normalizeCloudflareTempEmailAddress,
@@ -145,6 +148,7 @@
const requestedName = String(options.localPart || options.name || '').trim().toLowerCase() || generateCloudflareAliasLocalPart();
const payload = {
enablePrefix: true,
enableRandomSubdomain: Boolean(config.useRandomSubdomain),
name: requestedName,
domain: config.domain,
};
@@ -187,19 +191,56 @@
return result.email;
}
async function fetchCustomEmailPoolEmail(state, options = {}) {
throwIfStopped();
const latestState = state || await getState();
const requestedIndex = Math.max(0, Math.floor(Number(options.poolIndex) || 0));
const email = String(getCustomEmailPoolEmail?.(latestState, requestedIndex + 1) || '').trim().toLowerCase();
if (!email) {
throw new Error(
requestedIndex > 0
? `自定义邮箱池第 ${requestedIndex + 1} 个邮箱不存在,请检查邮箱池配置。`
: '自定义邮箱池为空,请先至少填写 1 个邮箱。'
);
}
await setEmailState(email);
await addLog(`自定义邮箱池:已取用 ${email}`, 'ok');
return email;
}
async function fetchManagedAliasEmail(state, options = {}) {
throwIfStopped();
const provider = String(options.mailProvider || state?.mailProvider || '').trim().toLowerCase();
const mergedState = {
let mergedState = {
...(state || {}),
mailProvider: provider,
};
if (options.mail2925Mode !== undefined) {
mergedState.mail2925Mode = String(options.mail2925Mode || '').trim();
}
if (options.gmailBaseEmail !== undefined) {
mergedState.gmailBaseEmail = String(options.gmailBaseEmail || '').trim();
}
if (options.mail2925BaseEmail !== undefined) {
mergedState.mail2925BaseEmail = String(options.mail2925BaseEmail || '').trim();
}
if (
provider === '2925'
&& Boolean(mergedState.mail2925UseAccountPool)
&& typeof ensureMail2925AccountForFlow === 'function'
) {
const account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: mergedState.currentMail2925AccountId || null,
});
const latestState = await getState();
mergedState = {
...latestState,
...mergedState,
currentMail2925AccountId: account.id,
};
}
const email = buildGeneratedAliasEmail(mergedState);
await setEmailState(email);
@@ -210,21 +251,48 @@
async function fetchGeneratedEmail(state, options = {}) {
const currentState = state || await getState();
const provider = String(options.mailProvider || currentState.mailProvider || '').trim().toLowerCase();
if (isGeneratedAliasProvider?.(provider)) {
return fetchManagedAliasEmail(currentState, options);
}
const mail2925Mode = options.mail2925Mode !== undefined
? options.mail2925Mode
: currentState.mail2925Mode;
const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator);
const mergedState = {
...currentState,
mailProvider: provider || currentState.mailProvider,
mail2925Mode,
emailGenerator: generator,
};
if (options.gmailBaseEmail !== undefined) {
mergedState.gmailBaseEmail = String(options.gmailBaseEmail || '').trim();
}
if (options.mail2925BaseEmail !== undefined) {
mergedState.mail2925BaseEmail = String(options.mail2925BaseEmail || '').trim();
}
if (options.customEmailPool !== undefined) {
mergedState.customEmailPool = options.customEmailPool;
}
if (generator === 'custom') {
throw new Error('当前邮箱生成方式为自定义邮箱,请直接填写注册邮箱。');
}
if (generator === CUSTOM_EMAIL_POOL_GENERATOR) {
return fetchCustomEmailPoolEmail(mergedState, options);
}
const shouldUseManagedAlias = typeof isGeneratedAliasProvider === 'function'
? isGeneratedAliasProvider(mergedState, mail2925Mode)
: false;
if (shouldUseManagedAlias) {
return fetchManagedAliasEmail(mergedState, options);
}
if (generator === 'icloud') {
return fetchIcloudHideMyEmail();
const stateFetchMode = String(mergedState.icloudFetchMode || '').trim().toLowerCase();
return fetchIcloudHideMyEmail({
generateNew: Boolean(options.generateNew) || stateFetchMode === 'always_new',
});
}
if (generator === 'cloudflare') {
return fetchCloudflareEmail(currentState, options);
return fetchCloudflareEmail(mergedState, options);
}
if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) {
return fetchCloudflareTempEmailAddress(currentState, options);
return fetchCloudflareTempEmailAddress(mergedState, options);
}
return fetchDuckEmail(options);
}
@@ -232,6 +300,7 @@
return {
ensureCloudflareTempEmailConfig,
fetchCloudflareEmail,
fetchCustomEmailPoolEmail,
fetchCloudflareTempEmailAddress,
fetchDuckEmail,
fetchGeneratedEmail,
+773
View File
@@ -0,0 +1,773 @@
(function attachBackgroundMail2925Session(root, factory) {
root.MultiPageBackgroundMail2925Session = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundMail2925SessionModule() {
function createMail2925SessionManager(deps = {}) {
const {
addLog,
broadcastDataUpdate,
chrome,
ensureContentScriptReadyOnTab,
findMail2925Account,
getMail2925AccountStatus,
getState,
isAutoRunLockedState,
isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account,
normalizeMail2925Accounts,
pickMail2925AccountForRun,
requestStop,
reuseOrCreateTab,
sendToContentScriptResilient,
sendToMailContentScriptResilient,
setPersistentSettings,
setState,
sleepWithStop,
throwIfStopped,
upsertMail2925AccountInList,
waitForTabComplete,
waitForTabUrlMatch,
} = deps;
const MAIL2925_SOURCE = 'mail-2925';
const MAIL2925_URL = 'https://2925.com/#/mailList';
const MAIL2925_LOGIN_URL = 'https://2925.com/login/';
const MAIL2925_INJECT = ['content/utils.js', 'content/mail-2925.js'];
const MAIL2925_INJECT_SOURCE = 'mail-2925';
const MAIL2925_COOKIE_DOMAINS = [
'2925.com',
'www.2925.com',
'mail2.xiyouji.com',
];
const MAIL2925_COOKIE_ORIGINS = [
'https://2925.com',
'https://www.2925.com',
'https://mail2.xiyouji.com',
];
const MAIL2925_LIMIT_ERROR_PREFIX = 'MAIL2925_LIMIT_REACHED::';
const MAIL2925_THREAD_TERMINATED_ERROR_PREFIX = 'MAIL2925_THREAD_TERMINATED::';
const MAIL2925_LOGIN_MESSAGE_RETRY_WINDOW_MS = 15000;
const MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS = 120000;
const MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS = 120000;
function getMail2925MailConfig() {
return {
provider: '2925',
source: MAIL2925_SOURCE,
url: MAIL2925_URL,
label: '2925 邮箱',
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
};
}
function getErrorMessage(error) {
return String(typeof error === 'string' ? error : error?.message || '');
}
function buildMail2925ThreadTerminatedError(message) {
return new Error(`${MAIL2925_THREAD_TERMINATED_ERROR_PREFIX}${String(message || '').trim()}`);
}
async function stopAutoRunForMail2925LoginFailure(errorMessage = '') {
if (typeof requestStop !== 'function') {
return false;
}
const state = await getState();
const autoRunning = typeof isAutoRunLockedState === 'function'
? isAutoRunLockedState(state)
: Boolean(state?.autoRunning);
if (!autoRunning) {
return false;
}
await requestStop({
logMessage: errorMessage || '2925 登录失败,已按手动停止逻辑暂停自动流程。',
});
return true;
}
function isMail2925LimitReachedError(error) {
const message = getErrorMessage(error);
return message.startsWith(MAIL2925_LIMIT_ERROR_PREFIX)
|| message.includes('子邮箱已达上限')
|| message.includes('已达上限邮箱');
}
function isMail2925ThreadTerminatedError(error) {
return getErrorMessage(error).startsWith(MAIL2925_THREAD_TERMINATED_ERROR_PREFIX);
}
function isRetryableMail2925TransportError(error) {
const message = getErrorMessage(error).toLowerCase();
return message.includes('receiving end does not exist')
|| message.includes('message port closed')
|| message.includes('content script on')
|| message.includes('did not respond');
}
async function syncMail2925Accounts(accounts) {
const normalized = normalizeMail2925Accounts(accounts);
await setPersistentSettings({ mail2925Accounts: normalized });
await setState({ mail2925Accounts: normalized });
broadcastDataUpdate({ mail2925Accounts: normalized });
return normalized;
}
async function upsertMail2925Account(input = {}) {
const state = await getState();
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
const normalizedEmail = String(input?.email || '').trim().toLowerCase();
const existing = input?.id
? findMail2925Account(accounts, input.id)
: accounts.find((account) => account.email === normalizedEmail) || null;
const credentialsChanged = !existing
|| (input?.email !== undefined && normalizedEmail !== existing.email)
|| (input?.password !== undefined && String(input.password || '') !== existing.password);
const normalized = normalizeMail2925Account({
...(existing || {}),
...(credentialsChanged ? { lastError: '' } : {}),
...input,
id: input?.id || existing?.id || crypto.randomUUID(),
});
const nextAccounts = existing
? accounts.map((account) => (account.id === normalized.id ? normalized : account))
: [...accounts, normalized];
await syncMail2925Accounts(nextAccounts);
return normalized;
}
function getCurrentMail2925Account(state = {}) {
return findMail2925Account(state.mail2925Accounts, state.currentMail2925AccountId) || null;
}
async function getMail2925CurrentTabUrl() {
try {
const state = await getState();
const tabId = Number(state?.tabRegistry?.[MAIL2925_SOURCE]?.tabId || 0);
if (!Number.isInteger(tabId) || tabId <= 0 || typeof chrome.tabs?.get !== 'function') {
return '';
}
const tab = await chrome.tabs.get(tabId);
return String(tab?.url || '').trim();
} catch {
return '';
}
}
async function getMail2925TabUrlById(tabId) {
try {
if (!Number.isInteger(Number(tabId)) || Number(tabId) <= 0 || typeof chrome.tabs?.get !== 'function') {
return '';
}
const tab = await chrome.tabs.get(Number(tabId));
return String(tab?.url || '').trim();
} catch {
return '';
}
}
function isMail2925LoginUrl(rawUrl = '') {
try {
const parsed = new URL(String(rawUrl || ''));
return (parsed.hostname === '2925.com' || parsed.hostname === 'www.2925.com')
&& /^\/login\/?$/.test(parsed.pathname);
} catch {
return false;
}
}
function normalizeMailboxEmail(value = '') {
return String(value || '').trim().toLowerCase();
}
async function setCurrentMail2925Account(accountId, options = {}) {
const { logMessage = '', updateLastUsedAt = false } = options;
const state = await getState();
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
const account = findMail2925Account(accounts, accountId);
if (!account) {
throw new Error('未找到对应的 2925 账号。');
}
let nextAccount = account;
if (updateLastUsedAt) {
nextAccount = normalizeMail2925Account({
...account,
lastUsedAt: Date.now(),
});
await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item)));
}
await setPersistentSettings({ currentMail2925AccountId: nextAccount.id });
await setState({ currentMail2925AccountId: nextAccount.id });
broadcastDataUpdate({ currentMail2925AccountId: nextAccount.id });
if (logMessage) {
await addLog(logMessage, 'ok');
}
return nextAccount;
}
async function patchMail2925Account(accountId, updates = {}) {
const state = await getState();
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
const account = findMail2925Account(accounts, accountId);
if (!account) {
throw new Error('未找到对应的 2925 账号。');
}
const nextAccount = normalizeMail2925Account({
...account,
...updates,
id: account.id,
});
await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item)));
if (state.currentMail2925AccountId === account.id && nextAccount.enabled === false) {
await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
}
return nextAccount;
}
async function deleteMail2925Account(accountId) {
const state = await getState();
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
const nextAccounts = accounts.filter((account) => account.id !== accountId);
await syncMail2925Accounts(nextAccounts);
if (state.currentMail2925AccountId === accountId) {
await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
}
}
async function deleteMail2925Accounts(mode = 'all') {
const state = await getState();
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
const nextAccounts = mode === 'all'
? []
: accounts.filter((account) => getMail2925AccountStatus(account) !== String(mode || '').trim());
const deletedCount = Math.max(0, accounts.length - nextAccounts.length);
await syncMail2925Accounts(nextAccounts);
if (state.currentMail2925AccountId && !findMail2925Account(nextAccounts, state.currentMail2925AccountId)) {
await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
}
return {
deletedCount,
remainingCount: nextAccounts.length,
};
}
async function ensureMail2925AccountForFlow(options = {}) {
const {
allowAllocate = true,
preferredAccountId = null,
excludeIds = [],
markUsed = false,
} = options;
const state = await getState();
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
const now = Date.now();
let account = null;
if (preferredAccountId) {
account = findMail2925Account(accounts, preferredAccountId);
}
if (!account && state.currentMail2925AccountId) {
account = findMail2925Account(accounts, state.currentMail2925AccountId);
}
if ((!account || !isMail2925AccountAvailable(account, now)) && allowAllocate) {
account = pickMail2925AccountForRun(accounts, {
excludeIds,
now,
});
}
if (!account) {
throw new Error('没有可用的 2925 账号。请先在侧边栏添加至少一个带密码的 2925 账号。');
}
if (!account.password) {
throw new Error(`2925 账号 ${account.email || account.id} 缺少密码,无法自动登录。`);
}
if (!isMail2925AccountAvailable(account, now)) {
const disabledUntil = Number(account.disabledUntil || 0);
if (disabledUntil > now) {
throw new Error(`2925 账号 ${account.email || account.id} 当前处于冷却期,将在 ${new Date(disabledUntil).toLocaleString('zh-CN', { hour12: false })} 后恢复。`);
}
throw new Error(`2925 账号 ${account.email || account.id} 当前不可用。`);
}
return setCurrentMail2925Account(account.id, { updateLastUsedAt: markUsed });
}
function normalizeCookieDomainForMatch(domain) {
return String(domain || '').trim().replace(/^\.+/, '').toLowerCase();
}
function shouldClearMail2925Cookie(cookie) {
const domain = normalizeCookieDomainForMatch(cookie?.domain);
if (!domain) return false;
return MAIL2925_COOKIE_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 collectMail2925Cookies() {
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 (!shouldClearMail2925Cookie(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 removeMail2925Cookie(cookie) {
const details = {
url: buildCookieRemovalUrl(cookie),
name: cookie.name,
};
if (cookie.storeId) {
details.storeId = cookie.storeId;
}
if (cookie.partitionKey) {
details.partitionKey = cookie.partitionKey;
}
try {
return Boolean(await chrome.cookies.remove(details));
} catch {
return false;
}
}
async function clearMail2925SessionCookies() {
if (!chrome.cookies?.getAll || !chrome.cookies?.remove) {
return 0;
}
const cookies = await collectMail2925Cookies();
let removedCount = 0;
for (const cookie of cookies) {
throwIfStopped();
if (await removeMail2925Cookie(cookie)) {
removedCount += 1;
}
}
if (chrome.browsingData?.removeCookies) {
try {
await chrome.browsingData.removeCookies({
since: 0,
origins: MAIL2925_COOKIE_ORIGINS,
});
} catch (_) {
// Best effort cleanup only.
}
}
return removedCount;
}
async function recoverMail2925LoginPageAfterTransportError(tabId) {
const numericTabId = Number(tabId);
if (!Number.isInteger(numericTabId) || numericTabId <= 0) {
return;
}
const currentUrl = (await getMail2925TabUrlById(numericTabId)) || await getMail2925CurrentTabUrl();
await addLog(
`2925:登录提交后页面发生跳转或重载,正在等待当前标签页恢复后继续确认登录态。当前地址:${currentUrl || 'unknown'}`,
'warn'
);
if (typeof waitForTabComplete === 'function') {
const completedTab = await waitForTabComplete(numericTabId, {
timeoutMs: MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS,
retryDelayMs: 300,
});
await addLog(
`2925:登录跳转等待结束,当前标签地址:${String(completedTab?.url || '').trim() || 'unknown'}`,
completedTab?.url ? 'info' : 'warn'
);
}
if (typeof ensureContentScriptReadyOnTab === 'function') {
await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, numericTabId, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
timeoutMs: MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS,
retryDelayMs: 800,
logMessage: '步骤 0:2925 登录后页面仍在跳转,正在等待邮箱页重新就绪...',
});
}
const recoveredUrl = (await getMail2925TabUrlById(numericTabId)) || await getMail2925CurrentTabUrl();
await addLog(`2925:登录跳转恢复后当前标签地址:${recoveredUrl || 'unknown'}`, 'info');
}
async function ensureMail2925MailboxSession(options = {}) {
const {
accountId = null,
forceRelogin = false,
actionLabel = '确保 2925 邮箱登录态',
allowLoginWhenOnLoginPage = true,
expectedMailboxEmail = '',
} = options;
const normalizedExpectedMailboxEmail = normalizeMailboxEmail(expectedMailboxEmail);
let account = null;
if (forceRelogin || (allowLoginWhenOnLoginPage && normalizedExpectedMailboxEmail)) {
account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: accountId,
});
}
const sendLoginMessage = typeof sendToContentScriptResilient === 'function'
? sendToContentScriptResilient
: async (source, message, runtimeOptions = {}) => sendToMailContentScriptResilient(
getMail2925MailConfig(),
message,
{
timeoutMs: runtimeOptions.timeoutMs,
responseTimeoutMs: runtimeOptions.responseTimeoutMs,
maxRecoveryAttempts: 0,
}
);
const buildSuccessPayload = () => ({
account,
mail: getMail2925MailConfig(),
result: {
loggedIn: true,
currentView: 'mailbox',
usedExistingSession: true,
},
});
const failMailboxSession = async (message) => {
const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`);
if (stopped) {
throw new Error('流程已被用户停止。');
}
throw new Error(message);
};
if (forceRelogin) {
const removedCount = await clearMail2925SessionCookies();
await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info');
if (typeof sleepWithStop === 'function') {
await addLog('2925:清理 cookie 后等待 3 秒,再打开登录页...', 'info');
await sleepWithStop(3000);
}
}
throwIfStopped();
const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL;
await addLog(
forceRelogin
? `2925:准备打开登录页 ${MAIL2925_LOGIN_URL}(强制重登录)`
: `2925:准备打开邮箱页 ${MAIL2925_URL}(登录页自动登录=${allowLoginWhenOnLoginPage ? '开启' : '关闭'}`,
'info'
);
const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
});
let openedUrl = await getMail2925TabUrlById(tabId);
if (!openedUrl) {
openedUrl = await getMail2925CurrentTabUrl();
}
await addLog(`2925:打开页后当前标签地址:${openedUrl || 'unknown'}`, 'info');
if (forceRelogin && typeof waitForTabUrlMatch === 'function') {
const matchedLoginTab = await waitForTabUrlMatch(
tabId,
(url) => isMail2925LoginUrl(url),
{ timeoutMs: 15000, retryDelayMs: 300 }
);
await addLog(`2925:等待最终落到登录页结果:${matchedLoginTab?.url || '超时'}`, matchedLoginTab ? 'info' : 'warn');
if (matchedLoginTab?.url) {
openedUrl = String(matchedLoginTab.url || '').trim();
}
}
if (!forceRelogin && !isMail2925LoginUrl(openedUrl) && !normalizedExpectedMailboxEmail) {
await addLog('2925:当前邮箱页未跳转到登录页,将直接复用已登录会话。', 'info');
return buildSuccessPayload();
}
if (!forceRelogin && isMail2925LoginUrl(openedUrl) && !allowLoginWhenOnLoginPage) {
await failMailboxSession(`2925${actionLabel}失败,当前页面已跳转到登录页,且当前未启用 2925 账号池,不执行自动登录。`);
}
if (!account && (forceRelogin || allowLoginWhenOnLoginPage)) {
account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: accountId,
});
}
if (typeof ensureContentScriptReadyOnTab === 'function') {
await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, tabId, {
inject: MAIL2925_INJECT,
injectSource: MAIL2925_INJECT_SOURCE,
timeoutMs: 20000,
retryDelayMs: 800,
logMessage: '步骤 0:2925 登录页内容脚本未就绪,正在等待页面稳定后继续登录...',
});
}
if (forceRelogin && typeof sleepWithStop === 'function') {
await addLog('2925:登录页已打开,等待 3 秒后开始检查输入框并执行登录...', 'info');
await sleepWithStop(3000);
}
let result;
const sendEnsureSessionRequest = async () => {
const beforeSendUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl();
await addLog(`2925:发送 ENSURE_MAIL2925_SESSION 前当前地址:${beforeSendUrl || 'unknown'}`, 'info');
return sendLoginMessage(
MAIL2925_SOURCE,
{
type: 'ENSURE_MAIL2925_SESSION',
step: 0,
source: 'background',
payload: {
email: account?.email || '',
password: account?.password || '',
forceLogin: forceRelogin,
allowLoginWhenOnLoginPage,
},
},
{
timeoutMs: MAIL2925_LOGIN_MESSAGE_RETRY_WINDOW_MS,
retryDelayMs: 800,
responseTimeoutMs: MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS,
logMessage: '步骤 0:2925 登录页通信异常,正在等待页面恢复...',
}
);
};
try {
result = await sendEnsureSessionRequest();
} catch (err) {
if (isRetryableMail2925TransportError(err)) {
try {
await recoverMail2925LoginPageAfterTransportError(tabId);
await addLog('2925:页面恢复完成,正在重新确认登录态...', 'info');
result = await sendEnsureSessionRequest();
} catch (recoveryErr) {
err = recoveryErr;
}
}
if (!result) {
const message = `2925${actionLabel}失败(${getErrorMessage(err) || '登录结果确认超时'})。`;
const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`);
if (stopped) {
throw new Error('流程已被用户停止。');
}
throw err;
}
}
if (result?.error) {
await failMailboxSession(`2925${actionLabel}失败(${result.error})。`);
}
if (result?.limitReached) {
throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '子邮箱已达上限邮箱'}`);
}
const actualMailboxEmail = normalizeMailboxEmail(result?.mailboxEmail || '');
if (normalizedExpectedMailboxEmail && actualMailboxEmail && actualMailboxEmail !== normalizedExpectedMailboxEmail) {
if (allowLoginWhenOnLoginPage) {
await addLog(
`2925:当前邮箱页显示账号 ${actualMailboxEmail},与目标账号 ${normalizedExpectedMailboxEmail} 不一致,准备登出当前账号并登录目标账号。`,
'warn'
);
return ensureMail2925MailboxSession({
accountId: account?.id || accountId || null,
forceRelogin: true,
allowLoginWhenOnLoginPage: true,
expectedMailboxEmail: normalizedExpectedMailboxEmail,
actionLabel,
});
}
await failMailboxSession(
`2925${actionLabel}失败,当前邮箱页显示账号 ${actualMailboxEmail},与目标账号 ${normalizedExpectedMailboxEmail} 不一致,且当前未启用 2925 账号池。`
);
}
if (normalizedExpectedMailboxEmail && !actualMailboxEmail && result?.currentView === 'mailbox') {
await addLog('2925:未能识别当前邮箱页顶部邮箱地址,已跳过邮箱一致性校验。', 'warn');
}
if (!result?.loggedIn) {
await failMailboxSession(`2925${actionLabel}失败,登录后仍未进入收件箱。`);
}
if (!account) {
await addLog('2925:未触发自动登录,继续复用当前已登录会话。', 'info');
return {
account: null,
mail: getMail2925MailConfig(),
result: {
...result,
usedExistingSession: true,
},
};
}
await patchMail2925Account(account.id, {
lastLoginAt: Date.now(),
lastError: '',
});
await setState({ currentMail2925AccountId: account.id });
broadcastDataUpdate({ currentMail2925AccountId: account.id });
const finalUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl();
await addLog(`2925:登录态确认成功,当前地址=${finalUrl || 'unknown'}`, 'ok');
return {
account: await ensureMail2925AccountForFlow({
allowAllocate: false,
preferredAccountId: account.id,
}),
mail: getMail2925MailConfig(),
result,
};
}
async function handleMail2925LimitReachedError(step, error) {
const reason = getErrorMessage(error).replace(MAIL2925_LIMIT_ERROR_PREFIX, '').trim()
|| '子邮箱已达上限邮箱';
const state = await getState();
const currentAccount = getCurrentMail2925Account(state);
const poolEnabled = Boolean(state?.mail2925UseAccountPool);
if (!poolEnabled) {
if (typeof requestStop === 'function') {
await requestStop({
logMessage: `步骤 ${step}2925 检测到“${reason}”,当前未启用账号池,已按手动停止逻辑暂停自动流程。`,
});
}
return new Error('流程已被用户停止。');
}
if (!currentAccount) {
if (typeof requestStop === 'function') {
await requestStop({
logMessage: `步骤 ${step}2925 检测到“${reason}”,但当前没有可识别的账号可供切换。`,
});
}
return new Error('流程已被用户停止。');
}
const disabledUntil = Date.now() + Math.max(1, Number(MAIL2925_LIMIT_COOLDOWN_MS) || (24 * 60 * 60 * 1000));
await patchMail2925Account(currentAccount.id, {
lastLimitAt: Date.now(),
disabledUntil,
lastError: reason,
});
await addLog(
`步骤 ${step}2925 账号 ${currentAccount.email} 命中“${reason}”,已禁用到 ${new Date(disabledUntil).toLocaleString('zh-CN', { hour12: false })}。`,
'warn'
);
const nextState = await getState();
const nextAccounts = normalizeMail2925Accounts(nextState.mail2925Accounts);
const nextAccount = pickMail2925AccountForRun(nextAccounts, {
excludeIds: [currentAccount.id],
});
if (!nextAccount) {
await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
if (typeof requestStop === 'function') {
await requestStop({
logMessage: `步骤 ${step}2925 账号 ${currentAccount.email} 命中“${reason}”,但当前没有可切换的下一个账号。`,
});
}
return new Error('流程已被用户停止。');
}
await setCurrentMail2925Account(nextAccount.id);
await ensureMail2925MailboxSession({
accountId: nextAccount.id,
forceRelogin: true,
allowLoginWhenOnLoginPage: true,
actionLabel: `步骤 ${step}:切换 2925 账号`,
});
await addLog(`步骤 ${step}2925 已切换到下一个账号 ${nextAccount.email}`, 'warn');
return buildMail2925ThreadTerminatedError(
`步骤 ${step}2925 账号 ${currentAccount.email} 命中“${reason}”,已切换到 ${nextAccount.email},当前尝试结束,等待下一轮重试。`
);
}
return {
MAIL2925_LIMIT_ERROR_PREFIX,
MAIL2925_THREAD_TERMINATED_ERROR_PREFIX,
clearMail2925SessionCookies,
deleteMail2925Account,
deleteMail2925Accounts,
ensureMail2925AccountForFlow,
ensureMail2925MailboxSession,
getCurrentMail2925Account,
getMail2925MailConfig,
handleMail2925LimitReachedError,
isMail2925LimitReachedError,
isMail2925ThreadTerminatedError,
patchMail2925Account,
setCurrentMail2925Account,
syncMail2925Accounts,
upsertMail2925Account,
};
}
return {
createMail2925SessionManager,
};
});
+105
View File
@@ -25,6 +25,7 @@
deleteUsedIcloudAliases,
disableUsedLuckmailPurchases,
doesStepUseCompletionSignal,
ensureMail2925MailboxSession,
ensureManualInteractionAllowed,
executeStep,
executeStepViaCompletionSignal,
@@ -35,9 +36,11 @@
findHotmailAccount,
flushCommand,
getCurrentLuckmailPurchase,
getCurrentMail2925Account,
getPendingAutoRunTimerPlan,
getSourceLabel,
getState,
getTabId,
getStopRequested,
handleAutoRunLoopUnhandledError,
importSettingsBundle,
@@ -48,14 +51,17 @@
isLocalhostOAuthCallbackUrl,
isLuckmailProvider,
isStopError,
isTabAlive,
launchAutoRunTimerPlan,
listIcloudAliases,
listLuckmailPurchasesForManagement,
normalizeHotmailAccounts,
normalizeMail2925Accounts,
normalizeRunCount,
AUTO_RUN_TIMER_KIND_SCHEDULED_START,
notifyStepComplete,
notifyStepError,
patchMail2925Account,
patchHotmailAccount,
pollContributionStatus,
registerTab,
@@ -65,6 +71,7 @@
resumeAutoRun,
scheduleAutoRun,
selectLuckmailPurchase,
setCurrentMail2925Account,
setCurrentHotmailAccount,
setContributionMode,
setEmailState,
@@ -81,8 +88,11 @@
skipStep,
startContributionFlow,
startAutoRunLoop,
deleteMail2925Account,
deleteMail2925Accounts,
syncHotmailAccounts,
testHotmailAccountMailAccess,
upsertMail2925Account,
upsertHotmailAccount,
verifyHotmailAccount,
} = deps;
@@ -100,6 +110,23 @@
return appendAccountRunRecord(status, state, reason);
}
async function ensureManualStepPrerequisites(step) {
if (step !== 4) {
return;
}
const signupTabId = typeof getTabId === 'function'
? await getTabId('signup-page')
: null;
const signupTabAlive = signupTabId && typeof isTabAlive === 'function'
? await isTabAlive('signup-page')
: Boolean(signupTabId);
if (!signupTabId || !signupTabAlive) {
throw new Error('手动执行步骤 4 前,请先执行步骤 1 或步骤 2,确保认证页仍然打开并停留在验证码页。');
}
}
async function handleStepData(step, payload) {
switch (step) {
case 1: {
@@ -113,6 +140,8 @@
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
if (Object.keys(updates).length) {
await setState(updates);
}
@@ -122,6 +151,18 @@
if (payload.email) {
await setEmailState(payload.email);
}
if (payload.skipRegistrationFlow) {
const latestState = await getState();
for (const skipStep of [3, 4, 5]) {
const status = latestState.stepStatuses?.[skipStep];
if (status === 'running' || status === 'completed' || status === 'manual_completed') {
continue;
}
await setStepStatus(skipStep, 'skipped');
}
await addLog('步骤 2:检测到当前已登录会话,已自动跳过步骤 3/4/5,流程将直接进入步骤 6。', 'warn');
break;
}
if (payload.skippedPasswordStep) {
const latestState = await getState();
const step3Status = latestState.stepStatuses?.[3];
@@ -150,6 +191,14 @@
lastEmailTimestamp: payload.emailTimestamp || null,
signupVerificationRequestedAt: null,
});
if (payload.skipProfileStep) {
const latestState = await getState();
const step5Status = latestState.stepStatuses?.[5];
if (step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') {
await setStepStatus(5, 'skipped');
await addLog('步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。', 'warn');
}
}
break;
case 8:
await setState({
@@ -178,6 +227,13 @@
});
await addLog('当前 Hotmail 账号已自动标记为已用。', 'ok');
}
if (String(latestState.mailProvider || '').trim().toLowerCase() === '2925' && latestState.currentMail2925AccountId) {
await patchMail2925Account(latestState.currentMail2925AccountId, {
lastUsedAt: Date.now(),
lastError: '',
});
await addLog('当前 2925 账号已记录最近使用时间。', 'ok');
}
if (isLuckmailProvider(latestState)) {
const currentPurchase = getCurrentLuckmailPurchase(latestState);
if (currentPurchase?.id) {
@@ -387,6 +443,9 @@
await ensureManualInteractionAllowed('手动执行步骤');
}
const step = message.payload.step;
if (message.source === 'sidepanel') {
await ensureManualStepPrerequisites(step);
}
if (message.source === 'sidepanel') {
await invalidateDownstreamAfterStepRestart(step, { logLabel: `步骤 ${step} 重新执行` });
}
@@ -578,6 +637,52 @@
return { ok: true, ...result };
}
case 'UPSERT_MAIL2925_ACCOUNT': {
const account = await upsertMail2925Account(message.payload || {});
return { ok: true, account };
}
case 'DELETE_MAIL2925_ACCOUNT': {
await deleteMail2925Account(String(message.payload?.accountId || ''));
return { ok: true };
}
case 'DELETE_MAIL2925_ACCOUNTS': {
const result = await deleteMail2925Accounts(String(message.payload?.mode || 'all'));
return { ok: true, ...result };
}
case 'SELECT_MAIL2925_ACCOUNT': {
const account = await setCurrentMail2925Account(String(message.payload?.accountId || ''), {
updateLastUsedAt: false,
});
return { ok: true, account };
}
case 'PATCH_MAIL2925_ACCOUNT': {
const account = await patchMail2925Account(
String(message.payload?.accountId || ''),
message.payload?.updates || {}
);
return { ok: true, account };
}
case 'LOGIN_MAIL2925_ACCOUNT': {
const accountId = String(message.payload?.accountId || '');
const account = await setCurrentMail2925Account(accountId, {
updateLastUsedAt: false,
});
if (typeof deps.ensureMail2925MailboxSession !== 'function') {
throw new Error('2925 登录能力尚未接入。');
}
await deps.ensureMail2925MailboxSession({
accountId: account.id,
forceRelogin: Boolean(message.payload?.forceRelogin),
actionLabel: '侧边栏手动登录 2925 账号',
});
return { ok: true, account };
}
case 'LIST_LUCKMAIL_PURCHASES': {
const purchases = await listLuckmailPurchasesForManagement();
return { ok: true, purchases };
+35 -2
View File
@@ -3,6 +3,7 @@
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundNavigationUtilsModule() {
function createNavigationUtils(deps = {}) {
const {
DEFAULT_CODEX2API_URL,
DEFAULT_SUB2API_URL,
normalizeLocalCpaStep9Mode,
} = deps;
@@ -27,13 +28,36 @@
return parsed.toString();
}
function 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 = {}) {
return state.panelMode === 'sub2api' ? 'sub2api' : 'cpa';
if (state.panelMode === 'sub2api') {
return 'sub2api';
}
if (state.panelMode === 'codex2api') {
return 'codex2api';
}
return 'cpa';
}
function getPanelModeLabel(modeOrState) {
const mode = typeof modeOrState === 'string' ? modeOrState : getPanelMode(modeOrState);
return mode === 'sub2api' ? 'SUB2API' : 'CPA';
if (mode === 'sub2api') {
return 'SUB2API';
}
if (mode === 'codex2api') {
return 'Codex2API';
}
return 'CPA';
}
function isSignupPageHost(hostname = '') {
@@ -124,6 +148,14 @@
|| candidate.pathname.startsWith('/login')
|| candidate.pathname === '/'
);
case 'codex2api-panel':
return Boolean(reference)
&& candidate.origin === reference.origin
&& (
candidate.pathname.startsWith('/admin/accounts')
|| candidate.pathname === '/admin'
|| candidate.pathname === '/'
);
default:
return false;
}
@@ -160,6 +192,7 @@
isSignupPageHost,
isSignupPasswordPageUrl,
matchesSourceUrlFamily,
normalizeCodex2ApiUrl,
normalizeSub2ApiUrl,
parseUrlSafely,
shouldBypassStep9ForLocalCpa,
+102
View File
@@ -8,6 +8,7 @@
closeConflictingTabsForSource,
ensureContentScriptReadyOnTab,
getPanelMode,
normalizeCodex2ApiUrl,
normalizeSub2ApiUrl,
rememberSourceLastUrl,
sendToContentScript,
@@ -17,7 +18,74 @@
SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
} = deps;
function normalizeAdminKey(value = '') {
return String(value || '').trim();
}
function extractStateFromAuthUrl(authUrl = '') {
try {
return new URL(authUrl).searchParams.get('state') || '';
} catch {
return '';
}
}
function getCodex2ApiErrorMessage(payload, responseStatus = 500) {
const candidates = [
payload?.error,
payload?.message,
payload?.detail,
payload?.reason,
];
const message = candidates
.map((value) => String(value || '').trim())
.find(Boolean);
return message || `Codex2API 请求失败(HTTP ${responseStatus})。`;
}
async function fetchCodex2ApiJson(origin, path, options = {}) {
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(`${origin}${path}`, {
method: options.method || 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Admin-Key': normalizeAdminKey(options.adminKey),
},
body: options.body === undefined ? undefined : JSON.stringify(options.body),
signal: controller.signal,
});
let payload = {};
try {
payload = await response.json();
} catch {
payload = {};
}
if (!response.ok) {
throw new Error(getCodex2ApiErrorMessage(payload, response.status));
}
return payload;
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error('Codex2API 请求超时,请稍后重试。');
}
throw error;
} finally {
clearTimeout(timer);
}
}
async function requestOAuthUrlFromPanel(state, options = {}) {
if (getPanelMode(state) === 'codex2api') {
return requestCodex2ApiOAuthUrl(state, options);
}
if (getPanelMode(state) === 'sub2api') {
return requestSub2ApiOAuthUrl(state, options);
}
@@ -74,6 +142,39 @@
return result || {};
}
async function requestCodex2ApiOAuthUrl(state, options = {}) {
const { logLabel = 'OAuth 刷新' } = options;
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
const adminKey = normalizeAdminKey(state.codex2apiAdminKey);
if (!adminKey) {
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
}
const origin = new URL(codex2apiUrl).origin;
await addLog(`${logLabel}:正在通过 Codex2API 协议生成 OAuth 授权链接...`);
const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/generate-auth-url', {
adminKey,
method: 'POST',
body: {},
});
const oauthUrl = String(result?.auth_url || result?.authUrl || '').trim();
const sessionId = String(result?.session_id || result?.sessionId || '').trim();
const oauthState = extractStateFromAuthUrl(oauthUrl);
if (!oauthUrl || !sessionId) {
throw new Error('Codex2API 未返回有效的 auth_url 或 session_id。');
}
return {
oauthUrl,
codex2apiSessionId: sessionId,
codex2apiOAuthState: oauthState || null,
};
}
async function requestSub2ApiOAuthUrl(state, options = {}) {
const { logLabel = 'OAuth 刷新' } = options;
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
@@ -135,6 +236,7 @@
return {
requestOAuthUrlFromPanel,
requestCodex2ApiOAuthUrl,
requestCpaOAuthUrl,
requestSub2ApiOAuthUrl,
};
+771
View File
@@ -0,0 +1,771 @@
(function attachBackgroundPhoneVerification(root, factory) {
root.MultiPageBackgroundPhoneVerification = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPhoneVerificationModule() {
function createPhoneVerificationHelpers(deps = {}) {
const {
addLog,
ensureStep8SignupPageReady,
fetchImpl = (...args) => fetch(...args),
getOAuthFlowStepTimeoutMs,
getState,
sendToContentScriptResilient,
setState,
sleepWithStop,
throwIfStopped,
DEFAULT_HERO_SMS_BASE_URL = 'https://hero-sms.com/stubs/handler_api.php',
HERO_SMS_COUNTRY_ID = 52,
HERO_SMS_COUNTRY_LABEL = 'Thailand',
HERO_SMS_SERVICE_CODE = 'dr',
HERO_SMS_SERVICE_LABEL = 'OpenAI',
} = deps;
const PHONE_ACTIVATION_STATE_KEY = 'currentPhoneActivation';
const REUSABLE_PHONE_ACTIVATION_STATE_KEY = 'reusablePhoneActivation';
const DEFAULT_PHONE_POLL_INTERVAL_MS = 5000;
const DEFAULT_PHONE_POLL_TIMEOUT_MS = 180000;
const DEFAULT_PHONE_REQUEST_TIMEOUT_MS = 20000;
const DEFAULT_PHONE_SUBMIT_ATTEMPTS = 3;
const DEFAULT_PHONE_CODE_WAIT_WINDOW_MS = 60000;
const DEFAULT_PHONE_NUMBER_MAX_USES = 3;
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::';
function normalizeUrl(value, fallback = DEFAULT_HERO_SMS_BASE_URL) {
const trimmed = String(value || '').trim();
if (!trimmed) {
return fallback;
}
try {
return new URL(trimmed).toString();
} catch {
return fallback;
}
}
function normalizeApiKey(value) {
return String(value || '').trim();
}
function normalizeUseCount(value) {
return Math.max(0, Math.floor(Number(value) || 0));
}
function resolveCountryConfig(state = {}) {
return {
id: Math.max(1, Math.floor(Number(state.heroSmsCountryId) || HERO_SMS_COUNTRY_ID)),
label: String(state.heroSmsCountryLabel || HERO_SMS_COUNTRY_LABEL).trim() || HERO_SMS_COUNTRY_LABEL,
};
}
function normalizeActivation(record) {
if (!record || typeof record !== 'object' || Array.isArray(record)) {
return null;
}
const activationId = String(
record.activationId ?? record.id ?? record.activation ?? ''
).trim();
const phoneNumber = String(
record.phoneNumber ?? record.number ?? record.phone ?? ''
).trim();
if (!activationId || !phoneNumber) {
return null;
}
return {
activationId,
phoneNumber,
provider: String(record.provider || 'hero-sms').trim() || 'hero-sms',
serviceCode: String(record.serviceCode || HERO_SMS_SERVICE_CODE).trim() || HERO_SMS_SERVICE_CODE,
countryId: Number(record.countryId) || HERO_SMS_COUNTRY_ID,
successfulUses: normalizeUseCount(record.successfulUses),
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)),
};
}
function describeHeroSmsPayload(raw) {
if (typeof raw === 'string') {
return raw.trim();
}
if (raw && typeof raw === 'object') {
if (raw.title || raw.details) {
const title = String(raw.title || '').trim();
const details = String(raw.details || '').trim();
return details ? `${title}: ${details}` : title;
}
if (raw.status === 'false' && raw.msg) {
return String(raw.msg).trim();
}
try {
return JSON.stringify(raw);
} catch {
return String(raw);
}
}
return String(raw || '').trim();
}
function parseHeroSmsPayload(text) {
const trimmed = String(text || '').trim();
if (!trimmed) {
return '';
}
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
try {
return JSON.parse(trimmed);
} catch {
return trimmed;
}
}
return trimmed;
}
function buildHeroSmsUrl(baseUrl, query = {}) {
const url = new URL(normalizeUrl(baseUrl));
Object.entries(query).forEach(([key, value]) => {
if (value === undefined || value === null || value === '') {
return;
}
url.searchParams.set(key, String(value));
});
return url.toString();
}
function buildPhoneCodeTimeoutError(lastResponse = '') {
const suffix = lastResponse ? ` Last HeroSMS status: ${lastResponse}` : '';
return new Error(`${PHONE_CODE_TIMEOUT_ERROR_PREFIX}Timed out waiting for the phone verification code.${suffix}`);
}
function isPhoneCodeTimeoutError(error) {
return String(error?.message || '').startsWith(PHONE_CODE_TIMEOUT_ERROR_PREFIX);
}
function buildPhoneRestartStep7Error(phoneNumber = '') {
const suffix = phoneNumber ? ` Current number: ${phoneNumber}.` : '';
return new Error(
`${PHONE_RESTART_STEP7_ERROR_PREFIX}Phone verification could not receive an SMS after resend. Restart step 7 with a new number.${suffix}`
);
}
function sanitizePhoneCodeTimeoutError(error) {
const message = String(error?.message || '');
if (!message.startsWith(PHONE_CODE_TIMEOUT_ERROR_PREFIX)) {
return error;
}
return new Error(message.slice(PHONE_CODE_TIMEOUT_ERROR_PREFIX.length).trim() || 'Timed out waiting for the phone verification code.');
}
function sanitizePhoneRestartStep7Error(error) {
const message = String(error?.message || '');
if (!message.startsWith(PHONE_RESTART_STEP7_ERROR_PREFIX)) {
return error;
}
return new Error(
message.slice(PHONE_RESTART_STEP7_ERROR_PREFIX.length).trim()
|| 'Phone verification could not receive an SMS after resend. Restart step 7 with a new number.'
);
}
async function fetchHeroSmsPayload(config, query, actionLabel) {
const requestUrl = buildHeroSmsUrl(config.baseUrl, {
api_key: config.apiKey,
...query,
});
const controller = typeof AbortController === 'function' ? new AbortController() : null;
const timeoutId = controller
? setTimeout(() => controller.abort(), DEFAULT_PHONE_REQUEST_TIMEOUT_MS)
: null;
try {
const response = await fetchImpl(requestUrl, {
method: 'GET',
signal: controller?.signal,
});
const text = await response.text();
const payload = parseHeroSmsPayload(text);
if (!response.ok) {
throw new Error(`${actionLabel} failed: ${describeHeroSmsPayload(payload) || response.status}`);
}
return payload;
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`${actionLabel} timed out.`);
}
throw error;
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
}
function resolvePhoneConfig(state = {}) {
const apiKey = normalizeApiKey(state.heroSmsApiKey);
if (!apiKey) {
throw new Error('HeroSMS API key is missing. Save it in the side panel before running the phone flow.');
}
return {
apiKey,
baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL),
};
}
function parseActivationPayload(payload, fallback = null) {
const normalizedFallback = normalizeActivation(fallback);
const directActivation = normalizeActivation(payload);
if (directActivation) {
return {
...directActivation,
successfulUses: normalizedFallback?.successfulUses || directActivation.successfulUses,
maxUses: normalizedFallback?.maxUses || directActivation.maxUses,
};
}
const text = describeHeroSmsPayload(payload);
const accessNumberMatch = text.match(/^ACCESS_NUMBER:([^:]+):(.+)$/i);
if (accessNumberMatch) {
return {
activationId: String(accessNumberMatch[1] || '').trim(),
phoneNumber: String(accessNumberMatch[2] || '').trim(),
provider: normalizedFallback?.provider || 'hero-sms',
serviceCode: normalizedFallback?.serviceCode || HERO_SMS_SERVICE_CODE,
countryId: normalizedFallback?.countryId || HERO_SMS_COUNTRY_ID,
successfulUses: normalizedFallback?.successfulUses || 0,
maxUses: normalizedFallback?.maxUses || DEFAULT_PHONE_NUMBER_MAX_USES,
};
}
if (/^ACCESS_READY$/i.test(text) && normalizedFallback) {
return normalizedFallback;
}
return null;
}
async function requestPhoneActivation(state = {}) {
const config = resolvePhoneConfig(state);
const countryConfig = resolveCountryConfig(state);
const payload = await fetchHeroSmsPayload(config, {
action: 'getNumber',
service: HERO_SMS_SERVICE_CODE,
country: countryConfig.id,
}, 'HeroSMS getNumber');
const activation = parseActivationPayload(payload, {
countryId: countryConfig.id,
});
if (!activation) {
const text = describeHeroSmsPayload(payload);
throw new Error(`HeroSMS getNumber failed: ${text || 'empty response'}`);
}
return activation;
}
async function reactivatePhoneActivation(state = {}, activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
throw new Error('Reusable phone activation is missing.');
}
const config = resolvePhoneConfig(state);
const payload = await fetchHeroSmsPayload(config, {
action: 'reactivate',
id: normalizedActivation.activationId,
}, 'HeroSMS reactivate');
const nextActivation = parseActivationPayload(payload, normalizedActivation);
if (!nextActivation) {
const text = describeHeroSmsPayload(payload);
throw new Error(`HeroSMS reactivate failed: ${text || 'empty response'}`);
}
return nextActivation;
}
async function setPhoneActivationStatus(state = {}, activation, status, actionLabel) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
return '';
}
const config = resolvePhoneConfig(state);
const payload = await fetchHeroSmsPayload(config, {
action: 'setStatus',
id: normalizedActivation.activationId,
status,
}, actionLabel);
return describeHeroSmsPayload(payload);
}
async function completePhoneActivation(state = {}, activation) {
await setPhoneActivationStatus(state, activation, 6, 'HeroSMS setStatus(6)');
}
async function cancelPhoneActivation(state = {}, activation) {
try {
await setPhoneActivationStatus(state, activation, 8, 'HeroSMS setStatus(8)');
} catch (_) {
// Best-effort cleanup.
}
}
async function requestAdditionalPhoneSms(state = {}, activation) {
try {
await setPhoneActivationStatus(state, activation, 3, 'HeroSMS setStatus(3)');
} catch (_) {
// Best-effort request only.
}
}
async function pollPhoneActivationCode(state = {}, activation, options = {}) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
throw new Error('Phone activation is missing.');
}
const config = resolvePhoneConfig(state);
const configuredTimeoutMs = Math.max(1000, Number(options.timeoutMs) || 0);
const timeoutMs = configuredTimeoutMs || (
typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(
DEFAULT_PHONE_POLL_TIMEOUT_MS,
{ step: 9, actionLabel: options.actionLabel || 'poll phone verification code' }
)
: DEFAULT_PHONE_POLL_TIMEOUT_MS
);
const intervalMs = Math.max(1000, Number(options.intervalMs) || DEFAULT_PHONE_POLL_INTERVAL_MS);
const start = Date.now();
let lastResponse = '';
let pollCount = 0;
while (Date.now() - start < timeoutMs) {
throwIfStopped();
const payload = await fetchHeroSmsPayload(config, {
action: 'getStatus',
id: normalizedActivation.activationId,
}, 'HeroSMS getStatus');
const text = describeHeroSmsPayload(payload);
lastResponse = text;
pollCount += 1;
if (typeof options.onStatus === 'function') {
await options.onStatus({
activation: normalizedActivation,
elapsedMs: Date.now() - start,
pollCount,
statusText: text,
timeoutMs,
});
}
const okMatch = text.match(/^STATUS_OK:(.+)$/i);
if (okMatch) {
const rawCode = String(okMatch[1] || '').trim();
const digitMatch = rawCode.match(/\b(\d{4,8})\b/);
return digitMatch?.[1] || rawCode;
}
if (/^STATUS_(WAIT_CODE|WAIT_RETRY|WAIT_RESEND)$/i.test(text)) {
await sleepWithStop(intervalMs);
continue;
}
if (/^STATUS_CANCEL$/i.test(text)) {
throw new Error('HeroSMS activation was cancelled before the SMS arrived.');
}
throw new Error(`HeroSMS getStatus failed: ${text || 'empty response'}`);
}
throw buildPhoneCodeTimeoutError(lastResponse);
}
async function readPhonePageState(tabId, timeoutMs = 10000) {
await ensureStep8SignupPageReady(tabId, {
timeoutMs,
logMessage: 'Step 9: waiting for auth page content script to recover before phone verification.',
});
const result = await sendToContentScriptResilient('signup-page', {
type: 'STEP8_GET_STATE',
source: 'background',
payload: {},
}, {
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 600,
logMessage: 'Step 9: auth page is switching, waiting to inspect phone verification state again...',
});
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
async function submitPhoneNumber(tabId, phoneNumber) {
const state = await getState();
const countryConfig = resolveCountryConfig(state);
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(30000, { step: 9, actionLabel: 'submit add-phone number' })
: 30000;
const result = await sendToContentScriptResilient('signup-page', {
type: 'SUBMIT_PHONE_NUMBER',
source: 'background',
payload: {
phoneNumber,
countryId: countryConfig.id,
countryLabel: countryConfig.label,
},
}, {
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 600,
logMessage: 'Step 9: waiting for add-phone page to become ready...',
});
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
async function submitPhoneVerificationCode(tabId, code) {
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(45000, { step: 9, actionLabel: 'submit phone verification code' })
: 45000;
const result = await sendToContentScriptResilient('signup-page', {
type: 'SUBMIT_PHONE_VERIFICATION_CODE',
source: 'background',
payload: { code },
}, {
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 600,
logMessage: 'Step 9: waiting for phone verification page before filling the SMS code...',
});
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
async function resendPhoneVerificationCode(tabId) {
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(30000, { step: 9, actionLabel: 'resend phone verification code' })
: 30000;
const result = await sendToContentScriptResilient('signup-page', {
type: 'RESEND_PHONE_VERIFICATION_CODE',
source: 'background',
payload: {},
}, {
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 600,
logMessage: 'Step 9: waiting for the phone verification resend button...',
});
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
async function returnToAddPhone(tabId) {
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(30000, { step: 9, actionLabel: 'return to add-phone page' })
: 30000;
const result = await sendToContentScriptResilient('signup-page', {
type: 'RETURN_TO_ADD_PHONE',
source: 'background',
payload: {},
}, {
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 600,
logMessage: 'Step 9: returning to add-phone page to replace the phone number...',
});
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
async function persistCurrentActivation(activation) {
await setState({
[PHONE_ACTIVATION_STATE_KEY]: activation || null,
});
}
async function persistReusableActivation(activation) {
await setState({
[REUSABLE_PHONE_ACTIVATION_STATE_KEY]: activation || null,
});
}
async function clearCurrentActivation() {
await persistCurrentActivation(null);
}
async function clearReusableActivation() {
await persistReusableActivation(null);
}
async function acquirePhoneActivation(state = {}) {
const countryConfig = resolveCountryConfig(state);
const reusableActivation = normalizeActivation(state[REUSABLE_PHONE_ACTIVATION_STATE_KEY]);
if (
reusableActivation
&& reusableActivation.countryId === countryConfig.id
&& reusableActivation.successfulUses < reusableActivation.maxUses
) {
try {
const reactivated = await reactivatePhoneActivation(state, reusableActivation);
await addLog(
`Step 9: reusing ${countryConfig.label} number ${reactivated.phoneNumber} (${reactivated.successfulUses + 1}/${reactivated.maxUses}).`,
'info'
);
return reactivated;
} catch (error) {
await addLog(`Step 9: failed to reuse phone number ${reusableActivation.phoneNumber}, falling back to a new number. ${error.message}`, 'warn');
await clearReusableActivation();
}
} else if (reusableActivation && reusableActivation.countryId !== countryConfig.id) {
await clearReusableActivation();
}
const activation = await requestPhoneActivation(state);
await addLog(
`Step 9: acquired ${HERO_SMS_SERVICE_LABEL} / ${countryConfig.label} number ${activation.phoneNumber}.`,
'info'
);
return activation;
}
async function markActivationReusableAfterSuccess(activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
await clearReusableActivation();
return;
}
const successfulUses = normalizedActivation.successfulUses + 1;
if (successfulUses >= normalizedActivation.maxUses) {
await clearReusableActivation();
return;
}
await persistReusableActivation({
...normalizedActivation,
successfulUses,
});
}
async function waitForPhoneCodeOrRotateNumber(tabId, state, activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
throw new Error('Phone activation is missing.');
}
let lastLoggedStatus = '';
let lastLoggedPollCount = 0;
for (let windowIndex = 1; windowIndex <= 2; windowIndex += 1) {
await addLog(
`Step 9: waiting up to 60 seconds for SMS on ${normalizedActivation.phoneNumber} (${windowIndex}/2).`,
'info'
);
try {
const code = await pollPhoneActivationCode(state, normalizedActivation, {
actionLabel: windowIndex === 1
? 'poll phone verification code from HeroSMS'
: 'poll resent phone verification code from HeroSMS',
timeoutMs: DEFAULT_PHONE_CODE_WAIT_WINDOW_MS,
onStatus: async ({ elapsedMs, pollCount, statusText }) => {
const shouldLog = (
pollCount === 1
|| statusText !== lastLoggedStatus
|| pollCount - lastLoggedPollCount >= 3
);
if (!shouldLog) {
return;
}
lastLoggedStatus = statusText;
lastLoggedPollCount = pollCount;
await addLog(
`Step 9: HeroSMS status for ${normalizedActivation.phoneNumber}: ${statusText} (${Math.ceil(elapsedMs / 1000)}s elapsed).`,
'info'
);
},
});
return {
code,
replaceNumber: false,
};
} catch (error) {
if (!isPhoneCodeTimeoutError(error)) {
throw error;
}
if (windowIndex === 1) {
await addLog(
`Step 9: no SMS arrived for ${normalizedActivation.phoneNumber} within 60 seconds, requesting another SMS.`,
'warn'
);
await requestAdditionalPhoneSms(state, normalizedActivation);
try {
await resendPhoneVerificationCode(tabId);
await addLog('Step 9: clicked "Resend text message" on the phone verification page.', 'info');
} catch (resendError) {
await addLog(`Step 9: failed to click resend on the phone verification page. ${resendError.message}`, 'warn');
}
continue;
}
await addLog(
`Step 9: still no SMS for ${normalizedActivation.phoneNumber} 60 seconds after resend, restarting from step 7 with a new number.`,
'warn'
);
throw buildPhoneRestartStep7Error(normalizedActivation.phoneNumber);
}
}
throw new Error('Phone verification did not complete successfully.');
}
async function completePhoneVerificationFlow(tabId, initialPageState = null) {
let state = await getState();
let activation = normalizeActivation(state[PHONE_ACTIVATION_STATE_KEY]);
let pageState = initialPageState || await readPhonePageState(tabId);
let shouldCancelActivation = false;
let remainingResendRequests = Math.max(0, Number(state.verificationResendCount) || 0);
try {
while (true) {
state = await getState();
if (!activation) {
activation = normalizeActivation(state[PHONE_ACTIVATION_STATE_KEY]);
}
if (pageState?.addPhonePage) {
if (activation) {
await cancelPhoneActivation(state, activation);
await clearCurrentActivation();
activation = null;
shouldCancelActivation = false;
}
activation = await acquirePhoneActivation(state);
shouldCancelActivation = true;
await persistCurrentActivation(activation);
const submitResult = await submitPhoneNumber(tabId, activation.phoneNumber);
await addLog('Step 9: submitted the phone number on add-phone page.', 'info');
pageState = {
...pageState,
...submitResult,
addPhonePage: false,
phoneVerificationPage: true,
};
}
if (!pageState?.phoneVerificationPage) {
pageState = await readPhonePageState(tabId);
}
if (!pageState?.phoneVerificationPage) {
return pageState;
}
if (!activation) {
throw new Error('The auth page is waiting for a phone verification code, but no HeroSMS activation is stored for this run.');
}
let shouldReplaceNumber = false;
for (let attempt = 1; attempt <= DEFAULT_PHONE_SUBMIT_ATTEMPTS; attempt += 1) {
throwIfStopped();
const codeResult = await waitForPhoneCodeOrRotateNumber(tabId, state, activation);
if (codeResult.replaceNumber) {
shouldReplaceNumber = true;
break;
}
await addLog(`Step 9: received phone verification code ${codeResult.code}.`, 'info');
const submitResult = await submitPhoneVerificationCode(tabId, codeResult.code);
if (submitResult.returnedToAddPhone) {
await addLog(
'Step 9: phone verification returned to add-phone after code submission, replacing the current number.',
'warn'
);
shouldReplaceNumber = true;
pageState = {
...pageState,
...submitResult,
addPhonePage: true,
phoneVerificationPage: false,
};
break;
}
if (submitResult.invalidCode) {
if (attempt >= DEFAULT_PHONE_SUBMIT_ATTEMPTS) {
throw new Error(
`Phone verification code was rejected after ${DEFAULT_PHONE_SUBMIT_ATTEMPTS} attempts: ${submitResult.errorText || submitResult.url || 'unknown error'}`
);
}
if (remainingResendRequests > 0) {
remainingResendRequests -= 1;
await requestAdditionalPhoneSms(state, activation);
try {
await resendPhoneVerificationCode(tabId);
await addLog('Step 9: clicked "Resend text message" after the phone code was rejected.', 'info');
} catch (resendError) {
await addLog(`Step 9: failed to click resend after code rejection. ${resendError.message}`, 'warn');
}
await addLog(
`Step 9: phone verification code was rejected, requested another SMS (${remainingResendRequests} resend attempts left).`,
'warn'
);
} else {
await addLog(
'Step 9: phone verification code was rejected and the configured resend budget is exhausted, retrying with the current activation window.',
'warn'
);
}
continue;
}
await completePhoneActivation(state, activation);
await markActivationReusableAfterSuccess(activation);
shouldCancelActivation = false;
await clearCurrentActivation();
await addLog('Step 9: phone verification finished, waiting for OAuth consent.', 'ok');
return submitResult;
}
if (!shouldReplaceNumber) {
throw new Error('Phone verification did not complete successfully.');
}
}
} catch (error) {
if (shouldCancelActivation && activation) {
await cancelPhoneActivation(state, activation);
}
await clearCurrentActivation();
throw sanitizePhoneRestartStep7Error(sanitizePhoneCodeTimeoutError(error));
}
}
return {
completePhoneVerificationFlow,
normalizeActivation,
pollPhoneActivationCode,
reactivatePhoneActivation,
requestPhoneActivation,
};
}
return {
createPhoneVerificationHelpers,
};
});
+38 -14
View File
@@ -3,14 +3,17 @@
})(typeof self !== 'undefined' ? self : globalThis, function createSignupFlowHelpersModule() {
function createSignupFlowHelpers(deps = {}) {
const {
addLog,
buildGeneratedAliasEmail,
chrome,
ensureContentScriptReadyOnTab,
ensureHotmailAccountForFlow,
ensureMail2925AccountForFlow,
ensureLuckmailPurchaseForFlow,
isGeneratedAliasProvider,
isReusableGeneratedAliasEmail,
isHotmailProvider,
isRetryableContentScriptTransportError = () => false,
isLuckmailProvider,
isSignupEmailVerificationPageUrl,
isSignupPasswordPageUrl,
@@ -163,20 +166,32 @@
logMessage: `步骤 ${step}:认证页仍在切换,正在等待页面恢复后继续确认提交流程...`,
});
const result = await sendToContentScriptResilient('signup-page', {
type: 'PREPARE_SIGNUP_VERIFICATION',
step,
source: 'background',
payload: {
password: password || '',
prepareSource: 'step3_finalize',
prepareLogLabel: '步骤 3 收尾',
},
}, {
timeoutMs: 30000,
retryDelayMs: 700,
logMessage: `步骤 ${step}:密码已提交,正在确认是否进入下一页面,必要时自动恢复重试页...`,
});
let result;
try {
result = await sendToContentScriptResilient('signup-page', {
type: 'PREPARE_SIGNUP_VERIFICATION',
step,
source: 'background',
payload: {
password: password || '',
prepareSource: 'step3_finalize',
prepareLogLabel: '步骤 3 收尾',
},
}, {
timeoutMs: 30000,
retryDelayMs: 700,
logMessage: `步骤 ${step}:密码已提交,正在确认是否进入下一页面,必要时自动恢复重试页...`,
});
} catch (error) {
if (isRetryableContentScriptTransportError(error)) {
const message = `步骤 ${step}:认证页在提交后切换过程中页面通信超时,未能重新就绪,暂时无法确认是否进入下一页面。请重试当前轮。`;
if (typeof addLog === 'function') {
await addLog(message, 'warn');
}
throw new Error(message);
}
throw error;
}
if (result?.error) {
throw new Error(result.error);
@@ -198,6 +213,15 @@
const purchase = await ensureLuckmailPurchaseForFlow({ allowReuse: true });
resolvedEmail = purchase.email_address;
} else if (isGeneratedAliasProvider(state)) {
if (Boolean(state?.mail2925UseAccountPool)
&& String(state?.mailProvider || '').trim().toLowerCase() === '2925'
&& typeof ensureMail2925AccountForFlow === 'function') {
await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: state.currentMail2925AccountId || null,
markUsed: true,
});
}
if (!isReusableGeneratedAliasEmail?.(state, resolvedEmail)) {
resolvedEmail = buildGeneratedAliasEmail(state);
}
+2 -8
View File
@@ -41,11 +41,11 @@
await addLog('步骤 9:正在监听 localhost 回调地址...');
const callbackTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(120000, {
? await getOAuthFlowStepTimeoutMs(240000, {
step: 9,
actionLabel: 'OAuth localhost 回调',
})
: 120000;
: 240000;
return new Promise((resolve, reject) => {
let resolved = false;
@@ -200,12 +200,6 @@
break;
}
if (effect.restartCurrentStep) {
await addLog(`步骤 9${getStep8EffectLabel(effect)},准备重新定位“继续”按钮并重试...`, 'warn');
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
continue;
}
if (round >= STEP8_MAX_ROUNDS) {
throw new Error(`步骤 9:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`);
}
+65 -17
View File
@@ -1,12 +1,16 @@
(function attachBackgroundStep8(root, factory) {
root.MultiPageBackgroundStep8 = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() {
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
function createStep8Executor(deps = {}) {
const {
addLog,
chrome,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
confirmCustomVerificationStepBypass,
ensureMail2925MailboxSession,
ensureIcloudMailSession,
ensureStep8VerificationPageReady,
getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs,
@@ -55,11 +59,50 @@
return String(value || '').trim().toLowerCase();
}
function getExpectedMail2925MailboxEmail(state = {}) {
if (Boolean(state?.mail2925UseAccountPool)) {
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
if (accountEmail) {
return accountEmail;
}
}
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
}
async function focusOrOpenMailTab(mail) {
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
return;
}
const tabId = await getTabId(mail.source);
await chrome.tabs.update(tabId, { active: true });
return;
}
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
async function runStep8Attempt(state) {
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const stepStartedAt = Date.now();
const verificationFilterAfterTimestamp = mail.provider === '2925'
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: stepStartedAt;
const verificationSessionKey = `8:${stepStartedAt}`;
const authTabId = await getTabId('signup-page');
@@ -98,6 +141,15 @@
return;
}
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
await addLog('步骤 8:正在确认 iCloud 邮箱登录态...', 'info');
await ensureIcloudMailSession({
state,
step: 8,
actionLabel: '步骤 8:确认 iCloud 邮箱登录态',
});
}
throwIfStopped();
if (
mail.provider === HOTMAIL_PROVIDER
@@ -107,23 +159,19 @@
await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`);
} else {
await addLog(`步骤 8:正在打开${mail.label}...`);
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
} else {
const tabId = await getTabId(mail.source);
await chrome.tabs.update(tabId, { active: true });
}
} else {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: 'Step 8: ensure 2925 mailbox session',
});
} else {
await focusOrOpenMailTab(mail);
}
if (mail.provider === '2925') {
await addLog(`步骤 8:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info');
}
}
@@ -131,7 +179,7 @@
...state,
step8VerificationTargetEmail: displayedVerificationEmail || '',
}, mail, {
filterAfterTimestamp: mail.provider === '2925' ? 0 : stepStartedAt,
filterAfterTimestamp: verificationFilterAfterTimestamp,
sessionKey: verificationSessionKey,
disableTimeBudgetCap: mail.provider === '2925',
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || ''),
+87 -26
View File
@@ -1,12 +1,16 @@
(function attachBackgroundStep4(root, factory) {
root.MultiPageBackgroundStep4 = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep4Module() {
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
function createStep4Executor(deps = {}) {
const {
addLog,
chrome,
completeStepFromBackground,
confirmCustomVerificationStepBypass,
ensureMail2925MailboxSession,
ensureIcloudMailSession,
getMailConfig,
getTabId,
HOTMAIL_PROVIDER,
@@ -21,19 +25,61 @@
throwIfStopped,
} = deps;
function getExpectedMail2925MailboxEmail(state = {}) {
if (Boolean(state?.mail2925UseAccountPool)) {
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
if (accountEmail) {
return accountEmail;
}
}
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
}
async function focusOrOpenMailTab(mail) {
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
return;
}
const tabId = await getTabId(mail.source);
await chrome.tabs.update(tabId, { active: true });
return;
}
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
async function executeStep4(state) {
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const stepStartedAt = Date.now();
const verificationFilterAfterTimestamp = mail.provider === '2925'
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: stepStartedAt;
const verificationSessionKey = `4:${stepStartedAt}`;
const signupTabId = await getTabId('signup-page');
if (!signupTabId) {
throw new Error('认证页面标签页已关闭,无法继续步骤 4。');
throw new Error('认证页面标签页已关闭,无法继续步骤 4。请先执行步骤 1 或步骤 2,重新打开认证页后再试。');
}
await chrome.tabs.update(signupTabId, { active: true });
throwIfStopped();
await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...');
const prepareResult = await sendToContentScriptResilient(
'signup-page',
{
@@ -66,36 +112,51 @@
return;
}
throwIfStopped();
if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
} else {
await addLog(`步骤 4:正在打开${mail.label}...`);
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
} else {
const tabId = await getTabId(mail.source);
await chrome.tabs.update(tabId, { active: true });
}
} else {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
await addLog('步骤 4:正在确认 iCloud 邮箱登录态...', 'info');
await ensureIcloudMailSession({
state,
step: 4,
actionLabel: '步骤 4:确认 iCloud 邮箱登录态',
});
}
throwIfStopped();
if (
mail.provider === HOTMAIL_PROVIDER
|| mail.provider === LUCKMAIL_PROVIDER
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
) {
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
} else if (mail.provider === '2925') {
await addLog(`步骤 4:正在打开${mail.label}...`);
if (typeof ensureMail2925MailboxSession === 'function') {
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
} else {
await focusOrOpenMailTab(mail);
}
await addLog(`步骤 4:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info');
} else {
await addLog(`步骤 4:正在打开${mail.label}...`);
await focusOrOpenMailTab(mail);
}
const shouldRequestFreshCodeFirst = ![
HOTMAIL_PROVIDER,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
].includes(mail.provider);
await resolveVerificationStep(4, state, mail, {
filterAfterTimestamp: mail.provider === '2925' ? 0 : stepStartedAt,
filterAfterTimestamp: verificationFilterAfterTimestamp,
sessionKey: verificationSessionKey,
disableTimeBudgetCap: mail.provider === '2925',
requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
requestFreshCodeFirst: shouldRequestFreshCodeFirst,
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
? 0
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
+21
View File
@@ -26,6 +26,20 @@
throwIfStopped,
} = deps;
function isManagementSecretConfigError(error) {
const message = String(typeof error === 'string' ? error : error?.message || '').trim();
if (!message) {
return false;
}
const mentionsSecret = /管理密钥|Admin Secret|X-Admin-Key|CPA Key/i.test(message);
if (!mentionsSecret) {
return false;
}
return /缺少|未配置|请输入|无效|错误|失败|401|认证失败|未授权|unauthorized|invalid/i.test(message);
}
async function executeStep7(state) {
if (!state.email) {
throw new Error('缺少邮箱地址,请先完成步骤 3。');
@@ -102,6 +116,13 @@
if (isAddPhoneAuthFailure(err)) {
throw err;
}
if (isManagementSecretConfigError(err)) {
await addLog(
`步骤 7:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`,
'error'
);
throw err;
}
lastError = err;
if (attempt >= STEP6_MAX_ATTEMPTS) {
break;
+128
View File
@@ -1,14 +1,142 @@
(function attachBackgroundStep1(root, factory) {
root.MultiPageBackgroundStep1 = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep1Module() {
const STEP1_COOKIE_CLEAR_DOMAINS = [
'chatgpt.com',
'chat.openai.com',
'openai.com',
'auth.openai.com',
'auth0.openai.com',
'accounts.openai.com',
];
const STEP1_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',
];
function normalizeCookieDomainForStep1(domain) {
return String(domain || '').trim().replace(/^\.+/, '').toLowerCase();
}
function shouldClearStep1Cookie(cookie) {
const domain = normalizeCookieDomainForStep1(cookie?.domain);
if (!domain) return false;
return STEP1_COOKIE_CLEAR_DOMAINS.some((target) => (
domain === target || domain.endsWith(`.${target}`)
));
}
function buildStep1CookieRemovalUrl(cookie) {
const host = normalizeCookieDomainForStep1(cookie?.domain);
const rawPath = String(cookie?.path || '/');
const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
return `https://${host}${path}`;
}
function getStep1ErrorMessage(error) {
return error?.message || String(error || '未知错误');
}
async function collectStep1Cookies(chromeApi) {
if (!chromeApi.cookies?.getAll) {
return [];
}
const stores = chromeApi.cookies.getAllCookieStores
? await chromeApi.cookies.getAllCookieStores()
: [{ id: undefined }];
const cookies = [];
const seen = new Set();
for (const store of stores) {
const storeId = store?.id;
const batch = await chromeApi.cookies.getAll(storeId ? { storeId } : {});
for (const cookie of batch || []) {
if (!shouldClearStep1Cookie(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 removeStep1Cookie(chromeApi, cookie) {
const details = {
url: buildStep1CookieRemovalUrl(cookie),
name: cookie.name,
};
if (cookie.storeId) {
details.storeId = cookie.storeId;
}
if (cookie.partitionKey) {
details.partitionKey = cookie.partitionKey;
}
try {
const result = await chromeApi.cookies.remove(details);
return Boolean(result);
} catch (error) {
console.warn('[MultiPage:step1] remove cookie failed', {
domain: cookie?.domain,
name: cookie?.name,
message: getStep1ErrorMessage(error),
});
return false;
}
}
function createStep1Executor(deps = {}) {
const {
addLog,
chrome: chromeApi = globalThis.chrome,
completeStepFromBackground,
openSignupEntryTab,
} = deps;
async function clearOpenAiCookiesBeforeStep1() {
if (!chromeApi?.cookies?.getAll || !chromeApi.cookies?.remove) {
await addLog('步骤 1:当前浏览器不支持 cookies API,跳过打开官网前 cookie 清理。', 'warn');
return;
}
await addLog('步骤 1:打开 ChatGPT 官网前清理 ChatGPT / OpenAI cookies...', 'info');
const cookies = await collectStep1Cookies(chromeApi);
let removedCount = 0;
for (const cookie of cookies) {
if (await removeStep1Cookie(chromeApi, cookie)) {
removedCount += 1;
}
}
if (chromeApi.browsingData?.removeCookies) {
try {
await chromeApi.browsingData.removeCookies({
since: 0,
origins: STEP1_COOKIE_CLEAR_ORIGINS,
});
} catch (error) {
await addLog(`步骤 1browsingData 补扫 cookies 失败:${getStep1ErrorMessage(error)}`, 'warn');
}
}
await addLog(`步骤 1:已清理 ${removedCount} 个 ChatGPT / OpenAI cookies。`, 'ok');
}
async function executeStep1() {
await clearOpenAiCookiesBeforeStep1();
await addLog('步骤 1:正在打开 ChatGPT 官网...');
await openSignupEntryTab(1);
await completeStepFromBackground(1, {});
+125 -1
View File
@@ -12,6 +12,7 @@
getTabId,
isLocalhostOAuthCallbackUrl,
isTabAlive,
normalizeCodex2ApiUrl,
normalizeSub2ApiUrl,
rememberSourceLastUrl,
reuseOrCreateTab,
@@ -21,7 +22,86 @@
SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
} = deps;
function normalizeString(value = '') {
return String(value || '').trim();
}
function parseLocalhostCallback(rawUrl) {
let parsed;
try {
parsed = new URL(rawUrl);
} catch {
throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 9。');
}
const code = normalizeString(parsed.searchParams.get('code'));
const state = normalizeString(parsed.searchParams.get('state'));
if (!code || !state) {
throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 9。');
}
return {
url: parsed.toString(),
code,
state,
};
}
function getCodex2ApiErrorMessage(payload, responseStatus = 500) {
const details = [
payload?.error,
payload?.message,
payload?.detail,
payload?.reason,
]
.map((value) => normalizeString(value))
.find(Boolean);
return details || `Codex2API 请求失败(HTTP ${responseStatus})。`;
}
async function fetchCodex2ApiJson(origin, path, options = {}) {
const controller = new AbortController();
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(`${origin}${path}`, {
method: options.method || 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Admin-Key': normalizeString(options.adminKey),
},
body: options.body === undefined ? undefined : JSON.stringify(options.body),
signal: controller.signal,
});
let payload = {};
try {
payload = await response.json();
} catch {
payload = {};
}
if (!response.ok) {
throw new Error(getCodex2ApiErrorMessage(payload, response.status));
}
return payload;
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error('Codex2API 请求超时,请稍后重试。');
}
throw error;
} finally {
clearTimeout(timer);
}
}
async function executeStep10(state) {
if (getPanelMode(state) === 'codex2api') {
return executeCodex2ApiStep10(state);
}
if (getPanelMode(state) === 'sub2api') {
return executeSub2ApiStep10(state);
}
@@ -79,7 +159,8 @@
source: 'background',
payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword },
}, {
timeoutMs: 30000,
timeoutMs: 125000,
responseTimeoutMs: 125000,
retryDelayMs: 700,
logMessage: '步骤 10:CPA 面板通信未就绪,正在等待页面恢复...',
});
@@ -89,6 +170,48 @@
}
}
async function executeCodex2ApiStep10(state) {
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
}
if (!state.localhostUrl) {
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
}
if (!state.codex2apiSessionId) {
throw new Error('缺少 Codex2API 会话信息,请重新执行步骤 7。');
}
if (!normalizeString(state.codex2apiAdminKey)) {
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
}
const callback = parseLocalhostCallback(state.localhostUrl);
const expectedState = normalizeString(state.codex2apiOAuthState);
if (expectedState && expectedState !== callback.state) {
throw new Error('Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 7。');
}
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
const origin = new URL(codex2apiUrl).origin;
await addLog('步骤 10:正在向 Codex2API 提交回调并创建账号...');
const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/exchange-code', {
adminKey: state.codex2apiAdminKey,
method: 'POST',
body: {
session_id: state.codex2apiSessionId,
code: callback.code,
state: callback.state,
},
});
const verifiedStatus = normalizeString(result?.message) || 'Codex2API OAuth 账号添加成功';
await addLog(`步骤 10${verifiedStatus}`, 'ok');
await completeStepFromBackground(10, {
localhostUrl: callback.url,
verifiedStatus,
});
}
async function executeSub2ApiStep10(state) {
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
@@ -160,6 +283,7 @@
return {
executeCpaStep10,
executeCodex2ApiStep10,
executeStep10,
executeSub2ApiStep10,
};
+164 -8
View File
@@ -7,6 +7,7 @@
chrome,
completeStepFromBackground,
ensureContentScriptReadyOnTab,
ensureSignupAuthEntryPageReady,
ensureSignupEntryPageReady,
ensureSignupPostEmailPageReadyInTab,
getTabId,
@@ -16,6 +17,107 @@
SIGNUP_PAGE_INJECT_FILES,
} = deps;
function getErrorMessage(error) {
return String(typeof error === 'string' ? error : error?.message || '');
}
function isSignupEntryUnavailableErrorMessage(errorLike) {
const message = getErrorMessage(errorLike);
return /未找到可用的邮箱输入入口|当前页面没有可用的注册入口,也不在邮箱\/密码页/.test(message);
}
function isRetryableStep2TransportErrorMessage(errorLike) {
const message = getErrorMessage(errorLike);
return /Content script on signup-page did not respond in \d+s|Receiving end does not exist|message channel closed|A listener indicated an asynchronous response|port closed before a response was received|did not respond in \d+s/i.test(message);
}
function isLikelyLoggedInChatgptHomeUrl(rawUrl) {
const url = String(rawUrl || '').trim();
if (!url) {
return false;
}
try {
const parsed = new URL(url);
const host = String(parsed.hostname || '').toLowerCase();
if (!['chatgpt.com', 'www.chatgpt.com'].includes(host)) {
return false;
}
const path = String(parsed.pathname || '');
if (/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(path)) {
return false;
}
return true;
} catch {
return false;
}
}
async function shouldForceAuthEntryRetry(tabId) {
if (!Number.isInteger(tabId) || typeof chrome?.tabs?.get !== 'function') {
return false;
}
try {
const tab = await chrome.tabs.get(tabId);
const currentUrl = String(tab?.url || '');
return isLikelyLoggedInChatgptHomeUrl(currentUrl);
} catch {
return false;
}
}
async function getTabUrl(tabId) {
if (!Number.isInteger(tabId) || typeof chrome?.tabs?.get !== 'function') {
return '';
}
try {
const tab = await chrome.tabs.get(tabId);
return String(tab?.url || '');
} catch {
return '';
}
}
async function completeStep2AsLoggedInSession(tabId, resolvedEmail, reasonMessage = '') {
const currentUrl = await getTabUrl(tabId);
if (!isLikelyLoggedInChatgptHomeUrl(currentUrl)) {
return false;
}
const reasonText = getErrorMessage(reasonMessage);
const reasonSuffix = reasonText ? `(触发原因:${reasonText}` : '';
await addLog(`步骤 2:检测到当前会话已登录 ChatGPT,已跳过注册链路(步骤 3/4/5),将直接进入步骤 6。${reasonSuffix}`, 'warn');
await completeStepFromBackground(2, {
email: resolvedEmail,
nextSignupState: 'already_logged_in_home',
nextSignupUrl: currentUrl || 'https://chatgpt.com/',
skippedPasswordStep: true,
skipRegistrationFlow: true,
});
return true;
}
async function submitSignupEmail(resolvedEmail, options = {}) {
const {
timeoutMs = 35000,
retryDelayMs = 700,
logMessage = '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...',
} = options;
try {
return await sendToContentScriptResilient('signup-page', {
type: 'EXECUTE_STEP',
step: 2,
source: 'background',
payload: { email: resolvedEmail },
}, {
timeoutMs,
retryDelayMs,
logMessage,
});
} catch (error) {
return { error: getErrorMessage(error) };
}
}
async function executeStep2(state) {
const resolvedEmail = await resolveSignupEmailForFlow(state);
@@ -34,19 +136,73 @@
});
}
const step2Result = await sendToContentScriptResilient('signup-page', {
type: 'EXECUTE_STEP',
step: 2,
source: 'background',
payload: { email: resolvedEmail },
}, {
timeoutMs: 20000,
if (await shouldForceAuthEntryRetry(signupTabId)) {
await addLog('步骤 2:检测到当前位于已登录 ChatGPT 首页,先切换认证入口页再提交邮箱。', 'warn');
try {
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
} catch (entryError) {
const entryErrorMessage = getErrorMessage(entryError);
if (await completeStep2AsLoggedInSession(signupTabId, resolvedEmail, entryErrorMessage)) {
return;
}
await addLog('步骤 2:切换认证入口失败,正在重新打开官网入口并重试提交邮箱...', 'warn');
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
}
}
let step2Result = await submitSignupEmail(resolvedEmail, {
timeoutMs: 35000,
retryDelayMs: 700,
logMessage: '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...',
});
if (step2Result?.error) {
throw new Error(step2Result.error);
const errorMessage = getErrorMessage(step2Result.error);
if (isSignupEntryUnavailableErrorMessage(errorMessage)) {
await addLog('步骤 2:未找到邮箱输入入口,正在切换认证入口页后重试一次...', 'warn');
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
step2Result = await submitSignupEmail(resolvedEmail, {
timeoutMs: 35000,
retryDelayMs: 700,
logMessage: '步骤 2:认证入口页已打开,正在重新提交邮箱...',
});
if (step2Result?.error) {
const retryErrorMessage = getErrorMessage(step2Result.error);
if (isSignupEntryUnavailableErrorMessage(retryErrorMessage)) {
if (await completeStep2AsLoggedInSession(signupTabId, resolvedEmail, retryErrorMessage)) {
return;
}
await addLog('步骤 2:认证入口仍不可用,正在重新进入官网注册入口再重试一次...', 'warn');
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
step2Result = await submitSignupEmail(resolvedEmail, {
timeoutMs: 35000,
retryDelayMs: 700,
logMessage: '步骤 2:重试官网注册入口后正在重新提交邮箱...',
});
}
}
} else if (isRetryableStep2TransportErrorMessage(errorMessage)) {
await addLog('步骤 2:注册入口页通信超时,正在切换认证入口页并重试提交邮箱...', 'warn');
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
step2Result = await submitSignupEmail(resolvedEmail, {
timeoutMs: 45000,
retryDelayMs: 700,
logMessage: '步骤 2:认证入口页已打开,正在重新提交邮箱...',
});
}
}
if (step2Result?.error) {
const finalErrorMessage = getErrorMessage(step2Result.error);
if (
(isSignupEntryUnavailableErrorMessage(finalErrorMessage)
|| isRetryableStep2TransportErrorMessage(finalErrorMessage))
&& await completeStep2AsLoggedInSession(signupTabId, resolvedEmail, finalErrorMessage)
) {
return;
}
throw new Error(finalErrorMessage);
}
if (!step2Result?.alreadyOnPasswordPage) {
+36 -6
View File
@@ -376,6 +376,17 @@
return 30000;
}
function resolveResponseTimeoutMs(message, requestedResponseTimeoutMs, remainingTimeoutMs = null) {
const fallbackTimeoutMs = getContentScriptResponseTimeoutMs(message);
const requestedTimeoutMs = Number.isFinite(Number(requestedResponseTimeoutMs))
? Math.max(1, Math.floor(Number(requestedResponseTimeoutMs)))
: fallbackTimeoutMs;
if (!Number.isFinite(Number(remainingTimeoutMs))) {
return requestedTimeoutMs;
}
return Math.max(1, Math.min(requestedTimeoutMs, Math.floor(Number(remainingTimeoutMs))));
}
function getMessageDebugLabel(source, message, tabId = null) {
const parts = [source || 'unknown', message?.type || 'UNKNOWN'];
if (Number.isInteger(message?.step)) parts.push(`step=${message.step}`);
@@ -439,7 +450,13 @@
pendingCommands.delete(source);
reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`));
}, timeout);
pendingCommands.set(source, { message, resolve, reject, timer });
pendingCommands.set(source, {
message,
resolve,
reject,
timer,
responseTimeoutMs: timeout,
});
console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`);
});
}
@@ -449,7 +466,7 @@
if (pending) {
clearTimeout(pending.timer);
pendingCommands.delete(source);
sendTabMessageWithTimeout(tabId, source, pending.message).then(pending.resolve).catch(pending.reject);
sendTabMessageWithTimeout(tabId, source, pending.message, pending.responseTimeoutMs).then(pending.resolve).catch(pending.reject);
console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
}
}
@@ -564,13 +581,13 @@
if (!entry || !entry.ready) {
throwIfStopped();
return queueCommand(source, message);
return queueCommand(source, message, responseTimeoutMs);
}
const alive = await isTabAlive(source);
throwIfStopped();
if (!alive) {
return queueCommand(source, message);
return queueCommand(source, message, responseTimeoutMs);
}
throwIfStopped();
@@ -592,12 +609,18 @@
while (Date.now() - start < timeoutMs) {
throwIfStopped();
attempt += 1;
const remainingTimeoutMs = Math.max(1, timeoutMs - (Date.now() - start));
const effectiveResponseTimeoutMs = resolveResponseTimeoutMs(
message,
responseTimeoutMs,
remainingTimeoutMs
);
try {
return await sendToContentScript(
source,
message,
responseTimeoutMs !== undefined ? { responseTimeoutMs } : {}
{ responseTimeoutMs: effectiveResponseTimeoutMs }
);
} catch (err) {
const retryable = isRetryableContentScriptTransportError(err);
@@ -631,12 +654,18 @@
while (Date.now() - start < timeoutMs) {
throwIfStopped();
const remainingTimeoutMs = Math.max(1, timeoutMs - (Date.now() - start));
const effectiveResponseTimeoutMs = resolveResponseTimeoutMs(
message,
responseTimeoutMs,
remainingTimeoutMs
);
try {
return await sendToContentScript(
mail.source,
message,
responseTimeoutMs !== undefined ? { responseTimeoutMs } : {}
{ responseTimeoutMs: effectiveResponseTimeoutMs }
);
} catch (err) {
if (!isRetryableContentScriptTransportError(err)) {
@@ -684,6 +713,7 @@
queueCommand,
registerTab,
rememberSourceLastUrl,
resolveResponseTimeoutMs,
reuseOrCreateTab,
sendTabMessageWithTimeout,
sendToContentScript,
+180 -18
View File
@@ -10,9 +10,11 @@
confirmCustomVerificationStepBypassRequest,
getHotmailVerificationPollConfig,
getHotmailVerificationRequestTimestamp,
handleMail2925LimitReachedError,
getState,
getTabId,
HOTMAIL_PROVIDER,
isMail2925LimitReachedError,
isStopError,
LUCKMAIL_PROVIDER,
MAIL_2925_VERIFICATION_INTERVAL_MS,
@@ -21,6 +23,7 @@
pollHotmailVerificationCode,
pollLuckmailVerificationCode,
sendToContentScript,
sendToContentScriptResilient,
sendToMailContentScriptResilient,
setState,
sleepWithStop,
@@ -28,6 +31,12 @@
VERIFICATION_POLL_MAX_ROUNDS,
} = deps;
const isRetryableVerificationTransportError = typeof deps.isRetryableContentScriptTransportError === 'function'
? deps.isRetryableContentScriptTransportError
: ((error) => /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(
String(typeof error === 'string' ? error : error?.message || '')
));
function getVerificationCodeStateKey(step) {
return step === 4 ? 'lastSignupCode' : 'lastLoginCode';
}
@@ -36,6 +45,89 @@
return step === 4 ? '注册' : '登录';
}
function isLikelyLoggedInChatgptHomeUrl(rawUrl) {
const url = String(rawUrl || '').trim();
if (!url) return false;
try {
const parsed = new URL(url);
const host = String(parsed.hostname || '').toLowerCase();
if (!['chatgpt.com', 'www.chatgpt.com'].includes(host)) {
return false;
}
const path = String(parsed.pathname || '');
if (/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(path)) {
return false;
}
return true;
} catch {
return false;
}
}
function isSignupProfilePageUrl(rawUrl) {
const url = String(rawUrl || '').trim();
if (!url) return false;
try {
const parsed = new URL(url);
const host = String(parsed.hostname || '').toLowerCase();
if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) {
return false;
}
return /\/create-account\/profile(?:[/?#]|$)/i.test(String(parsed.pathname || ''));
} catch {
return false;
}
}
async function detectStep4PostSubmitFallback(tabId, options = {}) {
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 8000);
const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs) || 250);
const startedAt = Date.now();
let lastUrl = '';
while (Date.now() - startedAt < timeoutMs) {
throwIfStopped();
try {
const tab = await chrome.tabs.get(tabId);
const currentUrl = String(tab?.url || '').trim();
if (currentUrl) {
lastUrl = currentUrl;
}
if (isLikelyLoggedInChatgptHomeUrl(currentUrl)) {
return {
success: true,
reason: 'chatgpt_home',
skipProfileStep: true,
url: currentUrl,
};
}
if (isSignupProfilePageUrl(currentUrl)) {
return {
success: true,
reason: 'signup_profile',
skipProfileStep: false,
url: currentUrl,
};
}
} catch {
// Keep polling until timeout; tab may be mid-navigation.
}
await sleepWithStop(pollIntervalMs);
}
return {
success: false,
reason: 'unknown',
skipProfileStep: false,
url: lastUrl,
};
}
function getVerificationResendStateKey() {
return 'verificationResendCount';
}
@@ -96,6 +188,9 @@
if (response?.error) {
throw new Error(response.error);
}
if (step === 8 && response?.addPhoneDetected) {
throw new Error('步骤 8:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。 URL: https://auth.openai.com/add-phone');
}
if (!response?.confirmed) {
throw new Error(`步骤 ${step}:已取消手动${verificationLabel}验证码确认。`);
}
@@ -111,12 +206,15 @@
function getVerificationPollPayload(step, state, overrides = {}) {
const is2925Provider = state?.mailProvider === '2925';
const mail2925MatchTargetEmail = is2925Provider
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive';
if (step === 4) {
return {
filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(4, state),
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
targetEmail: state.email,
mail2925MatchTargetEmail,
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
...overrides,
@@ -128,6 +226,7 @@
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
targetEmail: String(state?.step8VerificationTargetEmail || '').trim() || state.email,
mail2925MatchTargetEmail,
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
...overrides,
@@ -237,12 +336,16 @@
return requestedAt;
}
function shouldPreclear2925Mailbox(step, mail) {
return mail?.provider === '2925' && (step === 4 || step === 8);
function shouldPreclear2925Mailbox(step, mail, options = {}) {
if (mail?.provider !== '2925' || (step !== 4 && step !== 8)) {
return false;
}
return !(Number(options.filterAfterTimestamp) > 0);
}
async function clear2925MailboxBeforePolling(step, mail, options = {}) {
if (!shouldPreclear2925Mailbox(step, mail)) {
if (!shouldPreclear2925Mailbox(step, mail, options)) {
return;
}
@@ -417,6 +520,12 @@
if (isStopError(err)) {
throw err;
}
if (mail?.provider === '2925' && typeof isMail2925LimitReachedError === 'function' && isMail2925LimitReachedError(err)) {
if (typeof handleMail2925LimitReachedError === 'function') {
throw await handleMail2925LimitReachedError(step, err);
}
throw err;
}
lastError = err;
await addLog(`步骤 ${step}${err.message}`, 'warn');
}
@@ -429,6 +538,17 @@
`步骤 ${step}:距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,继续刷新邮箱(第 ${round}/${maxRounds} 轮)...`,
'info'
);
const configuredIntervalMs = Math.max(
1,
Number(payloadOverrides.intervalMs)
|| Number(pollOverrides.intervalMs)
|| 3000
);
const cooldownSleepMs = Math.min(
remainingBeforeResendMs,
Math.max(1000, Math.min(configuredIntervalMs, 3000))
);
await sleepWithStop(cooldownSleepMs);
continue;
}
@@ -554,6 +674,12 @@
if (isStopError(err)) {
throw err;
}
if (mail?.provider === '2925' && typeof isMail2925LimitReachedError === 'function' && isMail2925LimitReachedError(err)) {
if (typeof handleMail2925LimitReachedError === 'function') {
throw await handleMail2925LimitReachedError(step, err);
}
throw err;
}
lastError = err;
await addLog(`步骤 ${step}${err.message}`, 'warn');
if (round < maxRounds) {
@@ -572,19 +698,54 @@
}
await chrome.tabs.update(signupTabId, { active: true });
const result = await sendToContentScript('signup-page', {
const baseResponseTimeoutMs = await getResponseTimeoutMsForStep(
step,
options,
step === 7 ? 45000 : 30000,
`填写${getVerificationCodeLabel(step)}验证码`
);
const message = {
type: 'FILL_CODE',
step,
source: 'background',
payload: { code },
}, {
responseTimeoutMs: await getResponseTimeoutMsForStep(
step,
options,
step === 7 ? 45000 : 30000,
`填写${getVerificationCodeLabel(step)}验证码`
),
});
};
let result;
if (typeof sendToContentScriptResilient === 'function') {
try {
result = await sendToContentScriptResilient('signup-page', message, {
timeoutMs: Math.max(baseResponseTimeoutMs + 15000, 30000),
retryDelayMs: 700,
responseTimeoutMs: baseResponseTimeoutMs,
logMessage: `步骤 ${step}:认证页正在切换,等待页面重新就绪后继续确认验证码提交结果...`,
});
} catch (err) {
if (step === 4 && isRetryableVerificationTransportError(err)) {
const fallback = await detectStep4PostSubmitFallback(signupTabId, {
timeoutMs: 9000,
pollIntervalMs: 300,
});
if (fallback.success) {
const fallbackLabel = fallback.reason === 'chatgpt_home'
? 'ChatGPT 已登录首页'
: '注册资料页';
await addLog(`步骤 4:验证码提交后页面已切换到${fallbackLabel},按提交成功继续。`, 'warn');
return {
success: true,
assumed: true,
transportRecovered: true,
skipProfileStep: Boolean(fallback.skipProfileStep),
url: fallback.url,
};
}
}
throw err;
}
} else {
result = await sendToContentScript('signup-page', message, {
responseTimeoutMs: baseResponseTimeoutMs,
});
}
if (result && result.error) {
throw new Error(result.error);
@@ -710,6 +871,7 @@
`步骤 ${step}:提交失败后距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,先继续刷新邮箱(${attempt + 1}/${maxSubmitAttempts}...`,
'warn'
);
await sleepWithStop(Math.min(remainingBeforeResendMs, 2000));
continue;
}
@@ -725,11 +887,6 @@
continue;
}
if (submitResult.addPhonePage) {
const urlPart = submitResult.url ? ` URL: ${submitResult.url}` : '';
throw new Error(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim());
}
await setState({
lastEmailTimestamp: result.emailTimestamp,
[stateKey]: result.code,
@@ -738,9 +895,14 @@
await completeStepFromBackground(step, {
emailTimestamp: result.emailTimestamp,
code: result.code,
phoneVerificationRequired: Boolean(submitResult.addPhonePage),
...(step === 4 && submitResult?.skipProfileStep ? { skipProfileStep: true } : {}),
});
triggerPostSuccessMailboxCleanup(step, mail);
return;
return {
phoneVerificationRequired: Boolean(submitResult.addPhonePage),
url: submitResult.url || '',
};
}
}