diff --git a/README.md b/README.md index b3c01da..856467d 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ - 自动获取注册验证码与登录验证码 - 支持 `QQ Mail`、`163 Mail`、`Inbucket mailbox` - 支持从 DuckDuckGo Email Protection 自动生成新的 `@duck.com` 地址 +- 支持基于 Cloudflare 自定义域名自动生成随机邮箱前缀 - Step 5 同时兼容两种页面: - 页面要求填写 `birthday` - 页面要求填写 `age` @@ -59,6 +60,7 @@ - 你自己的 CPA 管理面板,且页面结构与当前脚本适配 - 至少准备一种验证码接收方式: - DuckDuckGo `@duck.com` + QQ / 163 / Inbucket 转发 + - Cloudflare 自定义域邮箱前缀 + QQ / 163 / Inbucket 转发 - 手动填写一个可收信邮箱 - 如果使用 `QQ` / `163` / `Inbucket`,对应页面需要提前能正常打开 @@ -135,13 +137,55 @@ Step 3 使用的注册邮箱。 来源有两种: - 手动粘贴 -- 点击 `Auto` 从 DuckDuckGo Email Protection 自动获取一个新的 `@duck.com` +- 点击 `获取` 自动生成邮箱(DuckDuckGo 或 Cloudflare) 注意: -- 当前 `Auto` 按钮只负责 DuckDuckGo 地址获取 +- 若 `邮箱生成 = Cloudflare`,插件里只需要维护 `CF 域名` +- `CF 域名` 支持保存多个,并通过下拉框切换当前要生成的域名 +- Cloudflare 侧的转发规则、Catch-all、路由目标邮箱等,都需要你自己提前在 Cloudflare 后台配置好 - 如果你使用 Inbucket,它只是验证码收件箱,不会自动生成 Inbucket 地址 +### `邮箱生成 = Cloudflare` 时的配置 + +- `CF 域名`:例如 `example.xyz` +- 右侧 `添加 / 保存`:用于保存多个可切换的域名 +- 下拉框:用于切换当前这次要生成邮箱所使用的域名 + +#### 当前实现是什么逻辑 + +Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 + +它现在只做一件事: + +1. 根据你当前选中的 `CF 域名` +2. 本地生成一个随机前缀 +3. 直接得到一个类似 `user20260412153000123@example.xyz` 的注册邮箱 +4. 把这个邮箱写入当前流程继续往下跑 + +也就是说,插件默认认为: + +- 你已经在 Cloudflare 后台把这个域名的收件转发规则配置好了 +- 这个随机前缀邮箱发来的邮件,最终能被你现有的 `163 / QQ / Inbucket` 收件链路接住 + +#### 你需要自己提前做什么 + +在 Cloudflare 后台,至少保证下面一条成立: + +- 你已经配好了 Catch-all / 通配规则,能接住任意前缀邮箱 +- 或者你本来就有一套能覆盖这些随机前缀邮箱的转发规则 + +否则插件虽然能生成 `@你的域名` 邮箱,但验证码邮件最后没人接收,后面的 Step 4 / Step 7 还是会失败。 + +#### 最简单的使用方式 + +1. 在 Cloudflare 后台先把你的域名收件转发规则配好 +2. 在插件里选择 `邮箱生成 = Cloudflare` +3. 在 `CF 域名` 里点 `添加` +4. 输入域名后点 `保存` +5. 以后直接从下拉框切换当前使用的域名 +6. 点击 `获取`,插件就会基于这个域名生成一个随机邮箱 + ### `Password` - 留空:自动生成强密码 @@ -186,14 +230,14 @@ Step 3 使用的注册邮箱。 1. Step 1 获取 CPA OAuth 链接 2. Step 2 打开 OpenAI 注册页 -3. 尝试自动获取 Duck 邮箱 -4. 如果 Duck 自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue` +3. 按当前“邮箱生成”配置尝试自动获取邮箱(Duck 或 Cloudflare) +4. 如果自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue` 5. 继续执行 Step 3 ~ Step 9 也就是说: -- 如果 Duck 邮箱可自动获取,整套流程更接近全自动 -- 如果 Duck 自动获取失败,后台会先自动重试 5 次;仍失败时,Auto 才会在邮箱阶段暂停 +- 如果邮箱可自动获取,整套流程更接近全自动 +- 如果自动获取失败,后台会先自动重试 5 次;仍失败时,Auto 才会在邮箱阶段暂停 - Auto 的暂停状态会保存在会话状态中,重新打开侧边栏后仍可继续 - 如果你在 Auto 暂停时改为手动点步骤或跳过步骤,面板会先确认并停止 Auto,再切回手动控制 - 选择 `继续当前` 时,后台不会先做大而全的前置校验,而是从当前步骤状态直接继续;缺什么条件,就在运行到那一步时再报错或暂停 @@ -221,7 +265,7 @@ Step 3 使用的注册邮箱。 ### Step 3: Fill Email / Password -- 如果侧边栏邮箱为空,会先尝试自动获取 DuckDuckGo 邮箱;失败时再提示手动粘贴 +- 如果侧边栏邮箱为空,会先按当前“邮箱生成”配置自动获取邮箱;失败时再提示手动粘贴 - 自动填写邮箱 - 如页面先要求邮箱,再进入密码页,会自动切页继续填写 - 使用自定义密码或自动生成密码 @@ -405,9 +449,9 @@ sidepanel/ 侧边栏 UI - 给脚本准备一个相对独立的 mailbox - 避免收件箱里混入过多无关邮件 -### 3. Duck 自动获取失败时直接手填 +### 3. 自动获取失败时直接手填 -如果 Duck 页面打不开、未登录或按钮变化: +如果 Duck 页面打不开、Cloudflare 域名未配置、未登录或按钮变化: - 直接在 `Email` 输入框中粘贴邮箱 - 手动点 `Step 3` 时,如果邮箱为空,脚本会先自动尝试获取 Duck 邮箱;失败后再改为手填 diff --git a/background.js b/background.js index 14ff076..fc8670c 100644 --- a/background.js +++ b/background.js @@ -25,9 +25,12 @@ const PERSISTED_SETTING_DEFAULTS = { autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。 autoRunDelayEnabled: false, // 自动运行是否启用启动前倒计时。 autoRunDelayMinutes: 30, // 自动运行倒计时分钟数。 - mailProvider: '163', // 验证码邮箱来源,当前支持 163 / inbucket。 + mailProvider: '163', // 验证码邮箱来源(163 / 163-vip / qq / inbucket)。 + emailGenerator: 'duck', // 注册邮箱生成方式:duck / cloudflare。 inbucketHost: '', // 仅当 mailProvider 为 inbucket 时填写 Inbucket 地址,其他情况保持为空。 inbucketMailbox: '', // 仅当 mailProvider 为 inbucket 时填写邮箱名,其他情况保持为空。 + cloudflareDomain: '', // 仅当 emailGenerator=cloudflare 时填写自定义域名。 + cloudflareDomains: [], // Cloudflare 可选域名列表。 }; const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS); @@ -91,6 +94,20 @@ function normalizeScheduledAutoRunPlan(plan) { }; } +function normalizeEmailGenerator(value = '') { + return String(value || '').trim().toLowerCase() === 'cloudflare' ? 'cloudflare' : 'duck'; +} + +function normalizeCloudflareDomain(rawValue = '') { + let value = String(rawValue || '').trim().toLowerCase(); + if (!value) return ''; + value = value.replace(/^@+/, ''); + value = value.replace(/^https?:\/\//, ''); + value = value.replace(/\/.*$/, ''); + if (!/^[a-z0-9.-]+\.[a-z]{2,}$/.test(value)) return ''; + return value; +} + async function getPersistedSettings() { const stored = await chrome.storage.local.get(PERSISTED_SETTING_KEYS); return { @@ -99,6 +116,7 @@ async function getPersistedSettings() { autoRunSkipFailures: Boolean(stored.autoRunSkipFailures ?? PERSISTED_SETTING_DEFAULTS.autoRunSkipFailures), autoRunDelayEnabled: Boolean(stored.autoRunDelayEnabled ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayEnabled), autoRunDelayMinutes: normalizeAutoRunDelayMinutes(stored.autoRunDelayMinutes ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayMinutes), + emailGenerator: normalizeEmailGenerator(stored.emailGenerator ?? PERSISTED_SETTING_DEFAULTS.emailGenerator), }; } @@ -125,7 +143,9 @@ async function initializeSessionStorageAccess() { async function setState(updates) { console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200)); - await chrome.storage.session.set(updates); + if (Object.keys(updates || {}).length > 0) { + await chrome.storage.session.set(updates); + } } async function setPersistentSettings(updates) { @@ -1721,8 +1741,13 @@ async function handleMessage(message, sender) { if (message.payload.autoRunDelayEnabled !== undefined) updates.autoRunDelayEnabled = Boolean(message.payload.autoRunDelayEnabled); if (message.payload.autoRunDelayMinutes !== undefined) updates.autoRunDelayMinutes = normalizeAutoRunDelayMinutes(message.payload.autoRunDelayMinutes); if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider; + if (message.payload.emailGenerator !== undefined) updates.emailGenerator = normalizeEmailGenerator(message.payload.emailGenerator); if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost; if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox; + if (message.payload.cloudflareDomain !== undefined) updates.cloudflareDomain = normalizeCloudflareDomain(message.payload.cloudflareDomain); + if (message.payload.cloudflareDomains !== undefined) updates.cloudflareDomains = Array.isArray(message.payload.cloudflareDomains) + ? message.payload.cloudflareDomains.map(domain => normalizeCloudflareDomain(domain)).filter(Boolean) + : []; await setPersistentSettings(updates); await setState(updates); return { ok: true }; @@ -1739,13 +1764,24 @@ async function handleMessage(message, sender) { return { ok: true, email: message.payload.email }; } + case 'FETCH_GENERATED_EMAIL': { + clearStopRequest(); + const state = await getState(); + if (isAutoRunLockedState(state)) { + throw new Error('自动流程运行中,当前不能手动获取邮箱。'); + } + const email = await fetchGeneratedEmail(state, message.payload || {}); + await resumeAutoRun(); + return { ok: true, email }; + } + case 'FETCH_DUCK_EMAIL': { clearStopRequest(); const state = await getState(); if (isAutoRunLockedState(state)) { - throw new Error('自动流程运行中,当前不能手动获取 Duck 邮箱。'); + throw new Error('自动流程运行中,当前不能手动获取邮箱。'); } - const email = await fetchDuckEmail(message.payload || {}); + const email = await fetchGeneratedEmail(state, { ...(message.payload || {}), generator: 'duck' }); await resumeAutoRun(); return { ok: true, email }; } @@ -1991,6 +2027,40 @@ async function executeStepAndWait(step, delayAfter = 2000) { } } +function getEmailGeneratorLabel(generator) { + return generator === 'cloudflare' ? 'Cloudflare 邮箱' : 'Duck 邮箱'; +} + +function generateCloudflareAliasLocalPart() { + const now = new Date(); + const stamp = [ + now.getFullYear(), + String(now.getMonth() + 1).padStart(2, '0'), + String(now.getDate()).padStart(2, '0'), + String(now.getHours()).padStart(2, '0'), + String(now.getMinutes()).padStart(2, '0'), + String(now.getSeconds()).padStart(2, '0'), + ].join(''); + const randomPart = String(Math.floor(Math.random() * 900) + 100); + return `user${stamp}${randomPart}`.toLowerCase(); +} + +async function fetchCloudflareEmail(state, options = {}) { + throwIfStopped(); + const latestState = state || await getState(); + const domain = normalizeCloudflareDomain(latestState.cloudflareDomain); + if (!domain) { + throw new Error('Cloudflare 域名为空或格式无效。'); + } + + const localPart = String(options.localPart || '').trim().toLowerCase() || generateCloudflareAliasLocalPart(); + const aliasEmail = `${localPart}@${domain}`; + + await setEmailState(aliasEmail); + await addLog(`Cloudflare 邮箱:已生成 ${aliasEmail}`, 'ok'); + return aliasEmail; +} + async function fetchDuckEmail(options = {}) { throwIfStopped(); const { generateNew = true } = options; @@ -2016,6 +2086,15 @@ async function fetchDuckEmail(options = {}) { return result.email; } +async function fetchGeneratedEmail(state, options = {}) { + const currentState = state || await getState(); + const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator); + if (generator === 'cloudflare') { + return fetchCloudflareEmail(currentState, options); + } + return fetchDuckEmail(options); +} + // ============================================================ // Auto Run Flow // ============================================================ @@ -2024,7 +2103,7 @@ let autoRunActive = false; let autoRunCurrentRun = 0; let autoRunTotalRuns = 1; let autoRunAttemptRun = 0; -const DUCK_EMAIL_MAX_ATTEMPTS = 5; +const EMAIL_FETCH_MAX_ATTEMPTS = 5; const VERIFICATION_POLL_MAX_ROUNDS = 5; const AUTO_STEP_DELAYS = { 1: 2000, @@ -2063,23 +2142,31 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { return currentState.email; } - let lastDuckError = null; - for (let duckAttempt = 1; duckAttempt <= DUCK_EMAIL_MAX_ATTEMPTS; duckAttempt++) { + const generator = normalizeEmailGenerator(currentState.emailGenerator); + const generatorLabel = getEmailGeneratorLabel(generator); + let lastError = null; + for (let attempt = 1; attempt <= EMAIL_FETCH_MAX_ATTEMPTS; attempt++) { try { - if (duckAttempt > 1) { - await addLog(`Duck 邮箱:正在进行第 ${duckAttempt}/${DUCK_EMAIL_MAX_ATTEMPTS} 次自动获取重试...`, 'warn'); + if (attempt > 1) { + await addLog(`${generatorLabel}:正在进行第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次自动获取重试...`, 'warn'); } - const duckEmail = await fetchDuckEmail({ generateNew: true }); - await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:Duck 邮箱已就绪:${duckEmail}(第 ${attemptRuns} 次尝试,Duck 第 ${duckAttempt}/${DUCK_EMAIL_MAX_ATTEMPTS} 次获取)===`, 'ok'); - return duckEmail; + const generatedEmail = await fetchGeneratedEmail(currentState, { generateNew: true, generator }); + await addLog( + `=== 目标 ${targetRun}/${totalRuns} 轮:${generatorLabel}已就绪:${generatedEmail}(第 ${attemptRuns} 次尝试,第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次获取)===`, + 'ok' + ); + return generatedEmail; } catch (err) { - lastDuckError = err; - await addLog(`Duck 邮箱自动获取失败(${duckAttempt}/${DUCK_EMAIL_MAX_ATTEMPTS}):${err.message}`, 'warn'); + lastError = err; + await addLog(`${generatorLabel}自动获取失败(${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS}):${err.message}`, 'warn'); + if (generator === 'cloudflare' && /域名/.test(String(err.message || ''))) { + break; + } } } - await addLog(`Duck 邮箱自动获取已连续失败 ${DUCK_EMAIL_MAX_ATTEMPTS} 次:${lastDuckError?.message || '未知错误'}`, 'error'); - await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先获取 Duck 邮箱或手动粘贴邮箱,然后继续 ===`, 'warn'); + await addLog(`${generatorLabel}自动获取已连续失败 ${EMAIL_FETCH_MAX_ATTEMPTS} 次:${lastError?.message || '未知错误'}`, 'error'); + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先自动获取邮箱或手动粘贴邮箱,然后继续 ===`, 'warn'); await broadcastAutoRunStatus('waiting_email', { currentRun: targetRun, totalRuns, @@ -2215,9 +2302,14 @@ async function autoRunLoop(totalRuns, options = {}) { vpsPassword: prevState.vpsPassword, customPassword: prevState.customPassword, autoRunSkipFailures: prevState.autoRunSkipFailures, + autoRunDelayEnabled: prevState.autoRunDelayEnabled, + autoRunDelayMinutes: prevState.autoRunDelayMinutes, mailProvider: prevState.mailProvider, + emailGenerator: prevState.emailGenerator, inbucketHost: prevState.inbucketHost, inbucketMailbox: prevState.inbucketMailbox, + cloudflareDomain: prevState.cloudflareDomain, + cloudflareDomains: prevState.cloudflareDomains, ...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns }), ...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}), }; diff --git a/content/duck-mail.js b/content/duck-mail.js index 35fd7f8..51c3f68 100644 --- a/content/duck-mail.js +++ b/content/duck-mail.js @@ -1,4 +1,4 @@ -// content/duck-mail.js — Content script for DuckDuckGo Email Protection autofill settings +// content/duck-mail.js - Content script for DuckDuckGo Email Protection autofill settings console.log('[MultiPage:duck-mail] Content script loaded on', location.href); @@ -30,9 +30,35 @@ async function fetchDuckEmail(payload = {}) { 15000 ); + const GENERATE_BUTTON_PATTERN = /generate\s+private\s+duck\s+address|new\s+private\s+duck\s+address|generate\s+new|new\s+address|生成.*duck.*地址|生成.*私有.*地址|生成.*地址|新.*地址/i; + const getAddressInput = () => document.querySelector('input.AutofillSettingsPanel__PrivateDuckAddressValue'); - const getGeneratorButton = () => document.querySelector('button.AutofillSettingsPanel__GeneratorButton') - || Array.from(document.querySelectorAll('button')).find(btn => /generate private duck address/i.test(btn.textContent || '')); + const getGeneratorButton = () => { + const direct = document.querySelector('button.AutofillSettingsPanel__GeneratorButton'); + if (direct) return direct; + + const selectors = [ + 'button[data-testid*="Generator"]', + 'button[class*="Generator"]', + 'button[aria-label*="duck" i]', + 'button[title*="duck" i]', + '[role="button"]', + 'button', + ]; + const candidates = selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector))); + return candidates.find((btn) => { + const text = [ + btn.textContent, + btn.getAttribute?.('aria-label'), + btn.getAttribute?.('title'), + ] + .filter(Boolean) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); + return GENERATE_BUTTON_PATTERN.test(text); + }) || null; + }; const readEmail = () => { const value = getAddressInput()?.value?.trim() || ''; return value.includes('@duck.com') ? value : ''; @@ -55,20 +81,39 @@ async function fetchDuckEmail(payload = {}) { return { email: currentEmail, generated: false }; } - await humanPause(500, 1300); const generatorButton = getGeneratorButton(); if (!generatorButton) { + if (generateNew) { + throw new Error('未找到“生成新 Duck 地址”按钮(可能是页面文案/语言变化、未登录或页面结构更新)。'); + } if (currentEmail) { - log(`Duck 邮箱:正在复用现有地址 ${currentEmail}`, 'warn'); + log(`Duck 邮箱:未找到生成按钮,复用现有地址 ${currentEmail}`, 'warn'); return { email: currentEmail, generated: false }; } throw new Error('未找到“生成 Duck 私有地址”按钮。'); } - generatorButton.click(); - log('Duck 邮箱:已点击“生成 Duck 私有地址”按钮'); + for (let attempt = 1; attempt <= 2; attempt++) { + await humanPause(500, 1300); + if (typeof simulateClick === 'function') { + simulateClick(generatorButton); + } else { + generatorButton.click(); + } + log(`Duck 邮箱:已点击“生成 Duck 私有地址”按钮(${attempt}/2)`); - const nextEmail = await waitForEmailValue(currentEmail); - log(`Duck 邮箱:地址已就绪 ${nextEmail}`, 'ok'); - return { email: nextEmail, generated: true }; + try { + const nextEmail = await waitForEmailValue(currentEmail); + log(`Duck 邮箱:地址已就绪 ${nextEmail}`, 'ok'); + return { email: nextEmail, generated: true }; + } catch (err) { + if (attempt >= 2) { + throw err; + } + log('Duck 邮箱:首次生成后地址未变化,准备重试一次...', 'warn'); + await sleep(800); + } + } + + throw new Error('Duck 地址生成失败。'); } diff --git a/manifest.json b/manifest.json index 14473c3..125dc21 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "多页面自动化", - "version": "1.1.0", + "version": "5.0.0", "description": "用于自动执行多步骤 OAuth 注册流程", "permissions": [ "sidePanel", diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 3c69dfe..e914505 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -1,19 +1,24 @@ +