添加Cloudflare服务
This commit is contained in:
@@ -44,6 +44,7 @@
|
|||||||
- 自动获取注册验证码与登录验证码
|
- 自动获取注册验证码与登录验证码
|
||||||
- 支持 `QQ Mail`、`163 Mail`、`Inbucket mailbox`
|
- 支持 `QQ Mail`、`163 Mail`、`Inbucket mailbox`
|
||||||
- 支持从 DuckDuckGo Email Protection 自动生成新的 `@duck.com` 地址
|
- 支持从 DuckDuckGo Email Protection 自动生成新的 `@duck.com` 地址
|
||||||
|
- 支持基于 Cloudflare 自定义域名自动生成随机邮箱前缀
|
||||||
- Step 5 同时兼容两种页面:
|
- Step 5 同时兼容两种页面:
|
||||||
- 页面要求填写 `birthday`
|
- 页面要求填写 `birthday`
|
||||||
- 页面要求填写 `age`
|
- 页面要求填写 `age`
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
- 你自己的 CPA 管理面板,且页面结构与当前脚本适配
|
- 你自己的 CPA 管理面板,且页面结构与当前脚本适配
|
||||||
- 至少准备一种验证码接收方式:
|
- 至少准备一种验证码接收方式:
|
||||||
- DuckDuckGo `@duck.com` + QQ / 163 / Inbucket 转发
|
- DuckDuckGo `@duck.com` + QQ / 163 / Inbucket 转发
|
||||||
|
- Cloudflare 自定义域邮箱前缀 + QQ / 163 / Inbucket 转发
|
||||||
- 手动填写一个可收信邮箱
|
- 手动填写一个可收信邮箱
|
||||||
- 如果使用 `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 地址
|
- 如果你使用 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`
|
### `Password`
|
||||||
|
|
||||||
- 留空:自动生成强密码
|
- 留空:自动生成强密码
|
||||||
@@ -186,14 +230,14 @@ Step 3 使用的注册邮箱。
|
|||||||
|
|
||||||
1. Step 1 获取 CPA OAuth 链接
|
1. Step 1 获取 CPA OAuth 链接
|
||||||
2. Step 2 打开 OpenAI 注册页
|
2. Step 2 打开 OpenAI 注册页
|
||||||
3. 尝试自动获取 Duck 邮箱
|
3. 按当前“邮箱生成”配置尝试自动获取邮箱(Duck 或 Cloudflare)
|
||||||
4. 如果 Duck 自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue`
|
4. 如果自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue`
|
||||||
5. 继续执行 Step 3 ~ Step 9
|
5. 继续执行 Step 3 ~ Step 9
|
||||||
|
|
||||||
也就是说:
|
也就是说:
|
||||||
|
|
||||||
- 如果 Duck 邮箱可自动获取,整套流程更接近全自动
|
- 如果邮箱可自动获取,整套流程更接近全自动
|
||||||
- 如果 Duck 自动获取失败,后台会先自动重试 5 次;仍失败时,Auto 才会在邮箱阶段暂停
|
- 如果自动获取失败,后台会先自动重试 5 次;仍失败时,Auto 才会在邮箱阶段暂停
|
||||||
- Auto 的暂停状态会保存在会话状态中,重新打开侧边栏后仍可继续
|
- Auto 的暂停状态会保存在会话状态中,重新打开侧边栏后仍可继续
|
||||||
- 如果你在 Auto 暂停时改为手动点步骤或跳过步骤,面板会先确认并停止 Auto,再切回手动控制
|
- 如果你在 Auto 暂停时改为手动点步骤或跳过步骤,面板会先确认并停止 Auto,再切回手动控制
|
||||||
- 选择 `继续当前` 时,后台不会先做大而全的前置校验,而是从当前步骤状态直接继续;缺什么条件,就在运行到那一步时再报错或暂停
|
- 选择 `继续当前` 时,后台不会先做大而全的前置校验,而是从当前步骤状态直接继续;缺什么条件,就在运行到那一步时再报错或暂停
|
||||||
@@ -221,7 +265,7 @@ Step 3 使用的注册邮箱。
|
|||||||
|
|
||||||
### Step 3: Fill Email / Password
|
### Step 3: Fill Email / Password
|
||||||
|
|
||||||
- 如果侧边栏邮箱为空,会先尝试自动获取 DuckDuckGo 邮箱;失败时再提示手动粘贴
|
- 如果侧边栏邮箱为空,会先按当前“邮箱生成”配置自动获取邮箱;失败时再提示手动粘贴
|
||||||
- 自动填写邮箱
|
- 自动填写邮箱
|
||||||
- 如页面先要求邮箱,再进入密码页,会自动切页继续填写
|
- 如页面先要求邮箱,再进入密码页,会自动切页继续填写
|
||||||
- 使用自定义密码或自动生成密码
|
- 使用自定义密码或自动生成密码
|
||||||
@@ -405,9 +449,9 @@ sidepanel/ 侧边栏 UI
|
|||||||
- 给脚本准备一个相对独立的 mailbox
|
- 给脚本准备一个相对独立的 mailbox
|
||||||
- 避免收件箱里混入过多无关邮件
|
- 避免收件箱里混入过多无关邮件
|
||||||
|
|
||||||
### 3. Duck 自动获取失败时直接手填
|
### 3. 自动获取失败时直接手填
|
||||||
|
|
||||||
如果 Duck 页面打不开、未登录或按钮变化:
|
如果 Duck 页面打不开、Cloudflare 域名未配置、未登录或按钮变化:
|
||||||
|
|
||||||
- 直接在 `Email` 输入框中粘贴邮箱
|
- 直接在 `Email` 输入框中粘贴邮箱
|
||||||
- 手动点 `Step 3` 时,如果邮箱为空,脚本会先自动尝试获取 Duck 邮箱;失败后再改为手填
|
- 手动点 `Step 3` 时,如果邮箱为空,脚本会先自动尝试获取 Duck 邮箱;失败后再改为手填
|
||||||
|
|||||||
+108
-16
@@ -25,9 +25,12 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。
|
autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。
|
||||||
autoRunDelayEnabled: false, // 自动运行是否启用启动前倒计时。
|
autoRunDelayEnabled: false, // 自动运行是否启用启动前倒计时。
|
||||||
autoRunDelayMinutes: 30, // 自动运行倒计时分钟数。
|
autoRunDelayMinutes: 30, // 自动运行倒计时分钟数。
|
||||||
mailProvider: '163', // 验证码邮箱来源,当前支持 163 / inbucket。
|
mailProvider: '163', // 验证码邮箱来源(163 / 163-vip / qq / inbucket)。
|
||||||
|
emailGenerator: 'duck', // 注册邮箱生成方式:duck / cloudflare。
|
||||||
inbucketHost: '', // 仅当 mailProvider 为 inbucket 时填写 Inbucket 地址,其他情况保持为空。
|
inbucketHost: '', // 仅当 mailProvider 为 inbucket 时填写 Inbucket 地址,其他情况保持为空。
|
||||||
inbucketMailbox: '', // 仅当 mailProvider 为 inbucket 时填写邮箱名,其他情况保持为空。
|
inbucketMailbox: '', // 仅当 mailProvider 为 inbucket 时填写邮箱名,其他情况保持为空。
|
||||||
|
cloudflareDomain: '', // 仅当 emailGenerator=cloudflare 时填写自定义域名。
|
||||||
|
cloudflareDomains: [], // Cloudflare 可选域名列表。
|
||||||
};
|
};
|
||||||
|
|
||||||
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
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() {
|
async function getPersistedSettings() {
|
||||||
const stored = await chrome.storage.local.get(PERSISTED_SETTING_KEYS);
|
const stored = await chrome.storage.local.get(PERSISTED_SETTING_KEYS);
|
||||||
return {
|
return {
|
||||||
@@ -99,6 +116,7 @@ async function getPersistedSettings() {
|
|||||||
autoRunSkipFailures: Boolean(stored.autoRunSkipFailures ?? PERSISTED_SETTING_DEFAULTS.autoRunSkipFailures),
|
autoRunSkipFailures: Boolean(stored.autoRunSkipFailures ?? PERSISTED_SETTING_DEFAULTS.autoRunSkipFailures),
|
||||||
autoRunDelayEnabled: Boolean(stored.autoRunDelayEnabled ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayEnabled),
|
autoRunDelayEnabled: Boolean(stored.autoRunDelayEnabled ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayEnabled),
|
||||||
autoRunDelayMinutes: normalizeAutoRunDelayMinutes(stored.autoRunDelayMinutes ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayMinutes),
|
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) {
|
async function setState(updates) {
|
||||||
console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
|
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) {
|
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.autoRunDelayEnabled !== undefined) updates.autoRunDelayEnabled = Boolean(message.payload.autoRunDelayEnabled);
|
||||||
if (message.payload.autoRunDelayMinutes !== undefined) updates.autoRunDelayMinutes = normalizeAutoRunDelayMinutes(message.payload.autoRunDelayMinutes);
|
if (message.payload.autoRunDelayMinutes !== undefined) updates.autoRunDelayMinutes = normalizeAutoRunDelayMinutes(message.payload.autoRunDelayMinutes);
|
||||||
if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
|
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.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
|
||||||
if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
|
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 setPersistentSettings(updates);
|
||||||
await setState(updates);
|
await setState(updates);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
@@ -1739,13 +1764,24 @@ async function handleMessage(message, sender) {
|
|||||||
return { ok: true, email: message.payload.email };
|
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': {
|
case 'FETCH_DUCK_EMAIL': {
|
||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
if (isAutoRunLockedState(state)) {
|
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();
|
await resumeAutoRun();
|
||||||
return { ok: true, email };
|
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 = {}) {
|
async function fetchDuckEmail(options = {}) {
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
const { generateNew = true } = options;
|
const { generateNew = true } = options;
|
||||||
@@ -2016,6 +2086,15 @@ async function fetchDuckEmail(options = {}) {
|
|||||||
return result.email;
|
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
|
// Auto Run Flow
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -2024,7 +2103,7 @@ let autoRunActive = false;
|
|||||||
let autoRunCurrentRun = 0;
|
let autoRunCurrentRun = 0;
|
||||||
let autoRunTotalRuns = 1;
|
let autoRunTotalRuns = 1;
|
||||||
let autoRunAttemptRun = 0;
|
let autoRunAttemptRun = 0;
|
||||||
const DUCK_EMAIL_MAX_ATTEMPTS = 5;
|
const EMAIL_FETCH_MAX_ATTEMPTS = 5;
|
||||||
const VERIFICATION_POLL_MAX_ROUNDS = 5;
|
const VERIFICATION_POLL_MAX_ROUNDS = 5;
|
||||||
const AUTO_STEP_DELAYS = {
|
const AUTO_STEP_DELAYS = {
|
||||||
1: 2000,
|
1: 2000,
|
||||||
@@ -2063,23 +2142,31 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
|
|||||||
return currentState.email;
|
return currentState.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastDuckError = null;
|
const generator = normalizeEmailGenerator(currentState.emailGenerator);
|
||||||
for (let duckAttempt = 1; duckAttempt <= DUCK_EMAIL_MAX_ATTEMPTS; duckAttempt++) {
|
const generatorLabel = getEmailGeneratorLabel(generator);
|
||||||
|
let lastError = null;
|
||||||
|
for (let attempt = 1; attempt <= EMAIL_FETCH_MAX_ATTEMPTS; attempt++) {
|
||||||
try {
|
try {
|
||||||
if (duckAttempt > 1) {
|
if (attempt > 1) {
|
||||||
await addLog(`Duck 邮箱:正在进行第 ${duckAttempt}/${DUCK_EMAIL_MAX_ATTEMPTS} 次自动获取重试...`, 'warn');
|
await addLog(`${generatorLabel}:正在进行第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次自动获取重试...`, 'warn');
|
||||||
}
|
}
|
||||||
const duckEmail = await fetchDuckEmail({ generateNew: true });
|
const generatedEmail = await fetchGeneratedEmail(currentState, { generateNew: true, generator });
|
||||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:Duck 邮箱已就绪:${duckEmail}(第 ${attemptRuns} 次尝试,Duck 第 ${duckAttempt}/${DUCK_EMAIL_MAX_ATTEMPTS} 次获取)===`, 'ok');
|
await addLog(
|
||||||
return duckEmail;
|
`=== 目标 ${targetRun}/${totalRuns} 轮:${generatorLabel}已就绪:${generatedEmail}(第 ${attemptRuns} 次尝试,第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次获取)===`,
|
||||||
|
'ok'
|
||||||
|
);
|
||||||
|
return generatedEmail;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
lastDuckError = err;
|
lastError = err;
|
||||||
await addLog(`Duck 邮箱自动获取失败(${duckAttempt}/${DUCK_EMAIL_MAX_ATTEMPTS}):${err.message}`, 'warn');
|
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(`${generatorLabel}自动获取已连续失败 ${EMAIL_FETCH_MAX_ATTEMPTS} 次:${lastError?.message || '未知错误'}`, 'error');
|
||||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先获取 Duck 邮箱或手动粘贴邮箱,然后继续 ===`, 'warn');
|
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先自动获取邮箱或手动粘贴邮箱,然后继续 ===`, 'warn');
|
||||||
await broadcastAutoRunStatus('waiting_email', {
|
await broadcastAutoRunStatus('waiting_email', {
|
||||||
currentRun: targetRun,
|
currentRun: targetRun,
|
||||||
totalRuns,
|
totalRuns,
|
||||||
@@ -2215,9 +2302,14 @@ async function autoRunLoop(totalRuns, options = {}) {
|
|||||||
vpsPassword: prevState.vpsPassword,
|
vpsPassword: prevState.vpsPassword,
|
||||||
customPassword: prevState.customPassword,
|
customPassword: prevState.customPassword,
|
||||||
autoRunSkipFailures: prevState.autoRunSkipFailures,
|
autoRunSkipFailures: prevState.autoRunSkipFailures,
|
||||||
|
autoRunDelayEnabled: prevState.autoRunDelayEnabled,
|
||||||
|
autoRunDelayMinutes: prevState.autoRunDelayMinutes,
|
||||||
mailProvider: prevState.mailProvider,
|
mailProvider: prevState.mailProvider,
|
||||||
|
emailGenerator: prevState.emailGenerator,
|
||||||
inbucketHost: prevState.inbucketHost,
|
inbucketHost: prevState.inbucketHost,
|
||||||
inbucketMailbox: prevState.inbucketMailbox,
|
inbucketMailbox: prevState.inbucketMailbox,
|
||||||
|
cloudflareDomain: prevState.cloudflareDomain,
|
||||||
|
cloudflareDomains: prevState.cloudflareDomains,
|
||||||
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns }),
|
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns }),
|
||||||
...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}),
|
...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}),
|
||||||
};
|
};
|
||||||
|
|||||||
+55
-10
@@ -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);
|
console.log('[MultiPage:duck-mail] Content script loaded on', location.href);
|
||||||
|
|
||||||
@@ -30,9 +30,35 @@ async function fetchDuckEmail(payload = {}) {
|
|||||||
15000
|
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 getAddressInput = () => document.querySelector('input.AutofillSettingsPanel__PrivateDuckAddressValue');
|
||||||
const getGeneratorButton = () => document.querySelector('button.AutofillSettingsPanel__GeneratorButton')
|
const getGeneratorButton = () => {
|
||||||
|| Array.from(document.querySelectorAll('button')).find(btn => /generate private duck address/i.test(btn.textContent || ''));
|
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 readEmail = () => {
|
||||||
const value = getAddressInput()?.value?.trim() || '';
|
const value = getAddressInput()?.value?.trim() || '';
|
||||||
return value.includes('@duck.com') ? value : '';
|
return value.includes('@duck.com') ? value : '';
|
||||||
@@ -55,20 +81,39 @@ async function fetchDuckEmail(payload = {}) {
|
|||||||
return { email: currentEmail, generated: false };
|
return { email: currentEmail, generated: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
await humanPause(500, 1300);
|
|
||||||
const generatorButton = getGeneratorButton();
|
const generatorButton = getGeneratorButton();
|
||||||
if (!generatorButton) {
|
if (!generatorButton) {
|
||||||
|
if (generateNew) {
|
||||||
|
throw new Error('未找到“生成新 Duck 地址”按钮(可能是页面文案/语言变化、未登录或页面结构更新)。');
|
||||||
|
}
|
||||||
if (currentEmail) {
|
if (currentEmail) {
|
||||||
log(`Duck 邮箱:正在复用现有地址 ${currentEmail}`, 'warn');
|
log(`Duck 邮箱:未找到生成按钮,复用现有地址 ${currentEmail}`, 'warn');
|
||||||
return { email: currentEmail, generated: false };
|
return { email: currentEmail, generated: false };
|
||||||
}
|
}
|
||||||
throw new Error('未找到“生成 Duck 私有地址”按钮。');
|
throw new Error('未找到“生成 Duck 私有地址”按钮。');
|
||||||
}
|
}
|
||||||
|
|
||||||
generatorButton.click();
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||||
log('Duck 邮箱:已点击“生成 Duck 私有地址”按钮');
|
await humanPause(500, 1300);
|
||||||
|
if (typeof simulateClick === 'function') {
|
||||||
|
simulateClick(generatorButton);
|
||||||
|
} else {
|
||||||
|
generatorButton.click();
|
||||||
|
}
|
||||||
|
log(`Duck 邮箱:已点击“生成 Duck 私有地址”按钮(${attempt}/2)`);
|
||||||
|
|
||||||
const nextEmail = await waitForEmailValue(currentEmail);
|
try {
|
||||||
log(`Duck 邮箱:地址已就绪 ${nextEmail}`, 'ok');
|
const nextEmail = await waitForEmailValue(currentEmail);
|
||||||
return { email: nextEmail, generated: true };
|
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
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "多页面自动化",
|
"name": "多页面自动化",
|
||||||
"version": "1.1.0",
|
"version": "5.0.0",
|
||||||
"description": "用于自动执行多步骤 OAuth 注册流程",
|
"description": "用于自动执行多步骤 OAuth 注册流程",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"sidePanel",
|
"sidePanel",
|
||||||
|
|||||||
+74
-38
@@ -1,19 +1,24 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>多页面自动化面板</title>
|
<title>多页面自动化面板</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<link rel="stylesheet" href="sidepanel.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div class="header-left">
|
<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">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
|
||||||
</svg>
|
</svg>
|
||||||
<h1>多页面</h1>
|
<h1>多页面</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,22 +26,36 @@
|
|||||||
<div class="run-group">
|
<div class="run-group">
|
||||||
<input type="number" id="input-run-count" class="run-count-input" value="1" min="1" max="50" title="运行次数" />
|
<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="自动执行全部步骤">
|
<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>
|
||||||
<button id="btn-stop" class="btn btn-danger" title="停止当前流程" disabled>停止</button>
|
<button id="btn-stop" class="btn btn-danger" title="停止当前流程" disabled>停止</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="btn-reset" class="btn btn-ghost" title="重置全部步骤">
|
<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">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
|
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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="btn-theme" class="theme-toggle" title="切换主题">
|
<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">
|
<svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
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>
|
||||||
<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">
|
<svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
<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"/>
|
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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,14 +66,28 @@
|
|||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">CPA</span>
|
<span class="data-label">CPA</span>
|
||||||
<div class="input-with-icon">
|
<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" />
|
<input type="password" id="input-vps-url" class="data-input data-input-with-icon"
|
||||||
<button id="btn-toggle-vps-url" class="input-icon-btn" type="button" aria-label="显示 CPA 地址" title="显示 CPA 地址"></button>
|
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>
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">管理密钥</span>
|
<span class="data-label">管理密钥</span>
|
||||||
<input type="password" id="input-vps-password" class="data-input" placeholder="请输入 CPA 管理密钥" />
|
<input type="password" id="input-vps-password" class="data-input" placeholder="请输入 CPA 管理密钥" />
|
||||||
</div>
|
</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">
|
<div class="data-row">
|
||||||
<span class="data-label">邮箱服务</span>
|
<span class="data-label">邮箱服务</span>
|
||||||
<select id="select-mail-provider" class="data-select">
|
<select id="select-mail-provider" class="data-select">
|
||||||
@@ -64,6 +97,22 @@
|
|||||||
<option value="inbucket">Inbucket(自定义主机)</option>
|
<option value="inbucket">Inbucket(自定义主机)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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;">
|
<div class="data-row" id="row-inbucket-host" style="display:none;">
|
||||||
<span class="data-label">Inbucket</span>
|
<span class="data-label">Inbucket</span>
|
||||||
<input type="text" id="input-inbucket-host" class="data-input" placeholder="填写主机或 https://主机地址" />
|
<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" />
|
<input type="text" id="input-inbucket-mailbox" class="data-input" placeholder="例如 zju2001" />
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">邮箱</span>
|
<span class="data-label">注册邮箱</span>
|
||||||
<div class="data-inline">
|
<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>
|
<button id="btn-fetch-email" class="btn btn-outline btn-sm" type="button">获取</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="data-row data-check-row">
|
||||||
<span class="data-label">兜底</span>
|
<span class="data-label">兜底</span>
|
||||||
<label class="data-check" for="input-auto-skip-failures">
|
<label class="data-check" for="input-auto-skip-failures">
|
||||||
@@ -104,16 +143,8 @@
|
|||||||
<span>启动前倒计时</span>
|
<span>启动前倒计时</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="auto-delay-controls">
|
<div class="auto-delay-controls">
|
||||||
<input
|
<input type="number" id="input-auto-delay-minutes" class="data-input auto-delay-input" value="30" min="1"
|
||||||
type="number"
|
max="1440" step="1" title="启动前倒计时分钟数" />
|
||||||
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>
|
<span class="data-unit">分钟</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,14 +163,18 @@
|
|||||||
<span id="display-status">就绪</span>
|
<span id="display-status">就绪</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="auto-continue-bar" class="auto-continue-bar" style="display:none;">
|
<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>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<span class="auto-hint">先自动获取 Duck 邮箱,或手动粘贴邮箱后再继续</span>
|
<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>
|
<button id="btn-auto-continue" class="btn btn-primary btn-sm">继续</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="auto-schedule-bar" class="auto-schedule-bar" style="display:none;">
|
<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">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="10"/>
|
<circle cx="12" cy="12" r="10" />
|
||||||
<polyline points="12 6 12 12 16 14"/>
|
<polyline points="12 6 12 12 16 14" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="auto-schedule-copy">
|
<div class="auto-schedule-copy">
|
||||||
<span id="auto-schedule-title" class="auto-schedule-title">已计划自动运行</span>
|
<span id="auto-schedule-title" class="auto-schedule-title">已计划自动运行</span>
|
||||||
@@ -232,4 +267,5 @@
|
|||||||
<div id="toast-container"></div>
|
<div id="toast-container"></div>
|
||||||
<script src="sidepanel.js"></script>
|
<script src="sidepanel.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+217
-10
@@ -36,10 +36,15 @@ const btnClearLog = document.getElementById('btn-clear-log');
|
|||||||
const inputVpsUrl = document.getElementById('input-vps-url');
|
const inputVpsUrl = document.getElementById('input-vps-url');
|
||||||
const inputVpsPassword = document.getElementById('input-vps-password');
|
const inputVpsPassword = document.getElementById('input-vps-password');
|
||||||
const selectMailProvider = document.getElementById('select-mail-provider');
|
const selectMailProvider = document.getElementById('select-mail-provider');
|
||||||
|
const selectEmailGenerator = document.getElementById('select-email-generator');
|
||||||
const rowInbucketHost = document.getElementById('row-inbucket-host');
|
const rowInbucketHost = document.getElementById('row-inbucket-host');
|
||||||
const inputInbucketHost = document.getElementById('input-inbucket-host');
|
const inputInbucketHost = document.getElementById('input-inbucket-host');
|
||||||
const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
|
const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
|
||||||
const inputInbucketMailbox = document.getElementById('input-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 inputRunCount = document.getElementById('input-run-count');
|
||||||
const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
|
const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
|
||||||
const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled');
|
const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled');
|
||||||
@@ -79,6 +84,7 @@ let currentAutoRun = {
|
|||||||
let settingsDirty = false;
|
let settingsDirty = false;
|
||||||
let settingsSaveInFlight = false;
|
let settingsSaveInFlight = false;
|
||||||
let settingsAutoSaveTimer = null;
|
let settingsAutoSaveTimer = null;
|
||||||
|
let cloudflareDomainEditMode = false;
|
||||||
let modalChoiceResolver = null;
|
let modalChoiceResolver = null;
|
||||||
let currentModalActions = [];
|
let currentModalActions = [];
|
||||||
let scheduledCountdownTimer = null;
|
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> 自动';
|
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() {
|
function collectSettingsPayload() {
|
||||||
|
const { domains, activeDomain } = getCloudflareDomainsFromState();
|
||||||
|
const selectedCloudflareDomain = normalizeCloudflareDomainValue(
|
||||||
|
!cloudflareDomainEditMode ? selectCfDomain.value : activeDomain
|
||||||
|
) || activeDomain;
|
||||||
return {
|
return {
|
||||||
vpsUrl: inputVpsUrl.value.trim(),
|
vpsUrl: inputVpsUrl.value.trim(),
|
||||||
vpsPassword: inputVpsPassword.value,
|
vpsPassword: inputVpsPassword.value,
|
||||||
customPassword: inputPassword.value,
|
customPassword: inputPassword.value,
|
||||||
mailProvider: selectMailProvider.value,
|
mailProvider: selectMailProvider.value,
|
||||||
|
emailGenerator: selectEmailGenerator.value,
|
||||||
inbucketHost: inputInbucketHost.value.trim(),
|
inbucketHost: inputInbucketHost.value.trim(),
|
||||||
inbucketMailbox: inputInbucketMailbox.value.trim(),
|
inbucketMailbox: inputInbucketMailbox.value.trim(),
|
||||||
|
cloudflareDomain: selectedCloudflareDomain,
|
||||||
|
cloudflareDomains: domains,
|
||||||
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
||||||
autoRunDelayEnabled: inputAutoDelayEnabled.checked,
|
autoRunDelayEnabled: inputAutoDelayEnabled.checked,
|
||||||
autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value),
|
autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value),
|
||||||
@@ -551,12 +639,17 @@ async function restoreState() {
|
|||||||
if (state.mailProvider) {
|
if (state.mailProvider) {
|
||||||
selectMailProvider.value = state.mailProvider;
|
selectMailProvider.value = state.mailProvider;
|
||||||
}
|
}
|
||||||
|
if (state.emailGenerator) {
|
||||||
|
selectEmailGenerator.value = state.emailGenerator;
|
||||||
|
}
|
||||||
if (state.inbucketHost) {
|
if (state.inbucketHost) {
|
||||||
inputInbucketHost.value = state.inbucketHost;
|
inputInbucketHost.value = state.inbucketHost;
|
||||||
}
|
}
|
||||||
if (state.inbucketMailbox) {
|
if (state.inbucketMailbox) {
|
||||||
inputInbucketMailbox.value = state.inbucketMailbox;
|
inputInbucketMailbox.value = state.inbucketMailbox;
|
||||||
}
|
}
|
||||||
|
renderCloudflareDomainOptions(state.cloudflareDomain || '');
|
||||||
|
setCloudflareDomainEditMode(false, { clearInput: true });
|
||||||
inputAutoSkipFailures.checked = Boolean(state.autoRunSkipFailures);
|
inputAutoSkipFailures.checked = Boolean(state.autoRunSkipFailures);
|
||||||
inputAutoDelayEnabled.checked = Boolean(state.autoRunDelayEnabled);
|
inputAutoDelayEnabled.checked = Boolean(state.autoRunDelayEnabled);
|
||||||
inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state.autoRunDelayMinutes));
|
inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state.autoRunDelayMinutes));
|
||||||
@@ -592,10 +685,78 @@ function syncPasswordField(state) {
|
|||||||
inputPassword.value = state.customPassword || state.password || '';
|
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() {
|
function updateMailProviderUI() {
|
||||||
const useInbucket = selectMailProvider.value === 'inbucket';
|
const useInbucket = selectMailProvider.value === 'inbucket';
|
||||||
rowInbucketHost.style.display = useInbucket ? '' : 'none';
|
rowInbucketHost.style.display = useInbucket ? '' : 'none';
|
||||||
rowInbucketMailbox.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;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDuckEmail(options = {}) {
|
async function fetchGeneratedEmail(options = {}) {
|
||||||
const { showFailureToast = true } = options;
|
const { showFailureToast = true } = options;
|
||||||
const defaultLabel = '获取';
|
const uiCopy = getEmailGeneratorUiCopy();
|
||||||
|
const defaultLabel = uiCopy.buttonLabel;
|
||||||
btnFetchEmail.disabled = true;
|
btnFetchEmail.disabled = true;
|
||||||
btnFetchEmail.textContent = '...';
|
btnFetchEmail.textContent = '...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await chrome.runtime.sendMessage({
|
const response = await chrome.runtime.sendMessage({
|
||||||
type: 'FETCH_DUCK_EMAIL',
|
type: 'FETCH_GENERATED_EMAIL',
|
||||||
source: 'sidepanel',
|
source: 'sidepanel',
|
||||||
payload: { generateNew: true },
|
payload: {
|
||||||
|
generateNew: true,
|
||||||
|
generator: selectEmailGenerator.value,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
if (!response?.email) {
|
if (!response?.email) {
|
||||||
throw new Error('未返回 Duck 邮箱。');
|
throw new Error('未返回可用邮箱。');
|
||||||
}
|
}
|
||||||
|
|
||||||
inputEmail.value = response.email;
|
inputEmail.value = response.email;
|
||||||
showToast(`已获取 ${response.email}`, 'success', 2500);
|
showToast(`已${uiCopy.successVerb} ${uiCopy.label}:${response.email}`, 'success', 2500);
|
||||||
return response.email;
|
return response.email;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (showFailureToast) {
|
if (showFailureToast) {
|
||||||
showToast(`自动获取失败:${err.message}`, 'error');
|
showToast(`${uiCopy.label}${uiCopy.successVerb}失败:${err.message}`, 'error');
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -897,7 +1062,7 @@ document.querySelectorAll('.step-btn').forEach(btn => {
|
|||||||
let email = inputEmail.value.trim();
|
let email = inputEmail.value.trim();
|
||||||
if (!email) {
|
if (!email) {
|
||||||
try {
|
try {
|
||||||
email = await fetchDuckEmail({ showFailureToast: false });
|
email = await fetchGeneratedEmail({ showFailureToast: false });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(`自动获取失败:${err.message},请手动粘贴邮箱后重试。`, 'warn');
|
showToast(`自动获取失败:${err.message},请手动粘贴邮箱后重试。`, 'warn');
|
||||||
return;
|
return;
|
||||||
@@ -920,7 +1085,7 @@ document.querySelectorAll('.step-btn').forEach(btn => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
btnFetchEmail.addEventListener('click', async () => {
|
btnFetchEmail.addEventListener('click', async () => {
|
||||||
await fetchDuckEmail().catch(() => { });
|
await fetchGeneratedEmail().catch(() => { });
|
||||||
});
|
});
|
||||||
|
|
||||||
btnTogglePassword.addEventListener('click', () => {
|
btnTogglePassword.addEventListener('click', () => {
|
||||||
@@ -1000,7 +1165,7 @@ btnAutoRun.addEventListener('click', async () => {
|
|||||||
btnAutoContinue.addEventListener('click', async () => {
|
btnAutoContinue.addEventListener('click', async () => {
|
||||||
const email = inputEmail.value.trim();
|
const email = inputEmail.value.trim();
|
||||||
if (!email) {
|
if (!email) {
|
||||||
showToast('请先获取或粘贴 DuckDuckGo 邮箱。', 'warn');
|
showToast('请先获取或粘贴邮箱。', 'warn');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
autoContinueBar.style.display = 'none';
|
autoContinueBar.style.display = 'none';
|
||||||
@@ -1107,6 +1272,48 @@ selectMailProvider.addEventListener('change', () => {
|
|||||||
saveSettings({ silent: true }).catch(() => { });
|
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', () => {
|
inputInbucketMailbox.addEventListener('input', () => {
|
||||||
markSettingsDirty(true);
|
markSettingsDirty(true);
|
||||||
scheduleSettingsAutoSave();
|
scheduleSettingsAutoSave();
|
||||||
|
|||||||
Reference in New Issue
Block a user