QLHazyCoder 4 місяців тому
батько
коміт
4228888cca
6 змінених файлів з 508 додано та 84 видалено
  1. 53 9
      README.md
  2. 108 16
      background.js
  3. 55 10
      content/duck-mail.js
  4. 1 1
      manifest.json
  5. 74 38
      sidepanel/sidepanel.html
  6. 217 10
      sidepanel/sidepanel.js

+ 53 - 9
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 邮箱;失败后再改为手填

+ 108 - 16
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: {} } : {}),
       };

+ 55 - 10
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)`);
+
+    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);
+    }
+  }
 
-  const nextEmail = await waitForEmailValue(currentEmail);
-  log(`Duck 邮箱:地址已就绪 ${nextEmail}`, 'ok');
-  return { email: nextEmail, generated: true };
+  throw new Error('Duck 地址生成失败。');
 }

+ 1 - 1
manifest.json

@@ -1,7 +1,7 @@
 {
   "manifest_version": 3,
   "name": "多页面自动化",
-  "version": "1.1.0",
+  "version": "5.0.0",
   "description": "用于自动执行多步骤 OAuth 注册流程",
   "permissions": [
     "sidePanel",

+ 74 - 38
sidepanel/sidepanel.html

@@ -1,19 +1,24 @@
 <!DOCTYPE html>
 <html lang="zh-CN">
+
 <head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>多页面自动化面板</title>
   <link rel="preconnect" href="https://fonts.googleapis.com">
   <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+  <link
+    href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
+    rel="stylesheet">
   <link rel="stylesheet" href="sidepanel.css">
 </head>
+
 <body>
   <header>
     <div class="header-left">
-      <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-        <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
+      <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
+        stroke-linecap="round" stroke-linejoin="round">
+        <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
       </svg>
       <h1>多页面</h1>
     </div>
@@ -21,22 +26,36 @@
       <div class="run-group">
         <input type="number" id="input-run-count" class="run-count-input" value="1" min="1" max="50" title="运行次数" />
         <button id="btn-auto-run" class="btn btn-success" title="自动执行全部步骤">
-          <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
+          <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
+            <polygon points="5 3 19 12 5 21 5 3" />
+          </svg>
           自动
         </button>
         <button id="btn-stop" class="btn btn-danger" title="停止当前流程" disabled>停止</button>
       </div>
       <button id="btn-reset" class="btn btn-ghost" title="重置全部步骤">
-        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-          <polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
+        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
+          stroke-linecap="round" stroke-linejoin="round">
+          <polyline points="1 4 1 10 7 10" />
+          <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
         </svg>
       </button>
       <button id="btn-theme" class="theme-toggle" title="切换主题">
-        <svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-          <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
+        <svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+          stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+          <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
         </svg>
-        <svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-          <circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
+        <svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+          stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+          <circle cx="12" cy="12" r="5" />
+          <line x1="12" y1="1" x2="12" y2="3" />
+          <line x1="12" y1="21" x2="12" y2="23" />
+          <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
+          <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
+          <line x1="1" y1="12" x2="3" y2="12" />
+          <line x1="21" y1="12" x2="23" y2="12" />
+          <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
+          <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
         </svg>
       </button>
     </div>
@@ -47,14 +66,28 @@
       <div class="data-row">
         <span class="data-label">CPA</span>
         <div class="input-with-icon">
-          <input type="password" id="input-vps-url" class="data-input data-input-with-icon" placeholder="http://ip:port/management.html#/oauth" />
-          <button id="btn-toggle-vps-url" class="input-icon-btn" type="button" aria-label="显示 CPA 地址" title="显示 CPA 地址"></button>
+          <input type="password" id="input-vps-url" class="data-input data-input-with-icon"
+            placeholder="http://ip:port/management.html#/oauth" />
+          <button id="btn-toggle-vps-url" class="input-icon-btn" type="button" aria-label="显示 CPA 地址"
+            title="显示 CPA 地址"></button>
         </div>
       </div>
       <div class="data-row">
         <span class="data-label">管理密钥</span>
         <input type="password" id="input-vps-password" class="data-input" placeholder="请输入 CPA 管理密钥" />
       </div>
+      <div class="data-row">
+        <span class="data-label">codex密码</span>
+        <div class="data-inline">
+          <div class="input-with-icon">
+            <input type="password" id="input-password" class="data-input data-input-with-icon"
+              placeholder="codex密码,留空则自动生成" />
+            <button id="btn-toggle-password" class="input-icon-btn" type="button" aria-label="显示密码"
+              title="显示密码"></button>
+          </div>
+          <button id="btn-save-settings" class="btn btn-outline btn-sm" type="button">保存</button>
+        </div>
+      </div>
       <div class="data-row">
         <span class="data-label">邮箱服务</span>
         <select id="select-mail-provider" class="data-select">
@@ -64,6 +97,22 @@
           <option value="inbucket">Inbucket(自定义主机)</option>
         </select>
       </div>
+      <div class="data-row">
+        <span class="data-label">邮箱生成</span>
+        <select id="select-email-generator" class="data-select">
+          <option value="duck">DuckDuckGo</option>
+          <option value="cloudflare">Cloudflare</option>
+        </select>
+      </div>
+      <div class="data-row" id="row-cf-domain" style="display:none;">
+        <span class="data-label">CF 域名</span>
+        <div class="data-inline">
+          <select id="select-cf-domain" class="data-select"></select>
+          <input type="text" id="input-cf-domain" class="data-input" placeholder="例如 yourdomain.xyz"
+            style="display:none;" />
+          <button id="btn-cf-domain-mode" class="btn btn-outline btn-sm" type="button">添加</button>
+        </div>
+      </div>
       <div class="data-row" id="row-inbucket-host" style="display:none;">
         <span class="data-label">Inbucket</span>
         <input type="text" id="input-inbucket-host" class="data-input" placeholder="填写主机或 https://主机地址" />
@@ -73,22 +122,12 @@
         <input type="text" id="input-inbucket-mailbox" class="data-input" placeholder="例如 zju2001" />
       </div>
       <div class="data-row">
-        <span class="data-label">邮箱</span>
+        <span class="data-label">注册邮箱</span>
         <div class="data-inline">
-          <input type="text" id="input-email" class="data-input" placeholder="粘贴 DuckDuckGo 邮箱" />
+          <input type="text" id="input-email" class="data-input" placeholder="自动生成或手动粘贴邮箱" />
           <button id="btn-fetch-email" class="btn btn-outline btn-sm" type="button">获取</button>
         </div>
       </div>
-      <div class="data-row">
-        <span class="data-label">密码</span>
-        <div class="data-inline">
-          <div class="input-with-icon">
-            <input type="password" id="input-password" class="data-input data-input-with-icon" placeholder="留空则自动生成" />
-            <button id="btn-toggle-password" class="input-icon-btn" type="button" aria-label="显示密码" title="显示密码"></button>
-          </div>
-          <button id="btn-save-settings" class="btn btn-outline btn-sm" type="button">保存</button>
-        </div>
-      </div>
       <div class="data-row data-check-row">
         <span class="data-label">兜底</span>
         <label class="data-check" for="input-auto-skip-failures">
@@ -104,16 +143,8 @@
             <span>启动前倒计时</span>
           </label>
           <div class="auto-delay-controls">
-            <input
-              type="number"
-              id="input-auto-delay-minutes"
-              class="data-input auto-delay-input"
-              value="30"
-              min="1"
-              max="1440"
-              step="1"
-              title="启动前倒计时分钟数"
-            />
+            <input type="number" id="input-auto-delay-minutes" class="data-input auto-delay-input" value="30" min="1"
+              max="1440" step="1" title="启动前倒计时分钟数" />
             <span class="data-unit">分钟</span>
           </div>
         </div>
@@ -132,14 +163,18 @@
       <span id="display-status">就绪</span>
     </div>
     <div id="auto-continue-bar" class="auto-continue-bar" style="display:none;">
-      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
-      <span class="auto-hint">先自动获取 Duck 邮箱,或手动粘贴邮箱后再继续</span>
+      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+        <circle cx="12" cy="12" r="10" />
+        <line x1="12" y1="8" x2="12" y2="12" />
+        <line x1="12" y1="16" x2="12.01" y2="16" />
+      </svg>
+      <span class="auto-hint">先自动获取邮箱,或手动粘贴邮箱后再继续</span>
       <button id="btn-auto-continue" class="btn btn-primary btn-sm">继续</button>
     </div>
     <div id="auto-schedule-bar" class="auto-schedule-bar" style="display:none;">
       <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-        <circle cx="12" cy="12" r="10"/>
-        <polyline points="12 6 12 12 16 14"/>
+        <circle cx="12" cy="12" r="10" />
+        <polyline points="12 6 12 12 16 14" />
       </svg>
       <div class="auto-schedule-copy">
         <span id="auto-schedule-title" class="auto-schedule-title">已计划自动运行</span>
@@ -232,4 +267,5 @@
   <div id="toast-container"></div>
   <script src="sidepanel.js"></script>
 </body>
+
 </html>

+ 217 - 10
sidepanel/sidepanel.js

@@ -36,10 +36,15 @@ const btnClearLog = document.getElementById('btn-clear-log');
 const inputVpsUrl = document.getElementById('input-vps-url');
 const inputVpsPassword = document.getElementById('input-vps-password');
 const selectMailProvider = document.getElementById('select-mail-provider');
+const selectEmailGenerator = document.getElementById('select-email-generator');
 const rowInbucketHost = document.getElementById('row-inbucket-host');
 const inputInbucketHost = document.getElementById('input-inbucket-host');
 const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
 const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
+const rowCfDomain = document.getElementById('row-cf-domain');
+const selectCfDomain = document.getElementById('select-cf-domain');
+const inputCfDomain = document.getElementById('input-cf-domain');
+const btnCfDomainMode = document.getElementById('btn-cf-domain-mode');
 const inputRunCount = document.getElementById('input-run-count');
 const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
 const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled');
@@ -79,6 +84,7 @@ let currentAutoRun = {
 let settingsDirty = false;
 let settingsSaveInFlight = false;
 let settingsAutoSaveTimer = null;
+let cloudflareDomainEditMode = false;
 let modalChoiceResolver = null;
 let currentModalActions = [];
 let scheduledCountdownTimer = null;
@@ -367,14 +373,96 @@ function setDefaultAutoRunButton() {
   btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> 自动';
 }
 
+function normalizeCloudflareDomainValue(value = '') {
+  let normalized = String(value || '').trim().toLowerCase();
+  if (!normalized) return '';
+  normalized = normalized.replace(/^@+/, '');
+  normalized = normalized.replace(/^https?:\/\//, '');
+  normalized = normalized.replace(/\/.*$/, '');
+  if (!/^[a-z0-9.-]+\.[a-z]{2,}$/.test(normalized)) {
+    return '';
+  }
+  return normalized;
+}
+
+function normalizeCloudflareDomains(values = []) {
+  const seen = new Set();
+  const domains = [];
+  for (const value of Array.isArray(values) ? values : []) {
+    const normalized = normalizeCloudflareDomainValue(value);
+    if (!normalized || seen.has(normalized)) continue;
+    seen.add(normalized);
+    domains.push(normalized);
+  }
+  return domains;
+}
+
+function getCloudflareDomainsFromState() {
+  const domains = normalizeCloudflareDomains(latestState?.cloudflareDomains || []);
+  const activeDomain = normalizeCloudflareDomainValue(latestState?.cloudflareDomain || '');
+  if (activeDomain && !domains.includes(activeDomain)) {
+    domains.unshift(activeDomain);
+  }
+  return { domains, activeDomain: activeDomain || domains[0] || '' };
+}
+
+function renderCloudflareDomainOptions(preferredDomain = '') {
+  const preferred = normalizeCloudflareDomainValue(preferredDomain);
+  const { domains, activeDomain } = getCloudflareDomainsFromState();
+  const selected = preferred || activeDomain;
+
+  selectCfDomain.innerHTML = '';
+  if (domains.length === 0) {
+    const option = document.createElement('option');
+    option.value = '';
+    option.textContent = '请先添加域名';
+    selectCfDomain.appendChild(option);
+    selectCfDomain.disabled = true;
+    selectCfDomain.value = '';
+    return;
+  }
+
+  for (const domain of domains) {
+    const option = document.createElement('option');
+    option.value = domain;
+    option.textContent = domain;
+    selectCfDomain.appendChild(option);
+  }
+  selectCfDomain.disabled = false;
+  selectCfDomain.value = domains.includes(selected) ? selected : domains[0];
+}
+
+function setCloudflareDomainEditMode(editing, options = {}) {
+  const { clearInput = false } = options;
+  cloudflareDomainEditMode = Boolean(editing);
+  selectCfDomain.style.display = cloudflareDomainEditMode ? 'none' : '';
+  inputCfDomain.style.display = cloudflareDomainEditMode ? '' : 'none';
+  btnCfDomainMode.textContent = cloudflareDomainEditMode ? '保存' : '添加';
+  if (cloudflareDomainEditMode) {
+    if (clearInput) {
+      inputCfDomain.value = '';
+    }
+    inputCfDomain.focus();
+  } else if (clearInput) {
+    inputCfDomain.value = '';
+  }
+}
+
 function collectSettingsPayload() {
+  const { domains, activeDomain } = getCloudflareDomainsFromState();
+  const selectedCloudflareDomain = normalizeCloudflareDomainValue(
+    !cloudflareDomainEditMode ? selectCfDomain.value : activeDomain
+  ) || activeDomain;
   return {
     vpsUrl: inputVpsUrl.value.trim(),
     vpsPassword: inputVpsPassword.value,
     customPassword: inputPassword.value,
     mailProvider: selectMailProvider.value,
+    emailGenerator: selectEmailGenerator.value,
     inbucketHost: inputInbucketHost.value.trim(),
     inbucketMailbox: inputInbucketMailbox.value.trim(),
+    cloudflareDomain: selectedCloudflareDomain,
+    cloudflareDomains: domains,
     autoRunSkipFailures: inputAutoSkipFailures.checked,
     autoRunDelayEnabled: inputAutoDelayEnabled.checked,
     autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value),
@@ -551,12 +639,17 @@ async function restoreState() {
     if (state.mailProvider) {
       selectMailProvider.value = state.mailProvider;
     }
+    if (state.emailGenerator) {
+      selectEmailGenerator.value = state.emailGenerator;
+    }
     if (state.inbucketHost) {
       inputInbucketHost.value = state.inbucketHost;
     }
     if (state.inbucketMailbox) {
       inputInbucketMailbox.value = state.inbucketMailbox;
     }
+    renderCloudflareDomainOptions(state.cloudflareDomain || '');
+    setCloudflareDomainEditMode(false, { clearInput: true });
     inputAutoSkipFailures.checked = Boolean(state.autoRunSkipFailures);
     inputAutoDelayEnabled.checked = Boolean(state.autoRunDelayEnabled);
     inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state.autoRunDelayMinutes));
@@ -592,10 +685,78 @@ function syncPasswordField(state) {
   inputPassword.value = state.customPassword || state.password || '';
 }
 
+function getSelectedEmailGenerator() {
+  return selectEmailGenerator.value === 'cloudflare' ? 'cloudflare' : 'duck';
+}
+
+function getEmailGeneratorUiCopy() {
+  if (getSelectedEmailGenerator() === 'cloudflare') {
+    return {
+      buttonLabel: '生成 Cloudflare',
+      placeholder: '点击生成 Cloudflare 邮箱,或手动粘贴邮箱',
+      successVerb: '生成',
+      label: 'Cloudflare 邮箱',
+    };
+  }
+
+  return {
+    buttonLabel: '获取 Duck',
+    placeholder: '点击获取 DuckDuckGo 邮箱,或手动粘贴邮箱',
+    successVerb: '获取',
+    label: 'Duck 邮箱',
+  };
+}
+
 function updateMailProviderUI() {
   const useInbucket = selectMailProvider.value === 'inbucket';
   rowInbucketHost.style.display = useInbucket ? '' : 'none';
   rowInbucketMailbox.style.display = useInbucket ? '' : 'none';
+  const useCloudflare = selectEmailGenerator.value === 'cloudflare';
+  rowCfDomain.style.display = useCloudflare ? '' : 'none';
+  const { domains } = getCloudflareDomainsFromState();
+  if (useCloudflare) {
+    setCloudflareDomainEditMode(cloudflareDomainEditMode || domains.length === 0, { clearInput: false });
+  } else {
+    setCloudflareDomainEditMode(false, { clearInput: false });
+  }
+
+  const uiCopy = getEmailGeneratorUiCopy();
+  inputEmail.placeholder = uiCopy.placeholder;
+  if (!btnFetchEmail.disabled) {
+    btnFetchEmail.textContent = uiCopy.buttonLabel;
+  }
+}
+
+async function saveCloudflareDomainSettings(domains, activeDomain, options = {}) {
+  const { silent = false } = options;
+  const normalizedDomains = normalizeCloudflareDomains(domains);
+  const normalizedActiveDomain = normalizeCloudflareDomainValue(activeDomain) || normalizedDomains[0] || '';
+  const payload = {
+    cloudflareDomain: normalizedActiveDomain,
+    cloudflareDomains: normalizedDomains,
+  };
+
+  const response = await chrome.runtime.sendMessage({
+    type: 'SAVE_SETTING',
+    source: 'sidepanel',
+    payload,
+  });
+
+  if (response?.error) {
+    throw new Error(response.error);
+  }
+
+  syncLatestState({
+    ...payload,
+  });
+  renderCloudflareDomainOptions(normalizedActiveDomain);
+  setCloudflareDomainEditMode(false, { clearInput: true });
+  markSettingsDirty(false);
+  updateMailProviderUI();
+
+  if (!silent) {
+    showToast('Cloudflare 域名已保存', 'success', 1800);
+  }
 }
 
 // ============================================================
@@ -773,32 +934,36 @@ function escapeHtml(text) {
   return div.innerHTML;
 }
 
-async function fetchDuckEmail(options = {}) {
+async function fetchGeneratedEmail(options = {}) {
   const { showFailureToast = true } = options;
-  const defaultLabel = '获取';
+  const uiCopy = getEmailGeneratorUiCopy();
+  const defaultLabel = uiCopy.buttonLabel;
   btnFetchEmail.disabled = true;
   btnFetchEmail.textContent = '...';
 
   try {
     const response = await chrome.runtime.sendMessage({
-      type: 'FETCH_DUCK_EMAIL',
+      type: 'FETCH_GENERATED_EMAIL',
       source: 'sidepanel',
-      payload: { generateNew: true },
+      payload: {
+        generateNew: true,
+        generator: selectEmailGenerator.value,
+      },
     });
 
     if (response?.error) {
       throw new Error(response.error);
     }
     if (!response?.email) {
-      throw new Error('未返回 Duck 邮箱。');
+      throw new Error('未返回可用邮箱。');
     }
 
     inputEmail.value = response.email;
-    showToast(`已获取 ${response.email}`, 'success', 2500);
+    showToast(`已${uiCopy.successVerb} ${uiCopy.label}:${response.email}`, 'success', 2500);
     return response.email;
   } catch (err) {
     if (showFailureToast) {
-      showToast(`自动获取失败:${err.message}`, 'error');
+      showToast(`${uiCopy.label}${uiCopy.successVerb}失败:${err.message}`, 'error');
     }
     throw err;
   } finally {
@@ -897,7 +1062,7 @@ document.querySelectorAll('.step-btn').forEach(btn => {
         let email = inputEmail.value.trim();
         if (!email) {
           try {
-            email = await fetchDuckEmail({ showFailureToast: false });
+            email = await fetchGeneratedEmail({ showFailureToast: false });
           } catch (err) {
             showToast(`自动获取失败:${err.message},请手动粘贴邮箱后重试。`, 'warn');
             return;
@@ -920,7 +1085,7 @@ document.querySelectorAll('.step-btn').forEach(btn => {
 });
 
 btnFetchEmail.addEventListener('click', async () => {
-  await fetchDuckEmail().catch(() => { });
+  await fetchGeneratedEmail().catch(() => { });
 });
 
 btnTogglePassword.addEventListener('click', () => {
@@ -1000,7 +1165,7 @@ btnAutoRun.addEventListener('click', async () => {
 btnAutoContinue.addEventListener('click', async () => {
   const email = inputEmail.value.trim();
   if (!email) {
-    showToast('请先获取或粘贴 DuckDuckGo 邮箱。', 'warn');
+    showToast('请先获取或粘贴邮箱。', 'warn');
     return;
   }
   autoContinueBar.style.display = 'none';
@@ -1107,6 +1272,48 @@ selectMailProvider.addEventListener('change', () => {
   saveSettings({ silent: true }).catch(() => { });
 });
 
+selectEmailGenerator.addEventListener('change', () => {
+  updateMailProviderUI();
+  markSettingsDirty(true);
+  saveSettings({ silent: true }).catch(() => { });
+});
+
+selectCfDomain.addEventListener('change', () => {
+  if (selectCfDomain.disabled) {
+    return;
+  }
+  markSettingsDirty(true);
+  saveSettings({ silent: true }).catch(() => { });
+});
+
+btnCfDomainMode.addEventListener('click', async () => {
+  try {
+    if (!cloudflareDomainEditMode) {
+      setCloudflareDomainEditMode(true, { clearInput: true });
+      return;
+    }
+
+    const newDomain = normalizeCloudflareDomainValue(inputCfDomain.value);
+    if (!newDomain) {
+      showToast('请输入有效的 Cloudflare 域名。', 'warn');
+      inputCfDomain.focus();
+      return;
+    }
+
+    const { domains } = getCloudflareDomainsFromState();
+    await saveCloudflareDomainSettings([...domains, newDomain], newDomain);
+  } catch (err) {
+    showToast(err.message, 'error');
+  }
+});
+
+inputCfDomain.addEventListener('keydown', (event) => {
+  if (event.key === 'Enter') {
+    event.preventDefault();
+    btnCfDomainMode.click();
+  }
+});
+
 inputInbucketMailbox.addEventListener('input', () => {
   markSettingsDirty(true);
   scheduleSettingsAutoSave();