添加Cloudflare服务

This commit is contained in:
QLHazyCoder
2026-04-12 19:30:35 +08:00
parent 62664ee862
commit 4228888cca
6 changed files with 508 additions and 84 deletions
+53 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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 地址生成失败。');
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "多页面自动化",
"version": "1.1.0",
"version": "5.0.0",
"description": "用于自动执行多步骤 OAuth 注册流程",
"permissions": [
"sidePanel",
+74 -38
View File
@@ -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
View File
@@ -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();