diff --git a/background.js b/background.js index 2173c2b..184d27e 100644 --- a/background.js +++ b/background.js @@ -16,6 +16,7 @@ importScripts( 'background/ip-proxy-provider-711proxy.js', 'background/ip-proxy-core.js', 'background/panel-bridge.js', + 'background/registration-email-state.js', 'background/generated-email-helpers.js', 'background/signup-flow-helpers.js', 'background/message-router.js', @@ -203,6 +204,56 @@ const { const { isRecoverableStep9AuthFailure, } = self.MultiPageActivationUtils; +const registrationEmailStateHelpers = self.MultiPageRegistrationEmailState?.createRegistrationEmailStateHelpers?.() || null; +const DEFAULT_REGISTRATION_EMAIL_STATE = registrationEmailStateHelpers?.DEFAULT_REGISTRATION_EMAIL_STATE || { + current: '', + previous: '', + source: '', + updatedAt: 0, +}; + +function getRegistrationEmailState(state = {}) { + if (registrationEmailStateHelpers?.getRegistrationEmailState) { + return registrationEmailStateHelpers.getRegistrationEmailState(state); + } + const fallbackEmail = String(state?.email || '').trim(); + return { + current: fallbackEmail, + previous: fallbackEmail, + source: '', + updatedAt: 0, + }; +} + +function buildRegistrationEmailStateUpdates(state = {}, options = {}) { + if (registrationEmailStateHelpers?.buildRegistrationEmailStateUpdates) { + return registrationEmailStateHelpers.buildRegistrationEmailStateUpdates(state, options); + } + const currentEmail = String(options?.currentEmail || '').trim(); + const preservePrevious = Boolean(options?.preservePrevious); + const currentState = getRegistrationEmailState(state); + return { + email: currentEmail || null, + registrationEmailState: { + current: currentEmail, + previous: currentEmail || (preservePrevious ? currentState.previous : ''), + source: currentEmail + ? String(options?.source || '').trim() + : (preservePrevious ? currentState.source : ''), + updatedAt: currentEmail || (preservePrevious && currentState.previous) ? Date.now() : 0, + }, + }; +} + +function getRegistrationEmailBaseline(state = {}, options = {}) { + if (registrationEmailStateHelpers?.getRegistrationEmailBaseline) { + return registrationEmailStateHelpers.getRegistrationEmailBaseline(state, options); + } + const preferredEmail = String(options?.preferredEmail || '').trim(); + const fallbackEmail = String(options?.fallbackEmail || '').trim(); + const currentState = getRegistrationEmailState(state); + return preferredEmail || currentState.current || currentState.previous || fallbackEmail || ''; +} const LOG_PREFIX = '[MultiPage:bg]'; const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill'; @@ -782,6 +833,7 @@ const DEFAULT_STATE = { resolvedSignupMethod: null, // 当前自动轮次冻结后的实际注册方式。 accountIdentifierType: null, accountIdentifier: '', + registrationEmailState: { ...DEFAULT_REGISTRATION_EMAIL_STATE }, email: null, // 运行时邮箱,由程序自动获取并写入,不能手动预填。 password: null, // 运行时实际密码,由 customPassword 或程序自动生成后写入。 accounts: [], // 已生成账号记录:{ email, password, createdAt }。 @@ -2861,6 +2913,7 @@ async function importSettingsBundle(configBundle) { ...importedSettings, currentHotmailAccountId: null, email: null, + registrationEmailState: { ...DEFAULT_REGISTRATION_EMAIL_STATE }, }; await setState(sessionUpdates); @@ -2868,6 +2921,7 @@ async function importSettingsBundle(configBundle) { ...importedSettings, currentHotmailAccountId: null, ...(sessionUpdates.email !== undefined ? { email: sessionUpdates.email } : {}), + registrationEmailState: sessionUpdates.registrationEmailState, }); return getState(); @@ -2917,12 +2971,14 @@ function isPhoneActivationForNumber(activation, phoneNumber) { return Boolean(activationDigits && targetDigits && activationDigits === targetDigits); } -async function setEmailStateSilently(email) { - const normalizedEmail = String(email || '').trim() || null; +async function setEmailStateSilently(email, options = {}) { const currentState = await getState(); - const updates = { - email: normalizedEmail, - }; + const updates = buildRegistrationEmailStateUpdates(currentState, { + currentEmail: email, + preservePrevious: Boolean(options?.preservePrevious), + source: options?.source || '', + }); + const normalizedEmail = updates.email; if (normalizedEmail) { updates.accountIdentifierType = 'email'; @@ -2942,8 +2998,8 @@ async function setEmailStateSilently(email) { broadcastDataUpdate(updates); } -async function setEmailState(email) { - await setEmailStateSilently(email); +async function setEmailState(email, options = {}) { + await setEmailStateSilently(email, options); if (email) { const latestState = await getState(); const recordStatus = shouldMarkAccountRunRecordRunning(latestState) ? 'running' : 'step2_stopped'; @@ -9769,6 +9825,7 @@ const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGenerat getCloudflareTempEmailAddressFromResponse, getCloudflareTempEmailConfig, getCustomEmailPoolEmail: getCustomEmailPoolEmailForRun, + getRegistrationEmailBaseline, getState, ensureMail2925AccountForFlow, joinCloudflareTempEmailUrl, @@ -11327,6 +11384,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ resolveSignupMethod, reuseOrCreateTab, sendToContentScriptResilient, + buildRegistrationEmailStateUpdates, setState, shouldUseCustomRegistrationEmail, sleepWithStop, diff --git a/background/cloudmail-provider.js b/background/cloudmail-provider.js index 9c8f01d..89c5f41 100644 --- a/background/cloudmail-provider.js +++ b/background/cloudmail-provider.js @@ -185,7 +185,7 @@ throw err; } } - await setEmailState(address); + await setEmailState(address, { source: 'generated:cloudmail' }); await addLog(`Cloud Mail:已生成 ${address}`, 'ok'); return address; } diff --git a/background/generated-email-helpers.js b/background/generated-email-helpers.js index c57f114..4b5c1f4 100644 --- a/background/generated-email-helpers.js +++ b/background/generated-email-helpers.js @@ -14,6 +14,7 @@ getCloudflareTempEmailAddressFromResponse, getCloudflareTempEmailConfig, getCustomEmailPoolEmail, + getRegistrationEmailBaseline, getState, ensureMail2925AccountForFlow, joinCloudflareTempEmailUrl, @@ -59,7 +60,7 @@ const localPart = String(options.localPart || '').trim().toLowerCase() || generateCloudflareAliasLocalPart(); const aliasEmail = `${localPart}@${domain}`; - await setEmailState(aliasEmail); + await setEmailState(aliasEmail, { source: 'generated:cloudflare' }); await addLog(`Cloudflare 邮箱:已生成 ${aliasEmail}`, 'ok'); return aliasEmail; } @@ -161,7 +162,7 @@ throw new Error('Cloudflare Temp Email 未返回可用邮箱地址。'); } - await setEmailState(address); + await setEmailState(address, { source: 'generated:cloudflare-temp-email' }); await addLog(`Cloudflare Temp Email:已生成 ${address}`, 'ok'); return address; } @@ -174,7 +175,7 @@ throwIfStopped(); const { generateNew = true, - previousEmail = '', + baselineEmail = '', } = options; await addLog(`Duck 邮箱:正在打开自动填充设置(${generateNew ? '生成新地址' : '复用当前地址'})...`); @@ -185,7 +186,7 @@ source: 'background', payload: { generateNew, - previousEmail: normalizeEmailForComparison(previousEmail), + baselineEmail: normalizeEmailForComparison(baselineEmail), }, }); @@ -196,7 +197,7 @@ throw new Error('未返回 Duck 邮箱地址。'); } - await setEmailState(result.email); + await setEmailState(result.email, { source: 'generated:duck' }); await addLog(`Duck 邮箱:${result.generated ? '已生成' : '已读取'} ${result.email}`, 'ok'); return result.email; } @@ -214,7 +215,7 @@ ); } - await setEmailState(email); + await setEmailState(email, { source: 'generated:custom-pool' }); await addLog(`自定义邮箱池:已取用 ${email}`, 'ok'); return email; } @@ -253,7 +254,7 @@ } const email = buildGeneratedAliasEmail(mergedState); - await setEmailState(email); + await setEmailState(email, { source: `generated:${provider || 'alias'}` }); await addLog(`${provider === 'gmail' ? 'Gmail +tag' : '2925'}:已生成 ${email}`, 'ok'); return email; } @@ -311,11 +312,20 @@ if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) { return fetchCloudflareTempEmailAddress(mergedState, options); } + const resolvedDuckBaselineEmail = typeof getRegistrationEmailBaseline === 'function' + ? getRegistrationEmailBaseline(mergedState, { + preferredEmail: options.currentEmail, + fallbackEmail: options.baselineEmail, + }) + : String( + options.currentEmail + || options.baselineEmail + || mergedState.email + || '' + ).trim(); return fetchDuckEmail({ ...options, - previousEmail: options.previousEmail !== undefined - ? options.previousEmail - : mergedState.email, + baselineEmail: resolvedDuckBaselineEmail, }); } diff --git a/background/message-router.js b/background/message-router.js index 01c305c..50adced 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -1320,7 +1320,7 @@ throw new Error('自动流程运行中,当前不能手动修改邮箱。'); } const email = String(message.payload?.email || '').trim() || null; - await setEmailStateSilently(email); + await setEmailStateSilently(email, { source: 'manual' }); return { ok: true, email }; } @@ -1329,7 +1329,7 @@ if (isAutoRunLockedState(state)) { throw new Error('自动流程运行中,当前不能手动修改邮箱。'); } - await setEmailState(message.payload.email); + await setEmailState(message.payload.email, { source: 'manual' }); await resumeAutoRun(); return { ok: true, email: message.payload.email }; } diff --git a/background/registration-email-state.js b/background/registration-email-state.js new file mode 100644 index 0000000..1df80bb --- /dev/null +++ b/background/registration-email-state.js @@ -0,0 +1,87 @@ +(function attachBackgroundRegistrationEmailState(root, factory) { + root.MultiPageRegistrationEmailState = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundRegistrationEmailStateModule() { + const DEFAULT_REGISTRATION_EMAIL_STATE = Object.freeze({ + current: '', + previous: '', + source: '', + updatedAt: 0, + }); + + function createRegistrationEmailStateHelpers() { + function normalizeEmailValue(value) { + return String(value || '').trim(); + } + + function cloneDefaultRegistrationEmailState() { + return { + current: DEFAULT_REGISTRATION_EMAIL_STATE.current, + previous: DEFAULT_REGISTRATION_EMAIL_STATE.previous, + source: DEFAULT_REGISTRATION_EMAIL_STATE.source, + updatedAt: DEFAULT_REGISTRATION_EMAIL_STATE.updatedAt, + }; + } + + function normalizeRegistrationEmailState(value, fallbackEmail = '') { + const fallback = normalizeEmailValue(fallbackEmail); + const candidate = value && typeof value === 'object' && !Array.isArray(value) + ? value + : null; + const current = normalizeEmailValue(candidate?.current || fallback); + const previous = normalizeEmailValue(candidate?.previous || current || fallback); + const source = String(candidate?.source || '').trim(); + const updatedAt = Number(candidate?.updatedAt) || 0; + return { + current, + previous, + source, + updatedAt: updatedAt > 0 ? updatedAt : 0, + }; + } + + function getRegistrationEmailState(state = {}) { + return normalizeRegistrationEmailState(state?.registrationEmailState, state?.email); + } + + function buildRegistrationEmailStateUpdates(state = {}, options = {}) { + const currentState = getRegistrationEmailState(state); + const currentEmail = normalizeEmailValue(options.currentEmail); + const preservePrevious = Boolean(options.preservePrevious); + const source = String(options.source || '').trim(); + const nextState = cloneDefaultRegistrationEmailState(); + + nextState.current = currentEmail; + nextState.previous = currentEmail || (preservePrevious ? currentState.previous : ''); + nextState.source = currentEmail + ? (source || currentState.source) + : (preservePrevious ? currentState.source : ''); + nextState.updatedAt = currentEmail || (preservePrevious && currentState.previous) + ? Date.now() + : 0; + + return { + email: currentEmail || null, + registrationEmailState: nextState, + }; + } + + function getRegistrationEmailBaseline(state = {}, options = {}) { + const currentState = getRegistrationEmailState(state); + const preferredEmail = normalizeEmailValue(options.preferredEmail); + const fallbackEmail = normalizeEmailValue(options.fallbackEmail); + return preferredEmail || currentState.current || currentState.previous || fallbackEmail || ''; + } + + return { + DEFAULT_REGISTRATION_EMAIL_STATE: cloneDefaultRegistrationEmailState(), + buildRegistrationEmailStateUpdates, + getRegistrationEmailBaseline, + getRegistrationEmailState, + normalizeRegistrationEmailState, + }; + } + + return { + createRegistrationEmailStateHelpers, + }; +}); diff --git a/background/signup-flow-helpers.js b/background/signup-flow-helpers.js index 5e1068a..f6cbe06 100644 --- a/background/signup-flow-helpers.js +++ b/background/signup-flow-helpers.js @@ -329,11 +329,12 @@ return; } const preservedPhoneIdentity = getPreservedPhoneIdentityForEmailResolution(state, options); + const generatedEmailAlreadyPersisted = Boolean(options?.generatedEmailAlreadyPersisted); if (preservedPhoneIdentity && typeof setState === 'function') { - await setState({ - email: resolvedEmail, - ...preservedPhoneIdentity, - }); + if (!generatedEmailAlreadyPersisted && resolvedEmail !== state.email) { + await setEmailState(resolvedEmail, { source: 'flow' }); + } + await setState(preservedPhoneIdentity); return; } if (resolvedEmail !== state.email) { @@ -377,7 +378,10 @@ } if (!generatedEmailAlreadyPersisted || options?.preserveAccountIdentity) { - await persistResolvedSignupEmail(resolvedEmail, state, options); + await persistResolvedSignupEmail(resolvedEmail, state, { + ...options, + generatedEmailAlreadyPersisted, + }); } return resolvedEmail; diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index ea7c81d..edfcecd 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -30,6 +30,7 @@ rerunStep7ForStep8Recovery, reuseOrCreateTab, sendToContentScriptResilient, + buildRegistrationEmailStateUpdates = null, phoneVerificationHelpers = null, setState, shouldUseCustomRegistrationEmail, @@ -258,8 +259,15 @@ async function resetStep8AfterEmailInUse(state, visibleStep) { const currentEmail = String(state?.email || '').trim(); + const registrationEmailUpdates = typeof buildRegistrationEmailStateUpdates === 'function' + ? buildRegistrationEmailStateUpdates(state, { + currentEmail: null, + preservePrevious: true, + source: 'step8_recovery', + }) + : { email: null }; await setState({ - email: null, + ...registrationEmailUpdates, step8VerificationTargetEmail: '', loginVerificationRequestedAt: null, }); diff --git a/content/duck-mail.js b/content/duck-mail.js index 8972be4..3a40cda 100644 --- a/content/duck-mail.js +++ b/content/duck-mail.js @@ -23,7 +23,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { async function fetchDuckEmail(payload = {}) { const { generateNew = true, - previousEmail = '', + baselineEmail = '', } = payload; log(`Duck 邮箱:正在${generateNew ? '生成' : '读取'}私有地址...`); @@ -119,21 +119,34 @@ async function fetchDuckEmail(payload = {}) { .map((value) => normalizeDuckEmail(value)) .filter(Boolean) ); + let stableCandidate = ''; + let stableCount = 0; for (let i = 0; i < 100; i++) { const nextValue = readEmail(); if (nextValue && !blockedValues.has(nextValue)) { - return nextValue; + if (nextValue === stableCandidate) { + stableCount += 1; + } else { + stableCandidate = nextValue; + stableCount = 1; + } + if (stableCount >= 2) { + return nextValue; + } + } else { + stableCandidate = ''; + stableCount = 0; } await sleep(150); } throw new Error('等待 Duck 地址变化超时。'); }; - const fallbackPreviousEmail = normalizeDuckEmail(previousEmail); + const knownBaselineEmail = normalizeDuckEmail(baselineEmail); let currentEmail = readEmail(); if (!currentEmail) { - currentEmail = await waitForVisibleEmail(generateNew ? 12 : 20); + currentEmail = await waitForVisibleEmail(generateNew ? (knownBaselineEmail ? 12 : 30) : 20); } if (currentEmail && !generateNew) { @@ -153,9 +166,9 @@ async function fetchDuckEmail(payload = {}) { throw new Error('未找到 Duck 私有地址生成按钮。'); } - const comparisonEmails = [currentEmail, fallbackPreviousEmail].filter(Boolean); - if (!currentEmail && fallbackPreviousEmail) { - log(`Duck 邮箱:当前地址尚未显示,改用上次地址 ${fallbackPreviousEmail} 作为对比基线。`, 'warn'); + const comparisonEmails = [currentEmail, knownBaselineEmail].filter(Boolean); + if (!currentEmail && knownBaselineEmail) { + log(`Duck 邮箱:当前地址尚未显示,改用已知基线 ${knownBaselineEmail} 作为对比基线。`, 'warn'); } for (let attempt = 1; attempt <= 2; attempt++) { diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 8fc6b94..92c9f87 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -10450,6 +10450,7 @@ async function fetchGeneratedEmail(options = {}) { source: 'sidepanel', payload: { generateNew: true, + currentEmail: inputEmail.value.trim(), generator: selectEmailGenerator.value, mailProvider: selectMailProvider.value, mail2925Mode: getSelectedMail2925Mode(), diff --git a/tests/background-generated-email-module.test.js b/tests/background-generated-email-module.test.js index ef7f59d..4f1e8bb 100644 --- a/tests/background-generated-email-module.test.js +++ b/tests/background-generated-email-module.test.js @@ -96,6 +96,7 @@ test('generated email helper forwards the previous email to Duck generation as a }, getCloudflareTempEmailAddressFromResponse: () => '', getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }), + getRegistrationEmailBaseline: (state, options = {}) => options.preferredEmail || options.fallbackEmail || state.email, getState: async () => ({ email: 'Previous@Duck.com', emailGenerator: 'duck', @@ -132,7 +133,82 @@ test('generated email helper forwards the previous email to Duck generation as a assert.equal(requests[0].type, 'FETCH_DUCK_EMAIL'); assert.deepEqual(requests[0].payload, { generateNew: true, - previousEmail: 'previous@duck.com', + baselineEmail: 'previous@duck.com', + }); +}); + +test('generated email helper prefers current UI email over preserved runtime baseline for Duck generation', async () => { + const api = loadGeneratedEmailHelpersApi(); + const requests = []; + + const helpers = api.createGeneratedEmailHelpers({ + addLog: async () => {}, + buildGeneratedAliasEmail: () => { + throw new Error('should not build alias'); + }, + buildCloudflareTempEmailHeaders: () => ({}), + CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email', + DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email', + fetch: async () => ({ ok: true, text: async () => '{}' }), + fetchIcloudHideMyEmail: async () => { + throw new Error('should not use icloud generator'); + }, + getCloudflareTempEmailAddressFromResponse: () => '', + getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }), + getRegistrationEmailBaseline: (state, options = {}) => ( + String(options.preferredEmail || '').trim() + || String(state?.registrationEmailState?.previous || '').trim() + || String(options.fallbackEmail || '').trim() + ), + getState: async () => ({ + email: '', + registrationEmailState: { + current: '', + previous: 'preserved@duck.com', + source: 'generated:duck', + updatedAt: 1, + }, + emailGenerator: 'duck', + mailProvider: 'gmail', + }), + ensureMail2925AccountForFlow: async () => { + throw new Error('should not allocate 2925 account'); + }, + joinCloudflareTempEmailUrl: () => '', + normalizeCloudflareDomain: () => '', + normalizeCloudflareTempEmailAddress: () => '', + normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(), + isGeneratedAliasProvider: () => false, + reuseOrCreateTab: async () => {}, + sendToContentScript: async (_source, message) => { + requests.push(message); + return { email: 'fresh@duck.com', generated: true }; + }, + setEmailState: async () => {}, + throwIfStopped: () => {}, + }); + + const email = await helpers.fetchGeneratedEmail({ + email: '', + registrationEmailState: { + current: '', + previous: 'preserved@duck.com', + source: 'generated:duck', + updatedAt: 1, + }, + emailGenerator: 'duck', + mailProvider: 'gmail', + }, { + generator: 'duck', + generateNew: true, + currentEmail: 'visible@duck.com', + }); + + assert.equal(email, 'fresh@duck.com'); + assert.equal(requests.length, 1); + assert.deepEqual(requests[0].payload, { + generateNew: true, + baselineEmail: 'visible@duck.com', }); }); diff --git a/tests/background-registration-email-state.test.js b/tests/background-registration-email-state.test.js new file mode 100644 index 0000000..84fc6ee --- /dev/null +++ b/tests/background-registration-email-state.test.js @@ -0,0 +1,58 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +function loadRegistrationEmailStateApi() { + const source = fs.readFileSync('background/registration-email-state.js', 'utf8'); + const globalScope = {}; + return new Function('self', `${source}; return self.MultiPageRegistrationEmailState;`)(globalScope); +} + +test('registration email state preserves the previous baseline when current email is cleared for recovery', () => { + const api = loadRegistrationEmailStateApi(); + const helpers = api.createRegistrationEmailStateHelpers(); + + const updates = helpers.buildRegistrationEmailStateUpdates({ + email: 'old.user@example.com', + registrationEmailState: { + current: 'old.user@example.com', + previous: 'old.user@example.com', + source: 'generated:duck', + updatedAt: 1, + }, + }, { + currentEmail: null, + preservePrevious: true, + source: 'step8_recovery', + }); + + assert.deepStrictEqual(updates, { + email: null, + registrationEmailState: { + current: '', + previous: 'old.user@example.com', + source: 'generated:duck', + updatedAt: updates.registrationEmailState.updatedAt, + }, + }); + assert.ok(updates.registrationEmailState.updatedAt > 0); +}); + +test('registration email baseline prefers the current UI email over preserved runtime state', () => { + const api = loadRegistrationEmailStateApi(); + const helpers = api.createRegistrationEmailStateHelpers(); + + const baseline = helpers.getRegistrationEmailBaseline({ + email: '', + registrationEmailState: { + current: '', + previous: 'preserved@duck.com', + source: 'generated:duck', + updatedAt: 1, + }, + }, { + preferredEmail: 'visible@duck.com', + }); + + assert.equal(baseline, 'visible@duck.com'); +}); diff --git a/tests/background-signup-step2-branching.test.js b/tests/background-signup-step2-branching.test.js index 9ee99c2..da22890 100644 --- a/tests/background-signup-step2-branching.test.js +++ b/tests/background-signup-step2-branching.test.js @@ -775,7 +775,6 @@ test('signup flow helper can generate an email on demand when add-email starts f assert.equal(fetchedStates[0].options.preserveAccountIdentity, true); assert.deepStrictEqual(setStateCalls, [ { - email: 'duck.generated@example.com', accountIdentifierType: 'phone', accountIdentifier: '+447780579093', signupPhoneNumber: '+447780579093', diff --git a/tests/background-step7-recovery.test.js b/tests/background-step7-recovery.test.js index 0ce9f21..5af1fde 100644 --- a/tests/background-step7-recovery.test.js +++ b/tests/background-step7-recovery.test.js @@ -352,6 +352,113 @@ test('step 8 submits add-email before polling the email verification code', asyn ]); }); +test('step 8 email_in_use recovery preserves the previous registration baseline', async () => { + const calls = { + contentCalls: 0, + setStates: [], + updatedUrls: [], + }; + + const executor = api.createStep8Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async (_tabId, payload) => { + calls.updatedUrls.push(payload.url); + }, + }, + }, + buildRegistrationEmailStateUpdates: () => ({ + email: null, + registrationEmailState: { + current: '', + previous: 'old.user@example.com', + source: 'step8_recovery', + updatedAt: 123, + }, + }), + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + confirmCustomVerificationStepBypass: async () => {}, + ensureStep8VerificationPageReady: async () => ({ + state: 'add_email_page', + url: 'https://auth.openai.com/add-email', + }), + getOAuthFlowRemainingMs: async () => 5000, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getMailConfig: () => ({ + provider: 'qq', + label: 'QQ 閭', + source: 'mail-qq', + url: 'https://mail.qq.com', + navigateOnReuse: false, + }), + getState: async () => ({ + email: '', + registrationEmailState: { + current: '', + previous: 'old.user@example.com', + source: 'step8_recovery', + updatedAt: 123, + }, + oauthUrl: 'https://auth.openai.com/add-email', + password: 'secret', + }), + getTabId: async () => 1, + HOTMAIL_PROVIDER: 'hotmail-api', + isTabAlive: async () => true, + isVerificationMailPollingError: () => false, + LUCKMAIL_PROVIDER: 'luckmail-api', + resolveSignupEmailForFlow: async () => 'new.user@example.com', + resolveVerificationStep: async () => {}, + rerunStep7ForStep8Recovery: async () => {}, + reuseOrCreateTab: async () => {}, + sendToContentScriptResilient: async () => { + calls.contentCalls += 1; + if (calls.contentCalls === 1) { + throw new Error('STEP8_EMAIL_IN_USE::old.user@example.com'); + } + return { + submitted: true, + displayedEmail: 'new.user@example.com', + url: 'https://auth.openai.com/email-verification', + }; + }, + setState: async (payload) => { + calls.setStates.push(payload); + }, + shouldUseCustomRegistrationEmail: () => false, + sleepWithStop: async () => {}, + STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000, + STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 3, + throwIfStopped: () => {}, + }); + + await executor.executeStep8({ + email: 'old.user@example.com', + registrationEmailState: { + current: 'old.user@example.com', + previous: 'old.user@example.com', + source: 'generated:duck', + updatedAt: 1, + }, + oauthUrl: 'https://auth.openai.com/add-email', + password: 'secret', + visibleStep: 8, + }); + + assert.deepStrictEqual(calls.setStates[0], { + email: null, + registrationEmailState: { + current: '', + previous: 'old.user@example.com', + source: 'step8_recovery', + updatedAt: 123, + }, + step8VerificationTargetEmail: '', + loginVerificationRequestedAt: null, + }); +}); + test('Plus login-code step reuses step 8 verification logic but completes visible step 11', async () => { let resolvedStep = null; let resolvedOptions = null; diff --git a/tests/duck-mail-content.test.js b/tests/duck-mail-content.test.js index aadd14a..4621062 100644 --- a/tests/duck-mail-content.test.js +++ b/tests/duck-mail-content.test.js @@ -176,7 +176,7 @@ test('fetchDuckEmail falls back to the previous Duck email when the page baselin const result = await api.fetchDuckEmail({ generateNew: true, - previousEmail: 'previous@duck.com', + baselineEmail: 'previous@duck.com', }); assert.deepEqual(result, { diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 3818dca..72e7230 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -150,9 +150,11 @@ - 当前轮冻结后的注册方式 `resolvedSignupMethod` - 当前统一账号标识 `accountIdentifierType / accountIdentifier` - 当前邮箱 / 密码 +- 注册邮箱运行态 `registrationEmailState = { current, previous, source, updatedAt }` - 当前手机号注册运行态 `signupPhoneNumber / signupPhoneActivation / signupPhoneCompletedActivation` - 侧栏“注册手机号”输入框只同步运行态 `signupPhoneNumber / accountIdentifierType / accountIdentifier`,不属于持久配置,也不参与配置导入导出 - 邮箱身份写入时会把 `accountIdentifierType / accountIdentifier` 改回邮箱,并清理旧的 `signupPhone*` 注册手机号运行态,避免后续 Step 4 / Step 8 被旧手机号污染而误走短信分支 +- 邮箱写入统一通过 `setEmailState*` 同步更新 `email` 与 `registrationEmailState`;如果当前邮箱因 Step 8 恢复被清空,但还需要保留上一份 Duck / add-email 对比基线,则只清空 `current`,保留 `previous` - 本轮联动自检重点: 1. Step 2 一旦自动取到手机号,必须立刻广播并回填侧栏运行态手机号。 2. sidepanel 重新加载后,必须从当前 state 恢复“注册手机号”文本框,而不是只恢复邮箱。 @@ -468,6 +470,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功 1. 先确认当前认证页真实状态,而不是只看 `accountIdentifierType`。 2. 当前是 `add-email`: - 调用 `resolveSignupEmailForFlow` 按需获取或生成本轮绑定邮箱;手机号注册不会在 Step 2 提前生成邮箱,只有真实进入 `add-email` 时才占用邮箱资源 + - 如果邮箱生成器已经在生成阶段写回运行态,这里不会再重复覆盖 `registrationEmailState`,但会保留手机号身份 - 内容脚本发送 `SUBMIT_ADD_EMAIL` 填写邮箱并提交 - 页面进入邮箱验证码页后,用绑定邮箱作为 `step8VerificationTargetEmail` 3. 当前是邮箱验证码页: @@ -489,7 +492,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功 - 当登录验证码提交后进入 `phone-verification` 页时,内容脚本会显式把该页面识别为“手机验证码页”,避免与邮箱验证码页混淆。 - 手机号注册身份本身不会让 Step 8 自动走短信;只有真实页面状态是 `phone_verification_page` 时才调用短信 helper。手机号注册登录后出现 `add-email` 时,Step 8 走“按需生成邮箱 -> 绑定邮箱 -> 邮箱验证码”链路。 - 如果手机号注册账号在 Step 8 的 `add-email` / 邮箱验证码阶段失败并回退到 Step 7,Step 7 会优先使用当前轮保留的注册手机号身份重新登录,避免误用尚未绑定成功的邮箱登录。 -- `email_in_use` 会在 Step 8 当前步骤内清理当前邮箱并重新打开 `add-email` 获取新邮箱;`max_check_attempts` 不继续点击认证页 `重试`,只恢复当前 Step 8。 +- `email_in_use` 会在 Step 8 当前步骤内仅清空 `registrationEmailState.current / state.email`,保留 `registrationEmailState.previous` 作为上一轮比较基线,再重新打开 `add-email` 获取新邮箱;`max_check_attempts` 不继续点击认证页 `重试`,只恢复当前 Step 8。 ### Step 9 @@ -599,11 +602,15 @@ Plus 模式可见步骤: 2. 点击 `获取 / 生成` 时: - Gmail 生成 `name+tag@gmail.com` - 2925 仅在 provide 模式下生成 `name123456@2925.com` -3. Step 2 / Step 3 进入注册流程前,会先判断当前 `state.email` - - 如果已经是与当前基邮箱兼容的完整邮箱,则直接复用 +3. 生成链路与手动保存邮箱都会统一维护 `registrationEmailState = { current, previous, source, updatedAt }` + - `current` 表示当前准备提交或刚生成的注册邮箱 + - `previous` 表示最近一份仍可作为 Duck / Step 8 恢复对比基线的邮箱 +4. Step 2 / Step 3 进入注册流程前,不再只看单一的 `state.email` + - 如果当前完整邮箱仍与当前基邮箱兼容,则直接复用 - 如果为空或不兼容,则按当前 provider / mode 重新生成 -4. 保存或执行 Step 3 时,如果手动填写的完整邮箱与当前 Gmail / 2925 provide 基邮箱不兼容,sidepanel 会直接拦截;`2925 receive` 不参与这条兼容性约束 -5. auto-run fresh attempt reset 时,会保留: +5. sidepanel 手动点击“获取 / 生成”时,会把输入框当前可见邮箱一并传给后台,优先作为 Duck 新地址比较基线;如果 UI 当前为空,再回退到 `registrationEmailState.current / previous` +6. 保存或执行 Step 3 时,如果手动填写的完整邮箱与当前 Gmail / 2925 provide 基邮箱不兼容,sidepanel 会直接拦截;`2925 receive` 不参与这条兼容性约束 +7. auto-run fresh attempt reset 时,会保留: - `gmailBaseEmail` - `mail2925BaseEmail` @@ -645,11 +652,14 @@ Plus 模式可见步骤: - 别名邮箱生成 - UI 文案输出 +- `background/registration-email-state.js` + 统一维护当前注册邮箱、上一份比较基线邮箱、来源与更新时间,并提供 Duck 生成前解析比较基线、Step 8 清空当前邮箱但保留历史基线的共享工具。 + - `background/generated-email-helpers.js` - 在原有 Duck / Cloudflare / iCloud / Cloudflare Temp Email 生成链路之外,新增 Gmail / 2925 的共享生成接入。 + 在原有 Duck / Cloudflare / iCloud / Cloudflare Temp Email 生成链路之外,新增 Gmail / 2925 的共享生成接入;Duck 生成前会按“侧栏当前可见邮箱 -> `registrationEmailState.current` -> `registrationEmailState.previous`”的顺序解析比较基线。 - `background/signup-flow-helpers.js` - 负责在真正提交注册邮箱前做“复用已有完整邮箱 / 重新生成”的最终决策。 + 负责在真正提交注册邮箱前做“复用已有完整邮箱 / 重新生成”的最终决策,并避免把生成阶段已经落库的邮箱再次重复写入运行态。 ### 7.1 生成邮箱 @@ -666,6 +676,11 @@ Plus 模式可见步骤: - 自定义邮箱池 - iCloud 隐私邮箱 +补充: + +- 所有生成器在返回邮箱后都会统一调用 `setEmailState(..., { source })` 回写运行态,保证 `email` 与 `registrationEmailState` 同步更新。 +- Duck 生成链路不会再直接拿“生成前瞬间读到的输入框值”当唯一基线,而是优先使用 sidepanel 当前可见邮箱,再回退到共享的注册邮箱运行态基线,避免生成前后地址对比失效。 + ### 7.1.1 Cloud Mail 组成: diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 5b1a28f..44c13a4 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -48,18 +48,19 @@ - `background/account-run-history.js`:账号记录模块,负责以统一账号标识维护最新记录;邮箱注册以邮箱为主身份,手机号注册以手机号为主身份,同一轮后续绑定的手机号或邮箱会并入同一条记录并覆盖旧占位状态;模块统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,兼容 phone-only 与 email+phone 组合记录、空密码,并按默认本地 helper 地址自动尝试把完整快照同步到本地 helper;若 helper 未启动,则静默跳过。 - `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 Plus 模式与 PayPal 配置、`gmailBaseEmail`、`mail2925BaseEmail` 与当前 2925 账号选择,避免自动流程重置后丢失关键持久配置。 - `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `apikey.qzz.io` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。 -- `background/cloudmail-provider.js`:Cloud Mail / SkyMail 后台 provider 模块,负责 Token 获取、邮箱创建、邮件列表读取、验证码轮询和生成/转发收件目标邮箱选择。 -- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 iCloud 且 `icloudFetchMode = always_new` 时,会强制跳过未用别名复用并新建别名;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱。 +- `background/cloudmail-provider.js`:Cloud Mail / SkyMail 后台 provider 模块,负责 Token 获取、邮箱创建、邮件列表读取、验证码轮询和生成/转发收件目标邮箱选择;新生成的 Cloud Mail 地址也会统一回写带来源标记的注册邮箱运行态。 +- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 iCloud 且 `icloudFetchMode = always_new` 时,会强制跳过未用别名复用并新建别名;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱;Duck 生成前会优先结合 `registrationEmailState` 与侧栏当前可见邮箱解析对比基线,避免重复拿到旧地址;所有生成结果都会统一写回带 `source` 标记的注册邮箱运行态。 - `background/ip-proxy-core.js`:IP 代理核心模块,负责代理条目解析、账号/接口代理池运行态、PAC 应用与清除、代理鉴权回填、出口探测、失败时的目标站点 fail-close 防漏规则,以及自动运行成功阈值后的代理切换支撑。 - `background/ip-proxy-provider-711proxy.js`:711Proxy provider 规则模块,负责从账号串中识别和写回 `region / session / sessTime` 等参数,并在固定账号模式下把侧栏配置转换为最终生效的代理账号。 - `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;日志条目统一写入结构化 `step / stepKey`,sidepanel 只读取该元数据渲染步骤标签,不再从日志正文解析步骤号;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。 - `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。 -- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;`LOG / STEP_COMPLETE / STEP_ERROR` 会把消息里的真实 `step` 继续传给结构化日志,跳过登录验证码这类派生日志也按当前步骤 key 写入;当前额外接入 PayPal 账号池的新增与切换、2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息;当侧栏关闭 `oauthFlowTimeoutEnabled` 时,会立即清空已存在的 OAuth 总预算 deadline。 +- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;`LOG / STEP_COMPLETE / STEP_ERROR` 会把消息里的真实 `step` 继续传给结构化日志,跳过登录验证码这类派生日志也按当前步骤 key 写入;当前额外接入 PayPal 账号池的新增与切换、2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息;手动改邮箱、手动取邮箱等入口也统一经这里落到 `setEmailState`,同步维护注册邮箱运行态;当侧栏关闭 `oauthFlowTimeoutEnabled` 时,会立即清空已存在的 OAuth 总预算 deadline。 +- `background/registration-email-state.js`:注册邮箱运行态共享模块,负责维护 `registrationEmailState = { current, previous, source, updatedAt }`,并提供“当前邮箱清空时保留上一比较基线”“Duck 生成前解析比较基线”等统一工具。 - `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。 - `background/paypal-account-store.js`:PayPal 账号池持久化模块,负责保存账号列表、切换当前账号,并把当前选中账号同步回兼容字段 `paypalEmail / paypalPassword`。 - `background/phone-verification-flow.js`:手机号验证码共享流程模块,负责复用 HeroSMS / 5sim / NexSMS 的取号、复用、轮询、重发、换号与收尾能力;既承接 OAuth 后置 `add-phone / phone-verification` 页面,也承接手机号注册 Step 4 和 Step 8 中真实出现的 `phone-verification` 页面,并保持 `signupPhone*` 运行态与 `currentPhoneActivation` 隔离;后置 add-phone 成功后会把已绑定手机号写入运行态 `phoneNumber`,供账号记录并入同一轮;提交后置手机号验证码时会一并透传可复用的资料页 payload,便于认证页偶发跳回资料页时继续补齐;该流程由调用方传入当前可见步骤号,手机号日志不会再固定显示普通模式 Step 9。 - `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。 -- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、Step 2 打开入口页后的加载完成与额外 3 秒稳定等待、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。 +- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、Step 2 打开入口页后的加载完成与额外 3 秒稳定等待、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号;若邮箱生成器在生成阶段已经写回运行态,这里不会再重复覆盖 `registrationEmailState`,但仍会按需保留手机号身份。 - `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;内容脚本恢复等待日志支持 `logStep / logStepKey`,用于保持 Plus 复用步骤的日志标签正确;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。 - `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置、Cloud Mail API 轮询以及 2925 长轮询参数;日志步骤号使用 `completionStep`,因此 Plus 登录验证码会显示可见第 11 步而不是内部复用的 Step 8;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若登录验证码提交后页面转入 `add-phone / 手机号页`,则会把“后续需要手机号验证”的结果继续传给 OAuth 确认步骤,而不是误判为普通失败;若登录验证码提交后通信中断但认证页已进入 OAuth 同意页或手机号验证页,会按真实页面状态继续后续流程;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。 @@ -68,7 +69,7 @@ - `background/steps/wait-registration-success.js`:步骤 6 实现,负责注册资料提交后等待 20 秒,让注册成功状态和页面跳转稳定;默认不清理 cookies,只有侧栏第六步 `清 Cookies` 开关开启时才会在等待结束后清理 ChatGPT / OpenAI 相关 cookies。 - `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成;等待 callback 期间会动态检查 OAuth 总预算开关,关闭后仅保留本地回调等待超时。 - `background/steps/create-plus-checkout.js`:Plus 模式第 6 步实现,负责在已登录 ChatGPT 页面创建 PayPal / GoPay checkout session,或在 GPC 模式下读取 accessToken 并通过 `https://gpc.qlhazycoder.top/api/gp/tasks` 创建队列任务,记录 checkout / GPC task 运行态。 -- `background/steps/fetch-login-code.js`:步骤 8 实现,负责按真实认证页状态分发登录后续流程:邮箱验证码页走邮箱轮询、验证码回填与回退控制;真实 `phone-verification` 页才复用手机号验证码共享流程;手机号注册登录后若进入 `add-email`,会先生成/解析邮箱并提交绑定,再进入邮箱验证码轮询;邮箱分支对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱,当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录。 +- `background/steps/fetch-login-code.js`:步骤 8 实现,负责按真实认证页状态分发登录后续流程:邮箱验证码页走邮箱轮询、验证码回填与回退控制;真实 `phone-verification` 页才复用手机号验证码共享流程;手机号注册登录后若进入 `add-email`,会先生成/解析邮箱并提交绑定,再进入邮箱验证码轮询;邮箱分支对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱,当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录;命中 `email_in_use` 时会仅清空当前邮箱并保留上一轮比较基线,再在当前 Step 8 内重开 `add-email` 获取新邮箱,`max_check_attempts` 也只重启当前步骤恢复链。 - `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;邮箱注册继续走邮箱验证码,手机号注册改走短信验证码;当 provider 为 2925 时,会在邮箱轮询前先确保当前 2925 账号已自动登录。 - `background/steps/fill-plus-checkout.js`:Plus 模式第 7 步实现,负责驱动 PayPal / GoPay checkout 页面选择付款方式、填写账单地址、提交订阅并等待跳转;GPC 模式则轮询队列任务,按远端 `api_waiting_for` 提交 OTP / PIN,并在任务失败、过期或取消时清理运行态。 - `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交;当前已改为身份中立,手机号注册遇到无密码页时会按页面状态自动跳过。 @@ -85,7 +86,7 @@ - `content/activation-utils.js`:内容脚本通用激活策略工具,负责按钮点击方式判断与平台回调可恢复失败文案判断。 - `content/auth-page-recovery.js`:认证页共享恢复模块,统一封装 `Try again / 重试` 页识别、点击重试和等待退出重试页;当前由注册验证码等待、OAuth 登录恢复、OAuth 同意页重试恢复链路复用。 -- `content/duck-mail.js`:DuckDuckGo Email Protection 页面脚本,负责生成或读取 `@duck.com` 地址。 +- `content/duck-mail.js`:DuckDuckGo Email Protection 页面脚本,负责生成或读取 `@duck.com` 地址;生成新地址时会优先读取显式传入的基线邮箱,并要求新地址稳定出现两次后才接受,避免页面旧值尚未渲染完成时误判“地址已变化”。 - `content/gmail-mail.js`:Gmail 邮箱轮询脚本,负责在 Gmail 页面中匹配验证码邮件。 - `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。 - `content/inbucket-mail.js`:Inbucket 邮箱轮询脚本,负责在 Inbucket 页面中读取、删除验证码邮件。 @@ -160,7 +161,7 @@ 动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接 到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显 示并同时服务于 provide / receive 两种模式;当 provider 为 iCloud 时,会保存并回显目标邮箱类型与转发邮箱 provider,相关 provider 选 - 项和 label 复用 `mail-provider-utils.js`;Cloud Mail 配置保存、回显和显隐由这里接入,可分别服务于邮箱生成器和转发收件 provider;`自定义邮箱池` 模式会按邮箱池长度锁定自动轮数,并把当前输入的邮箱池配置参与自动启动前保 + 项和 label 复用 `mail-provider-utils.js`;Cloud Mail 配置保存、回显和显隐由这里接入,可分别服务于邮箱生成器和转发收件 provider;手动点击“获取/生成邮箱”时,这里会把输入框当前可见邮箱一并传给后台,作为 Duck 新地址检测的优先对比基线;`自定义邮箱池` 模式会按邮箱池长度锁定自动轮数,并把当前输入的邮箱池配置参与自动启动前保 存;点击“自动”时会先冻结当时的目标轮数,避免启动前的异步刷新、保存或后台旧状态回灌把输入框重置为 1;`邮箱服务 = 自定义邮箱` 时,如果配置了 `customMailProviderPool`,也会按号池长度锁定自动轮数并在 Auto 中按轮次分配注册邮箱,同时 在普通失败时继续复用当前邮箱,只有成功或出现手机号验证时才切换下一个邮箱;Plus 模式开启后,步骤列表会切换为 13 步,并显示 PayPal 账 号下拉框与添加入口;当前选中的 PayPal 账号会同步回兼容字段 `paypalEmail / paypalPassword` 供后台步骤复用;Step 8 的自定义邮箱确认弹 @@ -187,7 +188,7 @@ - `tests/background-account-history-settings.test.js`:测试账号运行历史的独立配置项归一化,不再复用 Hotmail 模式作为启停条件。 - `tests/background-contribution-mode.test.js`:测试贡献模式的后台运行态与公开 OAuth 接入,覆盖 `contributionMode` 只存在于运行态、`SET_CONTRIBUTION_MODE / POLL_CONTRIBUTION_STATUS` 消息接入、reset 保留贡献运行态,以及 callback 自动提交在“无需手动提交”场景下的兼容处理。 - `tests/background-custom-email-pool.test.js`:测试后台对自定义邮箱池生成方式,以及自定义邮箱服务号池的归一化、标签文案和按轮次取邮箱逻辑。 -- `tests/background-generated-email-module.test.js`:测试生成邮箱辅助模块已接入且导出工厂,并覆盖 2925 receive 模式回退普通邮箱生成、自定义邮箱池按轮次取值、2925 provide 模式账号池预热,以及 iCloud `always_new` 传参。 +- `tests/background-generated-email-module.test.js`:测试生成邮箱辅助模块已接入且导出工厂,并覆盖 Duck 对比基线优先级、2925 receive 模式回退普通邮箱生成、自定义邮箱池按轮次取值、2925 provide 模式账号池预热,以及 iCloud `always_new` 传参。 - `tests/background-icloud.test.js`:测试 iCloud 相关后台纯函数与别名收尾逻辑。 - `tests/background-icloud-login-help.test.js`:测试 iCloud 登录判定、页面上下文回退、别名缓存回退和保留异常恢复的关键源码约束。 - `tests/background-icloud-mail-provider.test.js`:测试 iCloud 邮箱 provider 配置解析,并覆盖转发收码模式复用共享 provider 配置。 @@ -202,13 +203,14 @@ - `tests/background-panel-bridge-module.test.js`:测试面板桥接模块已接入且导出工厂。 - `tests/background-paypal-account-store-module.test.js`:测试 PayPal 账号池后台模块已接入、导出工厂,并覆盖当前账号选择会同步回兼容字段 `paypalEmail / paypalPassword`。 - `tests/background-platform-verify-codex2api.test.js`:测试 Codex2API 新来源在步骤 10 走协议式 callback 交换,不依赖后台页面注入。 +- `tests/background-registration-email-state.test.js`:测试注册邮箱运行态共享模块对 `registrationEmailState` 的维护,覆盖清空当前邮箱时保留上一比较基线,以及 Duck 生成前优先使用侧栏当前可见邮箱作为基线。 - `tests/background-signup-flow-module.test.js`:测试注册页辅助模块已接入且导出工厂。 - `tests/background-skip-step-linking.test.js`:测试手动跳过步骤 1 时,会级联跳过步骤 2~5,并仅跳过其中未完成且未运行的步骤。 -- `tests/background-signup-step2-branching.test.js`:测试在 Gmail / 2925 模式下,已有兼容别名邮箱时应直接复用,不应再次重生成。 +- `tests/background-signup-step2-branching.test.js`:测试在 Gmail / 2925 模式下,已有兼容别名邮箱时应直接复用,不应再次重生成,并覆盖生成阶段已落库邮箱不会被流程层重复覆盖。 - `tests/background-step-modules.test.js`:测试步骤模块文件都已由后台入口加载。 - `tests/background-step5-submit-short-circuit.test.js`:测试步骤 5 会把生成好的资料直接转发给内容脚本,并依赖完成信号收尾。 - `tests/background-step6-retry-limit.test.js`:测试步骤 6 的注册成功等待、可选 cookies 清理开关,以及步骤 7 的有限重试上限与 `add-phone` 命中后的立即跳出行为。 -- `tests/background-step7-recovery.test.js`:测试步骤 8 获取登录验证码后直接提交(不再回放步骤 7),覆盖邮箱验证码页、真实手机验证码页、`add-email` 绑定邮箱后收码、2925 固定回看窗口与关闭重发间隔。 +- `tests/background-step7-recovery.test.js`:测试步骤 8 获取登录验证码后直接提交(不再回放步骤 7),覆盖邮箱验证码页、真实手机验证码页、`add-email` 绑定邮箱后收码、`email_in_use / max_check_attempts` 当前步骤恢复、2925 固定回看窗口与关闭重发间隔。 - `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。 - `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂,并覆盖等待标签完成、等待标签稳定完成,以及等待过程中的 Stop 中断行为。 - `tests/background-verification-flow-module.test.js`:测试验证码流程模块已接入且导出工厂。 @@ -224,6 +226,7 @@ - `tests/cloudmail-provider.test.js`:测试 Cloud Mail provider 的验证码轮询和生成/转发收件目标邮箱选择逻辑。 - `tests/cloudmail-utils.test.js`:测试 Cloud Mail 工具层的 URL、域名、鉴权头、Token 与邮件响应归一化逻辑。 - `tests/content-utils.test.js`:测试内容脚本公共工具层对 `mail.126.com` 等网页邮箱来源的识别逻辑。 +- `tests/duck-mail-content.test.js`:测试 Duck 页面脚本会等待页面现有地址或显式基线出现后再比较新地址,并要求新地址稳定渲染后才判定生成成功。 - `tests/hotmail-api-mode.test.js`:测试 Hotmail API 模式相关文案和集成方式。 - `tests/hotmail-cors-headers.test.js`:测试 Microsoft token 请求相关 DNR 配置。 - `tests/hotmail-utils.test.js`:测试 Hotmail 工具层的账号筛选、验证码提取和消息匹配逻辑。