fix: preserve oauth step metadata and resolve dev merge conflicts

This commit is contained in:
cnxwzy
2026-04-28 07:54:29 +08:00
20 changed files with 8366 additions and 54 deletions
File diff suppressed because it is too large Load Diff
+110
View File
@@ -0,0 +1,110 @@
// background/ip-proxy-provider-711proxy.js — 711Proxy 参数与账号规则
(function register711ProxyProvider(root) {
function normalizeCountryCode(value = '') {
const raw = String(value || '').trim().toUpperCase().replace(/[^A-Z]/g, '');
return /^[A-Z]{2}$/.test(raw) ? raw : '';
}
function normalize711SessionId(value = '') {
return String(value || '').trim().replace(/[^A-Za-z0-9_-]/g, '').slice(0, 64);
}
function normalize711SessionMinutes(value = '') {
const raw = String(value ?? '').trim();
if (!raw) return '';
const numeric = Number.parseInt(raw, 10);
if (!Number.isInteger(numeric)) return '';
return String(Math.max(1, Math.min(180, numeric)));
}
function apply711SessionToUsername(username = '', options = {}) {
const text = String(username || '').trim();
if (!text) {
return text;
}
const sessionId = normalize711SessionId(options?.sessionId || '');
const sessTime = normalize711SessionMinutes(options?.sessTime || '');
let next = text;
if (sessionId) {
if (/(?:^|[-_])session[-_:][A-Za-z0-9_-]+?(?=(?:[-_](?:sessTime|sessAuto|region|life|zone|ptype|country|area)\b)|$)/i.test(next)) {
next = next.replace(
/((?:^|[-_])session[-_:])([A-Za-z0-9_-]+?)(?=(?:[-_](?:sessTime|sessAuto|region|life|zone|ptype|country|area)\b)|$)/i,
`$1${sessionId}`
);
} else {
next = `${next}-session-${sessionId}`;
}
}
if (sessTime) {
if (/(?:^|[-_])sessTime[-_:]?\d+\b/i.test(next)) {
next = next.replace(/((?:^|[-_])sessTime[-_:]?)(\d+)\b/i, `$1${sessTime}`);
} else {
next = `${next}-sessTime-${sessTime}`;
}
}
return next;
}
function apply711RegionToUsername(username = '', regionCode = '') {
const text = String(username || '').trim();
const normalizedRegion = normalizeCountryCode(regionCode);
if (!text || !normalizedRegion) {
return text;
}
if (/(?:^|[-_])region[-_:]?[A-Za-z]{2}\b/i.test(text)) {
return text.replace(/((?:^|[-_])region[-_:]?)([A-Za-z]{2})\b/i, `$1${normalizedRegion}`);
}
return `${text}-region-${normalizedRegion}`;
}
function transform711ProxyAccountEntry(entry = {}, context = {}) {
const state = context?.state || {};
const hasAccountList = Boolean(context?.hasAccountList);
const nextEntry = { ...entry };
const username = String(nextEntry.username || '').trim();
if (!username) {
return nextEntry;
}
const configuredRegion = normalizeCountryCode(state?.ipProxyRegion || '');
if (!hasAccountList && configuredRegion) {
nextEntry.username = apply711RegionToUsername(username, configuredRegion);
if (!String(nextEntry.region || '').trim()) {
nextEntry.region = configuredRegion;
}
}
// 账号列表模式按每行原样生效,不叠加固定账号区的 session/sessTime。
if (hasAccountList) {
return nextEntry;
}
const sessionId = normalize711SessionId(state?.ipProxyAccountSessionPrefix || '');
const sessTime = normalize711SessionMinutes(state?.ipProxyAccountLifeMinutes || '');
if (!sessionId && !sessTime) {
return nextEntry;
}
nextEntry.username = apply711SessionToUsername(nextEntry.username, {
sessionId,
sessTime,
});
return nextEntry;
}
root.transformIpProxyAccountEntryByProvider = function transformIpProxyAccountEntryByProvider(
provider = '',
entry = {},
context = {}
) {
const normalizedProvider = String(provider || '').trim().toLowerCase();
if (normalizedProvider === '711proxy') {
return transform711ProxyAccountEntry(entry, context);
}
return entry;
};
})(typeof self !== 'undefined' ? self : globalThis);
+131 -2
View File
@@ -10,6 +10,7 @@
buildLuckmailSessionSettingsPayload,
buildPersistentSettingsPayload,
broadcastDataUpdate,
applyIpProxySettingsFromState,
cancelScheduledAutoRun,
checkIcloudSession,
clearAccountRunHistory,
@@ -58,6 +59,7 @@
launchAutoRunTimerPlan,
listIcloudAliases,
listLuckmailPurchasesForManagement,
refreshIpProxyPool,
normalizeHotmailAccounts,
normalizeMail2925Accounts,
normalizeRunCount,
@@ -69,11 +71,14 @@
pollContributionStatus,
registerTab,
requestStop,
probeIpProxyExit,
handleCloudflareSecurityBlocked,
resetState,
resumeAutoRun,
scheduleAutoRun,
selectLuckmailPurchase,
switchIpProxy,
changeIpProxyExit,
setCurrentMail2925Account,
setCurrentHotmailAccount,
setContributionMode,
@@ -609,14 +614,138 @@
}
case 'SAVE_SETTING': {
const currentState = await getState();
const updates = buildPersistentSettingsPayload(message.payload || {});
const sessionUpdates = buildLuckmailSessionSettingsPayload(message.payload || {});
const modeChanged = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
&& Boolean(currentState?.plusModeEnabled) !== Boolean(updates.plusModeEnabled);
await setPersistentSettings(updates);
await setState({
const stateUpdates = {
...updates,
...sessionUpdates,
};
if (modeChanged && typeof getStepIdsForState === 'function') {
const nextStateForSteps = { ...currentState, ...stateUpdates };
stateUpdates.stepStatuses = Object.fromEntries(
getStepIdsForState(nextStateForSteps).map((stepId) => [stepId, 'pending'])
);
stateUpdates.currentStep = 0;
}
await setState(stateUpdates);
const mergedState = await getState();
const hasIpProxyUpdates = Object.keys(updates).some((key) => key.startsWith('ipProxy'));
const hasIpProxyEnabledUpdate = Object.prototype.hasOwnProperty.call(updates, 'ipProxyEnabled');
const previousIpProxyEnabled = Boolean(currentState?.ipProxyEnabled);
const nextIpProxyEnabled = hasIpProxyEnabledUpdate
? Boolean(updates.ipProxyEnabled)
: previousIpProxyEnabled;
// 仅在“手动开关代理”时自动应用。
// 其他字段改动(host/账号/地区/session 等)需由“同步/下一条/检测出口/Change”显式触发。
const shouldApplyIpProxyOnSave = hasIpProxyUpdates
&& hasIpProxyEnabledUpdate
&& previousIpProxyEnabled !== nextIpProxyEnabled;
let proxyRouting = null;
if (shouldApplyIpProxyOnSave && typeof applyIpProxySettingsFromState === 'function') {
const isEnablingProxy = !previousIpProxyEnabled && nextIpProxyEnabled;
proxyRouting = await applyIpProxySettingsFromState(mergedState, {
// 手动开启时自动应用一次代理,不做出口探测;
// 出口探测由“同步/检测出口”按钮显式触发,避免开启即误判为失败。
skipExitProbe: true,
resetNetworkState: false,
forceAuthRebind: false,
suppressAuthRebind: !isEnablingProxy,
}).catch((error) => ({
applied: false,
reason: 'apply_failed',
error: error?.message || String(error || '代理应用失败'),
}));
}
if (Boolean(currentState?.contributionMode) && typeof setContributionMode === 'function') {
await setContributionMode(true);
}
if (modeChanged) {
await addLog(
Boolean(updates.plusModeEnabled)
? 'Plus 模式已开启,已切换为 Plus Checkout + PayPal 步骤。'
: 'Plus 模式已关闭,已恢复普通注册授权步骤。',
'info'
);
}
return { ok: true, state: await getState(), proxyRouting };
}
case 'REFRESH_IP_PROXY_POOL': {
if (typeof refreshIpProxyPool !== 'function') {
throw new Error('IP 代理池能力尚未接入。');
}
const result = await refreshIpProxyPool({
maxItems: message.payload?.maxItems,
mode: message.payload?.mode,
});
return { ok: true, state: await getState() };
return { ok: true, ...result };
}
case 'SWITCH_IP_PROXY': {
if (typeof switchIpProxy !== 'function') {
throw new Error('IP 代理切换能力尚未接入。');
}
const result = await switchIpProxy(message.payload?.direction || 'next', {
maxItems: message.payload?.maxItems,
mode: message.payload?.mode,
forceRefresh: message.payload?.forceRefresh,
});
return { ok: true, ...result };
}
case 'CHANGE_IP_PROXY_EXIT': {
if (typeof changeIpProxyExit !== 'function') {
throw new Error('IP 代理 Change 能力尚未接入。');
}
const result = await changeIpProxyExit({
mode: message.payload?.mode,
});
return { ok: true, ...result };
}
case 'PROBE_IP_PROXY_EXIT': {
if (typeof probeIpProxyExit !== 'function') {
throw new Error('IP 代理出口检测能力尚未接入。');
}
const probeState = await getState();
const mode = typeof normalizeIpProxyMode === 'function'
? normalizeIpProxyMode(probeState?.ipProxyMode)
: String(probeState?.ipProxyMode || 'account').trim().toLowerCase();
const provider = typeof normalizeIpProxyProviderValue === 'function'
? normalizeIpProxyProviderValue(probeState?.ipProxyService)
: String(probeState?.ipProxyService || '').trim().toLowerCase();
const is711AccountMode = mode === 'account' && provider === '711proxy';
const previousReason = String(probeState?.ipProxyAppliedReason || '').trim().toLowerCase();
const previousExitError = String(probeState?.ipProxyAppliedExitError || '').trim();
const hadMissingAuthChallenge = /challenge=0|provided=0|未触发代理鉴权挑战|未收到 407/i.test(previousExitError);
const shouldPreRebindBeforeProbe = Boolean(
probeState?.ipProxyEnabled
&& is711AccountMode
&& (hadMissingAuthChallenge || previousReason === 'connectivity_failed')
);
const timeoutMs = Number(message.payload?.timeoutMs) > 0
? Number(message.payload.timeoutMs)
: (is711AccountMode ? (shouldPreRebindBeforeProbe ? 8000 : 6000) : undefined);
// 手动“检测出口”前先轻量应用当前配置,避免读取到旧代理链路状态。
if (probeState?.ipProxyEnabled && typeof applyIpProxySettingsFromState === 'function') {
await applyIpProxySettingsFromState(probeState, {
skipExitProbe: true,
resetNetworkState: shouldPreRebindBeforeProbe,
forceAuthRebind: shouldPreRebindBeforeProbe,
suppressAuthRebind: !shouldPreRebindBeforeProbe,
}).catch(() => null);
}
const result = await probeIpProxyExit({
timeoutMs,
authRebindMaxAttempts: is711AccountMode ? 1 : undefined,
});
return { ok: true, ...result };
}
case 'EXPORT_SETTINGS': {
+100 -8
View File
@@ -8,6 +8,7 @@
addLog,
chrome,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
completeStepFromBackground,
confirmCustomVerificationStepBypass,
ensureMail2925MailboxSession,
ensureIcloudMailSession,
@@ -68,6 +69,68 @@
return String(value || '').trim().toLowerCase();
}
async function completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, options = {}) {
await setState({
step8VerificationTargetEmail: '',
loginVerificationRequestedAt: null,
});
const fromRecovery = Boolean(options.fromRecovery);
await addLog(
`步骤 ${visibleStep}:当前认证页已进入 OAuth 授权页${fromRecovery ? '(轮询失败后复核)' : ''},跳过登录验证码拉取并继续后续流程。`,
'warn'
);
if (typeof completeStepFromBackground === 'function') {
await completeStepFromBackground(visibleStep, {
loginVerificationRequestedAt: null,
skipLoginVerificationStep: true,
directOAuthConsentPage: true,
});
}
}
function isStep8AddPhoneStateError(error) {
const message = String(error?.message || error || '');
return /add-phone|手机号页面|手机号验证页|phone[\s-_]verification|phone\s+number/i.test(message);
}
async function recoverStep8PollingFailure(currentState, visibleStep) {
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
try {
const pageState = await ensureStep8VerificationPageReady({
visibleStep,
authLoginStep,
timeoutMs: await getStep8ReadyTimeoutMs(
'登录验证码轮询异常后复核认证页状态',
currentState?.oauthUrl || '',
visibleStep
),
});
if (pageState?.state === 'oauth_consent_page') {
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { fromRecovery: true });
return { outcome: 'completed' };
}
if (pageState?.state === 'verification_page') {
await addLog(
`步骤 ${visibleStep}:检测到邮箱轮询/页面通信异常,但认证页仍在验证码页,先在当前链路重试,不回到步骤 ${authLoginStep}`,
'warn'
);
return { outcome: 'retry_without_step7' };
}
} catch (inspectError) {
if (isStep8RestartStep7Error(inspectError)) {
return { outcome: 'restart_step7', error: inspectError };
}
if (isStep8AddPhoneStateError(inspectError)) {
throw inspectError;
}
await addLog(
`步骤 ${visibleStep}:轮询失败后复核认证页状态异常:${inspectError?.message || inspectError},将回到步骤 ${authLoginStep} 重试。`,
'warn'
);
}
return { outcome: 'restart_step7' };
}
function getExpectedMail2925MailboxEmail(state = {}) {
if (Boolean(state?.mail2925UseAccountPool)) {
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
@@ -131,6 +194,10 @@
authLoginStep: getAuthLoginStepForVisibleStep(visibleStep),
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep),
});
if (pageState?.state === 'oauth_consent_page') {
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep);
return;
}
const shouldCompareVerificationEmail = mail.provider !== '2925';
const displayedVerificationEmail = shouldCompareVerificationEmail
? normalizeStep8VerificationTargetEmail(pageState?.displayedEmail)
@@ -201,9 +268,11 @@
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || '', visibleStep),
requestFreshCodeFirst: false,
targetEmail: fixedTargetEmail,
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
? 0
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
? 15000
: ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
? 0
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS),
});
}
@@ -224,24 +293,47 @@
} catch (err) {
const visibleStep = getVisibleStep(currentState, 8);
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
if (!isVerificationMailPollingError(err) && !isStep8RestartStep7Error(err)) {
throw err;
let currentError = err;
let retryWithoutStep7 = false;
const isMailPollingError = isVerificationMailPollingError(err);
if (isMailPollingError && !isStep8RestartStep7Error(err)) {
const recovery = await recoverStep8PollingFailure(currentState, visibleStep);
if (recovery?.outcome === 'completed') {
return;
}
if (recovery?.outcome === 'retry_without_step7') {
retryWithoutStep7 = true;
}
if (recovery?.error) {
currentError = recovery.error;
}
}
if (!isVerificationMailPollingError(currentError) && !isStep8RestartStep7Error(currentError)) {
throw currentError;
}
lastMailPollingError = err;
lastMailPollingError = currentError;
if (mailPollingAttempt >= STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS) {
break;
}
mailPollingAttempt += 1;
if (retryWithoutStep7) {
await addLog(
`步骤 ${visibleStep}:认证页仍保持在验证码页,将在当前链路直接重试(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}),不回到步骤 ${authLoginStep}`,
'warn'
);
currentState = await getState();
continue;
}
await addLog(
isStep8RestartStep7Error(err)
isStep8RestartStep7Error(currentError)
? `步骤 ${visibleStep}:检测到认证页进入重试/超时报错状态,准备从步骤 ${authLoginStep} 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}...`
: `步骤 ${visibleStep}:检测到邮箱轮询类失败,准备从步骤 ${authLoginStep} 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}...`,
'warn'
);
await rerunStep7ForStep8Recovery({
logMessage: isStep8RestartStep7Error(err)
logMessage: isStep8RestartStep7Error(currentError)
? `步骤 ${visibleStep}:认证页进入重试/超时报错状态,正在回到步骤 ${authLoginStep} 重新发起登录流程...`
: `步骤 ${visibleStep}:正在回到步骤 ${authLoginStep},重新发起登录验证码流程...`,
});
+6 -3
View File
@@ -149,6 +149,7 @@
const shouldRequestFreshCodeFirst = ![
HOTMAIL_PROVIDER,
LUCKMAIL_PROVIDER,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
].includes(mail.provider);
@@ -157,9 +158,11 @@
sessionKey: verificationSessionKey,
disableTimeBudgetCap: mail.provider === '2925',
requestFreshCodeFirst: shouldRequestFreshCodeFirst,
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
? 0
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
? 15000
: ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
? 0
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS),
});
}
+112 -1
View File
@@ -129,6 +129,86 @@
};
}
async function detectStep8PostSubmitFallback(options = {}) {
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 9000);
const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs) || 300);
const step = Number(options.step) || 8;
const startedAt = Date.now();
let lastSnapshot = null;
while (Date.now() - startedAt < timeoutMs) {
throwIfStopped();
try {
const request = {
type: 'GET_LOGIN_AUTH_STATE',
source: 'background',
payload: {},
};
const requestTimeoutMs = Math.max(1200, Math.min(5000, timeoutMs));
const result = typeof sendToContentScriptResilient === 'function'
? await sendToContentScriptResilient(
'signup-page',
request,
{
timeoutMs: requestTimeoutMs,
responseTimeoutMs: requestTimeoutMs,
retryDelayMs: 400,
logMessage: `步骤 ${step}:验证码提交后页面正在切换,等待页面恢复并确认授权状态...`,
}
)
: await sendToContentScript('signup-page', request, {
responseTimeoutMs: requestTimeoutMs,
});
if (result?.error) {
throw new Error(result.error);
}
const authState = String(result?.state || '').trim();
const authUrl = String(result?.url || '').trim();
lastSnapshot = {
state: authState || 'unknown',
url: authUrl,
};
if (authState === 'oauth_consent_page') {
return {
success: true,
reason: 'oauth_consent_page',
addPhonePage: false,
url: authUrl,
};
}
if (authState === 'add_phone_page' || authState === 'phone_verification_page') {
return {
success: true,
reason: 'add_phone_page',
addPhonePage: true,
url: authUrl || 'https://auth.openai.com/add-phone',
};
}
if (authState === 'login_timeout_error_page') {
return {
success: false,
reason: 'login_timeout_error_page',
restartStep7: true,
url: authUrl,
};
}
} catch (_) {
// Ignore transient inspect failures and keep polling.
}
await sleepWithStop(pollIntervalMs);
}
return {
success: false,
reason: 'unknown',
snapshot: lastSnapshot,
};
}
function getVerificationResendStateKey() {
return 'verificationResendCount';
}
@@ -772,6 +852,31 @@
};
}
}
if (step === 8 && isRetryableVerificationTransportError(err)) {
const fallback = await detectStep8PostSubmitFallback({
step,
timeoutMs: 9000,
pollIntervalMs: 300,
});
if (fallback.success) {
if (fallback.addPhonePage) {
await addLog('步骤 8:验证码提交后通信中断,但页面已进入手机号验证页,按提交成功继续。', 'warn');
} else {
await addLog('步骤 8:验证码提交后通信中断,但页面已进入 OAuth 授权页,按提交成功继续。', 'warn');
}
return {
success: true,
assumed: true,
transportRecovered: true,
addPhonePage: Boolean(fallback.addPhonePage),
url: fallback.url || '',
};
}
if (fallback.restartStep7) {
const urlPart = fallback.url ? ` URL: ${fallback.url}` : '';
throw new Error(`STEP8_RESTART_STEP7::步骤 8:验证码提交后认证页进入登录超时报错页,请回到步骤 7 重新开始。${urlPart}`.trim());
}
}
throw err;
}
} else {
@@ -812,7 +917,7 @@
getLegacyVerificationResendCountDefault(step, { requestFreshCodeFirst })
)
: getConfiguredVerificationResendCount(step, state, { requestFreshCodeFirst });
const maxSubmitAttempts = 15;
const maxSubmitAttempts = mail.provider === LUCKMAIL_PROVIDER ? 3 : 15;
const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
let lastResendAt = Number(options.lastResendAt) || 0;
@@ -897,6 +1002,12 @@
throw new Error(`步骤 ${step}:验证码连续失败,已达到 ${maxSubmitAttempts} 次重试上限。`);
}
if (mail.provider === LUCKMAIL_PROVIDER) {
await addLog(`步骤 ${step}:LuckMail 验证码提交失败,等待 15 秒后重新轮询 /code 接口(${attempt + 1}/${maxSubmitAttempts}...`, 'warn');
await sleepWithStop(15000);
continue;
}
const remainingBeforeResendMs = resendIntervalMs > 0 && lastResendAt > 0
? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt))
: 0;