From 55c90dd8f7aa54d3bc7cafff7b98f60b33180fb5 Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Sun, 12 Apr 2026 22:00:49 +0800
Subject: [PATCH 1/6] fix(step8): stop debugger click cleanly
---
background.js | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/background.js b/background.js
index f1adbdb..7ad4bad 100644
--- a/background.js
+++ b/background.js
@@ -2211,6 +2211,7 @@ async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY
}
async function clickWithDebugger(tabId, rect) {
+ throwIfStopped();
if (!tabId) {
throw new Error('未找到用于调试点击的认证页面标签页。');
}
@@ -2229,10 +2230,12 @@ async function clickWithDebugger(tabId, rect) {
}
try {
+ throwIfStopped();
const x = Math.round(rect.centerX);
const y = Math.round(rect.centerY);
await chrome.debugger.sendCommand(target, 'Page.bringToFront');
+ throwIfStopped();
await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
type: 'mouseMoved',
x,
@@ -2241,6 +2244,7 @@ async function clickWithDebugger(tabId, rect) {
buttons: 0,
clickCount: 0,
});
+ throwIfStopped();
await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
type: 'mousePressed',
x,
@@ -2249,6 +2253,7 @@ async function clickWithDebugger(tabId, rect) {
buttons: 1,
clickCount: 1,
});
+ throwIfStopped();
await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
type: 'mouseReleased',
x,
From c9736f6c5d4d027a19e3c64ca14491ee1da726d9 Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Sun, 12 Apr 2026 23:31:07 +0800
Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9C=AC?=
=?UTF-8?q?=E5=9C=B0=20CPA=20=E7=AC=AC=209=20=E6=AD=A5=E7=AD=96=E7=95=A5?=
=?UTF-8?q?=E5=88=87=E6=8D=A2=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 3 +++
background.js | 18 +++++++++++++++--
sidepanel/sidepanel.css | 35 ++++++++++++++++++++++++++++++++
sidepanel/sidepanel.html | 7 +++++++
sidepanel/sidepanel.js | 39 ++++++++++++++++++++++++++++++++++++
tests/step9-cpa-mode.test.js | 17 ++++++++++++++--
6 files changed, 115 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index ca02eb3..584b08d 100644
--- a/README.md
+++ b/README.md
@@ -369,6 +369,9 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
- 步骤 9 会拒绝任何不是真实 `/auth/callback`,或缺少 `code` / `state` 的本地回调地址
- 成功后的清理只会针对 `/auth` 这一类真实回调标签页,不会再泛化清理任意 localhost 路径
+- 侧边栏可切换“本地 CPA”策略,默认是 `全部回调`
+- 选择 `全部回调` 时,即使 CPA 部署在本地,也会执行步骤 9
+- 选择 `跳过第9步` 时,仅当本地 CPA 且步骤 8 已拿到回调地址时,才会直接跳过步骤 9
回到 CPA 面板:
diff --git a/background.js b/background.js
index 7ad4bad..503d710 100644
--- a/background.js
+++ b/background.js
@@ -39,6 +39,7 @@ const DEFAULT_SUB2API_REDIRECT_URI = 'http://localhost:1455/auth/callback';
const AUTO_RUN_ALARM_NAME = 'scheduled-auto-run';
const AUTO_RUN_DELAY_MIN_MINUTES = 1;
const AUTO_RUN_DELAY_MAX_MINUTES = 1440;
+const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
initializeSessionStorageAccess();
@@ -50,6 +51,7 @@ const PERSISTED_SETTING_DEFAULTS = {
panelMode: 'cpa', // Step 1 / Step 9 的来源模式:cpa | sub2api。
vpsUrl: '', // VPS 面板地址,可手动填写。
vpsPassword: '', // VPS 面板登录密码,可手动填写。
+ localCpaStep9Mode: DEFAULT_LOCAL_CPA_STEP9_MODE, // 本地 CPA 的第 9 步策略:submit | bypass。
sub2apiUrl: DEFAULT_SUB2API_URL, // SUB2API 管理后台地址。
sub2apiEmail: '', // SUB2API 登录邮箱。
sub2apiPassword: '', // SUB2API 登录密码。
@@ -139,6 +141,12 @@ function normalizeEmailGenerator(value = '') {
return String(value || '').trim().toLowerCase() === 'cloudflare' ? 'cloudflare' : 'duck';
}
+function normalizeLocalCpaStep9Mode(value = '') {
+ return String(value || '').trim().toLowerCase() === 'bypass'
+ ? 'bypass'
+ : DEFAULT_LOCAL_CPA_STEP9_MODE;
+}
+
function normalizeCloudflareDomain(rawValue = '') {
let value = String(rawValue || '').trim().toLowerCase();
if (!value) return '';
@@ -158,6 +166,7 @@ async function getPersistedSettings() {
autoRunDelayEnabled: Boolean(stored.autoRunDelayEnabled ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayEnabled),
autoRunDelayMinutes: normalizeAutoRunDelayMinutes(stored.autoRunDelayMinutes ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayMinutes),
emailGenerator: normalizeEmailGenerator(stored.emailGenerator ?? PERSISTED_SETTING_DEFAULTS.emailGenerator),
+ localCpaStep9Mode: normalizeLocalCpaStep9Mode(stored.localCpaStep9Mode ?? PERSISTED_SETTING_DEFAULTS.localCpaStep9Mode),
hotmailAccounts: normalizeHotmailAccounts(stored.hotmailAccounts),
};
}
@@ -198,6 +207,8 @@ async function setPersistentSettings(updates) {
persistedUpdates[key] = Boolean(updates[key]);
} else if (key === 'autoRunDelayMinutes') {
persistedUpdates[key] = normalizeAutoRunDelayMinutes(updates[key]);
+ } else if (key === 'localCpaStep9Mode') {
+ persistedUpdates[key] = normalizeLocalCpaStep9Mode(updates[key]);
} else if (key === 'hotmailAccounts') {
persistedUpdates[key] = normalizeHotmailAccounts(updates[key]);
} else {
@@ -983,7 +994,9 @@ function isLocalCpaUrl(rawUrl) {
}
function shouldBypassStep9ForLocalCpa(state) {
- return Boolean(state?.localhostUrl) && isLocalCpaUrl(state?.vpsUrl);
+ return normalizeLocalCpaStep9Mode(state?.localCpaStep9Mode) === 'bypass'
+ && Boolean(state?.localhostUrl)
+ && isLocalCpaUrl(state?.vpsUrl);
}
function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
@@ -2439,6 +2452,7 @@ async function handleMessage(message, sender) {
if (message.payload.panelMode !== undefined) updates.panelMode = message.payload.panelMode;
if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
if (message.payload.vpsPassword !== undefined) updates.vpsPassword = message.payload.vpsPassword;
+ if (message.payload.localCpaStep9Mode !== undefined) updates.localCpaStep9Mode = normalizeLocalCpaStep9Mode(message.payload.localCpaStep9Mode);
if (message.payload.sub2apiUrl !== undefined) updates.sub2apiUrl = message.payload.sub2apiUrl;
if (message.payload.sub2apiEmail !== undefined) updates.sub2apiEmail = message.payload.sub2apiEmail;
if (message.payload.sub2apiPassword !== undefined) updates.sub2apiPassword = message.payload.sub2apiPassword;
@@ -4420,7 +4434,7 @@ async function executeCpaStep9(state) {
}
if (shouldBypassStep9ForLocalCpa(state)) {
- await addLog('步骤 9:检测到本地 CPA,步骤 8 完成后已自动添加,无需重复提交回调地址。', 'info');
+ await addLog('步骤 9:检测到本地 CPA,且当前策略为“跳过第9步”,本轮不再重复提交回调地址。', 'info');
await completeStepFromBackground(9, {
localhostUrl: state.localhostUrl,
verifiedStatus: 'local-auto',
diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css
index a300720..8324132 100644
--- a/sidepanel/sidepanel.css
+++ b/sidepanel/sidepanel.css
@@ -378,6 +378,41 @@ header {
background: var(--bg-hover);
}
+.choice-group {
+ flex: 1;
+ min-width: 0;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.choice-btn {
+ flex: 1;
+ min-width: 0;
+ padding: 7px 10px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ background: var(--bg-base);
+ color: var(--text-secondary);
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: border-color var(--transition), background var(--transition), color var(--transition), box-shadow var(--transition);
+}
+
+.choice-btn:hover {
+ border-color: var(--blue);
+ color: var(--blue);
+ background: var(--blue-soft);
+}
+
+.choice-btn.is-active {
+ border-color: var(--blue);
+ color: var(--blue);
+ background: var(--blue-soft);
+ box-shadow: 0 0 0 1px var(--blue-glow);
+}
+
#btn-fetch-email,
#btn-save-settings {
padding-inline: 10px;
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index e8a9e2c..049391f 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -83,6 +83,13 @@
管理密钥
+
SUB2API
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index 6efc3d9..1016fea 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -38,6 +38,8 @@ const rowVpsUrl = document.getElementById('row-vps-url');
const inputVpsUrl = document.getElementById('input-vps-url');
const rowVpsPassword = document.getElementById('row-vps-password');
const inputVpsPassword = document.getElementById('input-vps-password');
+const rowLocalCpaStep9Mode = document.getElementById('row-local-cpa-step9-mode');
+const localCpaStep9ModeButtons = Array.from(document.querySelectorAll('[data-local-cpa-step9-mode]'));
const rowSub2ApiUrl = document.getElementById('row-sub2api-url');
const inputSub2ApiUrl = document.getElementById('input-sub2api-url');
const rowSub2ApiEmail = document.getElementById('row-sub2api-email');
@@ -96,6 +98,7 @@ const SKIPPABLE_STEPS = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9]);
const AUTO_DELAY_MIN_MINUTES = 1;
const AUTO_DELAY_MAX_MINUTES = 1440;
const AUTO_DELAY_DEFAULT_MINUTES = 30;
+const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
let latestState = null;
let currentAutoRun = {
@@ -492,6 +495,7 @@ function collectSettingsPayload() {
panelMode: selectPanelMode.value,
vpsUrl: inputVpsUrl.value.trim(),
vpsPassword: inputVpsPassword.value,
+ localCpaStep9Mode: getSelectedLocalCpaStep9Mode(),
sub2apiUrl: inputSub2ApiUrl.value.trim(),
sub2apiEmail: inputSub2ApiEmail.value.trim(),
sub2apiPassword: inputSub2ApiPassword.value,
@@ -509,6 +513,26 @@ function collectSettingsPayload() {
};
}
+function normalizeLocalCpaStep9Mode(value = '') {
+ return String(value || '').trim().toLowerCase() === 'bypass'
+ ? 'bypass'
+ : DEFAULT_LOCAL_CPA_STEP9_MODE;
+}
+
+function getSelectedLocalCpaStep9Mode() {
+ const activeButton = localCpaStep9ModeButtons.find((button) => button.classList.contains('is-active'));
+ return normalizeLocalCpaStep9Mode(activeButton?.dataset.localCpaStep9Mode);
+}
+
+function setLocalCpaStep9Mode(mode) {
+ const resolvedMode = normalizeLocalCpaStep9Mode(mode);
+ localCpaStep9ModeButtons.forEach((button) => {
+ const active = button.dataset.localCpaStep9Mode === resolvedMode;
+ button.classList.toggle('is-active', active);
+ button.setAttribute('aria-pressed', String(active));
+ });
+}
+
function markSettingsDirty(isDirty = true) {
settingsDirty = isDirty;
updateSaveButtonState();
@@ -677,6 +701,7 @@ async function restoreState() {
if (state.vpsPassword) {
inputVpsPassword.value = state.vpsPassword;
}
+ setLocalCpaStep9Mode(state.localCpaStep9Mode);
if (state.panelMode) {
selectPanelMode.value = state.panelMode;
}
@@ -1051,6 +1076,7 @@ function updatePanelModeUI() {
const useSub2Api = selectPanelMode.value === 'sub2api';
rowVpsUrl.style.display = useSub2Api ? 'none' : '';
rowVpsPassword.style.display = useSub2Api ? 'none' : '';
+ rowLocalCpaStep9Mode.style.display = useSub2Api ? 'none' : '';
rowSub2ApiUrl.style.display = useSub2Api ? '' : 'none';
rowSub2ApiEmail.style.display = useSub2Api ? '' : 'none';
rowSub2ApiPassword.style.display = useSub2Api ? '' : 'none';
@@ -1698,6 +1724,18 @@ btnToggleVpsUrl.addEventListener('click', () => {
syncVpsUrlToggleLabel();
});
+localCpaStep9ModeButtons.forEach((button) => {
+ button.addEventListener('click', () => {
+ const nextMode = button.dataset.localCpaStep9Mode;
+ if (getSelectedLocalCpaStep9Mode() === normalizeLocalCpaStep9Mode(nextMode)) {
+ return;
+ }
+ setLocalCpaStep9Mode(nextMode);
+ markSettingsDirty(true);
+ saveSettings({ silent: true }).catch(() => { });
+ });
+});
+
btnSaveSettings.addEventListener('click', async () => {
if (!settingsDirty) {
showToast('配置已是最新', 'info', 1400);
@@ -2147,6 +2185,7 @@ initializeManualStepActions();
initTheme();
initHotmailListExpandedState();
updateSaveButtonState();
+setLocalCpaStep9Mode(DEFAULT_LOCAL_CPA_STEP9_MODE);
restoreState().then(() => {
syncPasswordToggleLabel();
syncVpsUrlToggleLabel();
diff --git a/tests/step9-cpa-mode.test.js b/tests/step9-cpa-mode.test.js
index 0c2d8e7..7645bca 100644
--- a/tests/step9-cpa-mode.test.js
+++ b/tests/step9-cpa-mode.test.js
@@ -28,29 +28,42 @@ function extractFunction(name) {
}
const bundle = [
+ 'const DEFAULT_LOCAL_CPA_STEP9_MODE = "submit";',
extractFunction('parseUrlSafely'),
extractFunction('isLocalCpaUrl'),
+ extractFunction('normalizeLocalCpaStep9Mode'),
extractFunction('shouldBypassStep9ForLocalCpa'),
].join('\n');
-const api = new Function(`${bundle}; return { isLocalCpaUrl, shouldBypassStep9ForLocalCpa };`)();
+const api = new Function(`${bundle}; return { isLocalCpaUrl, normalizeLocalCpaStep9Mode, shouldBypassStep9ForLocalCpa };`)();
assert.strictEqual(api.isLocalCpaUrl('http://127.0.0.1:8317/management.html#/oauth'), true, '127.0.0.1 应视为本地 CPA');
assert.strictEqual(api.isLocalCpaUrl('http://localhost:1455/management.html#/oauth'), true, 'localhost 应视为本地 CPA');
assert.strictEqual(api.isLocalCpaUrl('https://example.com/management.html#/oauth'), false, '远程域名不应视为本地 CPA');
assert.strictEqual(api.isLocalCpaUrl('notaurl'), false, '非法 URL 不应视为本地 CPA');
+assert.strictEqual(api.normalizeLocalCpaStep9Mode('submit'), 'submit', 'submit 应保持为 submit');
+assert.strictEqual(api.normalizeLocalCpaStep9Mode('bypass'), 'bypass', 'bypass 应保持为 bypass');
+assert.strictEqual(api.normalizeLocalCpaStep9Mode('other'), 'submit', '未知模式应回退为 submit');
assert.strictEqual(api.shouldBypassStep9ForLocalCpa({
vpsUrl: 'http://127.0.0.1:8317/management.html#/oauth',
localhostUrl: 'http://127.0.0.1:8317/codex/callback?code=abc&state=xyz',
-}), true, '本地 CPA 且已有 callback 时应跳过远程提交流程');
+}), false, '默认模式下,本地 CPA 也应执行步骤 9');
assert.strictEqual(api.shouldBypassStep9ForLocalCpa({
+ localCpaStep9Mode: 'bypass',
+ vpsUrl: 'http://127.0.0.1:8317/management.html#/oauth',
+ localhostUrl: 'http://127.0.0.1:8317/codex/callback?code=abc&state=xyz',
+}), true, '切换为 bypass 后,本地 CPA 且已有 callback 时应跳过步骤 9');
+
+assert.strictEqual(api.shouldBypassStep9ForLocalCpa({
+ localCpaStep9Mode: 'bypass',
vpsUrl: 'https://example.com/management.html#/oauth',
localhostUrl: 'http://127.0.0.1:8317/codex/callback?code=abc&state=xyz',
}), false, '远程 CPA 不应跳过步骤 9');
assert.strictEqual(api.shouldBypassStep9ForLocalCpa({
+ localCpaStep9Mode: 'bypass',
vpsUrl: 'http://127.0.0.1:8317/management.html#/oauth',
localhostUrl: '',
}), false, '没有 callback 时不应跳过步骤 9');
From b31459b3b82ff7859f35adddad86e9eb9ccf32bd Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Sun, 12 Apr 2026 23:45:35 +0800
Subject: [PATCH 3/6] =?UTF-8?q?feat(sidepanel):=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=AD=A3=E5=9C=A8=E8=BF=90=E8=A1=8C=E6=AD=A5?=
=?UTF-8?q?=E9=AA=A4=E7=9A=84=E5=8A=9F=E8=83=BD=E5=B9=B6=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=89=A7=E8=A1=8C=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
background.js | 50 ++++++++++++++++++++++++++++++++++++------
sidepanel/sidepanel.js | 42 ++++++++++++++++++++++++++++++-----
2 files changed, 79 insertions(+), 13 deletions(-)
diff --git a/background.js b/background.js
index 503d710..0da1ebb 100644
--- a/background.js
+++ b/background.js
@@ -1894,6 +1894,13 @@ function clearStopRequest() {
stopRequested = false;
}
+function getRunningSteps(statuses = {}) {
+ return Object.entries({ ...DEFAULT_STATE.stepStatuses, ...statuses })
+ .filter(([, status]) => status === 'running')
+ .map(([step]) => Number(step))
+ .sort((a, b) => a - b);
+}
+
function getAutoRunStatusPayload(phase, payload = {}) {
const currentRun = payload.currentRun ?? autoRunCurrentRun;
const totalRuns = payload.totalRuns ?? autoRunTotalRuns;
@@ -1902,7 +1909,7 @@ function getAutoRunStatusPayload(phase, payload = {}) {
? (payload.scheduledAt ?? payload.scheduledAutoRunAt ?? null)
: null;
const scheduledAt = rawScheduledAt === null ? null : Number(rawScheduledAt);
- const autoRunning = phase === 'scheduled' || phase === 'running' || phase === 'waiting_email' || phase === 'retrying';
+ const autoRunning = phase === 'scheduled' || phase === 'running' || phase === 'waiting_step' || phase === 'waiting_email' || phase === 'retrying';
return {
autoRunning,
@@ -1937,7 +1944,7 @@ async function broadcastAutoRunStatus(phase, payload = {}, extraState = {}) {
}
function isAutoRunLockedState(state) {
- return Boolean(state.autoRunning) && (state.autoRunPhase === 'running' || state.autoRunPhase === 'retrying');
+ return Boolean(state.autoRunning) && (state.autoRunPhase === 'running' || state.autoRunPhase === 'waiting_step' || state.autoRunPhase === 'retrying');
}
function isAutoRunPausedState(state) {
@@ -2707,11 +2714,29 @@ async function completeStepFromBackground(step, payload = {}) {
notifyStepComplete(step, payload);
}
+async function waitForRunningStepsToFinish(payload = {}) {
+ let currentState = await getState();
+ let runningSteps = getRunningSteps(currentState.stepStatuses);
+ if (!runningSteps.length) {
+ return currentState;
+ }
+
+ await addLog(`自动继续:检测到步骤 ${runningSteps.join(', ')} 正在运行,等待完成后再继续自动流程...`, 'info');
+ await broadcastAutoRunStatus('waiting_step', payload);
+
+ while (runningSteps.length) {
+ await sleepWithStop(250);
+ currentState = await getState();
+ runningSteps = getRunningSteps(currentState.stepStatuses);
+ }
+
+ await addLog('自动继续:当前运行步骤已结束,准备按最新进度继续自动流程...', 'info');
+ return currentState;
+}
+
async function markRunningStepsStopped() {
const state = await getState();
- const runningSteps = Object.entries(state.stepStatuses || {})
- .filter(([, status]) => status === 'running')
- .map(([step]) => Number(step));
+ const runningSteps = getRunningSteps(state.stepStatuses);
for (const step of runningSteps) {
await setStepStatus(step, 'stopped');
@@ -3102,10 +3127,14 @@ async function autoRunLoop(totalRuns, options = {}) {
let attemptRuns = Math.max(0, resumeAttemptRunsProcessed);
let forceFreshTabsNextRun = false;
let continueCurrentOnFirstAttempt = initialMode === 'continue';
+ const initialState = await getState();
+ const initialPhase = continueCurrentOnFirstAttempt && getRunningSteps(initialState.stepStatuses).length
+ ? 'waiting_step'
+ : 'running';
await setState({
autoRunSkipFailures,
- ...getAutoRunStatusPayload('running', {
+ ...getAutoRunStatusPayload(initialPhase, {
currentRun: resumeCurrentRun,
totalRuns,
attemptRun: resumeAttemptRunsProcessed,
@@ -3121,7 +3150,14 @@ async function autoRunLoop(totalRuns, options = {}) {
let useExistingProgress = false;
if (continueCurrentOnFirstAttempt) {
- const currentState = await getState();
+ let currentState = await getState();
+ if (getRunningSteps(currentState.stepStatuses).length) {
+ currentState = await waitForRunningStepsToFinish({
+ currentRun: targetRun,
+ totalRuns,
+ attemptRun: attemptRuns,
+ });
+ }
const resumeStep = getFirstUnfinishedStep(currentState.stepStatuses);
if (resumeStep && hasSavedProgress(currentState.stepStatuses)) {
startStep = resumeStep;
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index 1016fea..6e0018b 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -228,10 +228,14 @@ function openActionModal({ title, message, actions }) {
});
}
-function openAutoStartChoiceDialog(startStep) {
+function openAutoStartChoiceDialog(startStep, options = {}) {
+ const runningStep = Number.isInteger(options.runningStep) ? options.runningStep : null;
+ const continueMessage = runningStep
+ ? `继续当前会先等待步骤 ${runningStep} 完成,再按最新进度自动执行。`
+ : `继续当前会从步骤 ${startStep} 开始自动执行。`;
return openActionModal({
title: '启动自动',
- message: `检测到当前已有流程进度。继续当前会从步骤 ${startStep} 开始自动执行,重新开始会清空当前流程进度并从步骤 1 新开一轮。`,
+ message: `检测到当前已有流程进度。${continueMessage}重新开始会清空当前流程进度并从步骤 1 新开一轮。`,
actions: [
{ id: null, label: '取消', variant: 'btn-ghost' },
{ id: 'restart', label: '重新开始', variant: 'btn-outline' },
@@ -270,6 +274,14 @@ function getFirstUnfinishedStep(state = latestState) {
return null;
}
+function getRunningSteps(state = latestState) {
+ const statuses = getStepStatuses(state);
+ return Object.entries(statuses)
+ .filter(([, status]) => status === 'running')
+ .map(([step]) => Number(step))
+ .sort((a, b) => a - b);
+}
+
function hasSavedProgress(state = latestState) {
const statuses = getStepStatuses(state);
return Object.values(statuses).some((status) => status !== 'pending');
@@ -296,7 +308,7 @@ function syncAutoRunState(source = {}) {
const autoRunning = source.autoRunning !== undefined
? Boolean(source.autoRunning)
: (source.autoRunPhase !== undefined || source.phase !== undefined
- ? ['scheduled', 'running', 'waiting_email', 'retrying'].includes(phase)
+ ? ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying'].includes(phase)
: currentAutoRun.autoRunning);
currentAutoRun = {
@@ -310,13 +322,17 @@ function syncAutoRunState(source = {}) {
}
function isAutoRunLockedPhase() {
- return currentAutoRun.phase === 'running' || currentAutoRun.phase === 'retrying';
+ return currentAutoRun.phase === 'running' || currentAutoRun.phase === 'waiting_step' || currentAutoRun.phase === 'retrying';
}
function isAutoRunPausedPhase() {
return currentAutoRun.phase === 'waiting_email';
}
+function isAutoRunWaitingStepPhase() {
+ return currentAutoRun.phase === 'waiting_step';
+}
+
function isAutoRunScheduledPhase() {
return currentAutoRun.phase === 'scheduled';
}
@@ -615,6 +631,10 @@ function applyAutoRunStatus(payload = currentAutoRun) {
autoContinueBar.style.display = 'none';
btnAutoRun.innerHTML = `已计划${runLabel}`;
break;
+ case 'waiting_step':
+ autoContinueBar.style.display = 'none';
+ btnAutoRun.innerHTML = `等待中${runLabel}`;
+ break;
case 'waiting_email':
autoContinueBar.style.display = 'flex';
btnAutoRun.innerHTML = `已暂停${runLabel}`;
@@ -1192,6 +1212,15 @@ function updateStatusDisplay(state) {
return;
}
+ if (isAutoRunWaitingStepPhase()) {
+ const runningSteps = getRunningSteps(state);
+ displayStatus.textContent = runningSteps.length
+ ? `自动等待步骤 ${runningSteps.join(', ')} 完成后继续${getAutoRunLabel()}`
+ : `自动正在按最新进度准备继续${getAutoRunLabel()}`;
+ statusBar.classList.add('running');
+ return;
+ }
+
const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running');
if (running) {
displayStatus.textContent = `步骤 ${running[0]} 运行中...`;
@@ -1765,7 +1794,8 @@ btnAutoRun.addEventListener('click', async () => {
if (shouldOfferAutoModeChoice()) {
const startStep = getFirstUnfinishedStep();
- const choice = await openAutoStartChoiceDialog(startStep);
+ const runningStep = getRunningSteps()[0] ?? null;
+ const choice = await openAutoStartChoiceDialog(startStep, { runningStep });
if (!choice) {
return;
}
@@ -2137,7 +2167,7 @@ chrome.runtime.onMessage.addListener((message) => {
case 'AUTO_RUN_STATUS': {
syncLatestState({
- autoRunning: ['scheduled', 'running', 'waiting_email', 'retrying'].includes(message.payload.phase),
+ autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying'].includes(message.payload.phase),
autoRunPhase: message.payload.phase,
autoRunCurrentRun: message.payload.currentRun,
autoRunTotalRuns: message.payload.totalRuns,
From be8e2e25c2a9d0da2f2005c32ce1fc71d43bda08 Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Mon, 13 Apr 2026 00:15:50 +0800
Subject: [PATCH 4/6] =?UTF-8?q?feat(sidepanel):=20=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E9=82=AE=E7=AE=B1=E7=8A=B6=E6=80=81=E5=A4=84=E7=90=86=E9=80=BB?=
=?UTF-8?q?=E8=BE=91=E5=B9=B6=E5=A2=9E=E5=BC=BA=E8=AE=BE=E7=BD=AE=E5=8D=A1?=
=?UTF-8?q?=E7=89=87=E7=9A=84=E9=94=81=E5=AE=9A=E7=8A=B6=E6=80=81=E7=AE=A1?=
=?UTF-8?q?=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
background.js | 16 +++++++++-
sidepanel/sidepanel.css | 22 ++++++++++++++
sidepanel/sidepanel.html | 20 ++++++++-----
sidepanel/sidepanel.js | 64 ++++++++++++++++++++++++++++++++++++++--
4 files changed, 111 insertions(+), 11 deletions(-)
diff --git a/background.js b/background.js
index 0da1ebb..ab1bf59 100644
--- a/background.js
+++ b/background.js
@@ -229,9 +229,13 @@ function broadcastDataUpdate(payload) {
}).catch(() => { });
}
-async function setEmailState(email) {
+async function setEmailStateSilently(email) {
await setState({ email });
broadcastDataUpdate({ email });
+}
+
+async function setEmailState(email) {
+ await setEmailStateSilently(email);
if (email) {
await resumeAutoRunIfWaitingForEmail();
}
@@ -2539,6 +2543,16 @@ async function handleMessage(message, sender) {
}
// Side panel data updates
+ case 'SET_EMAIL_STATE': {
+ const state = await getState();
+ if (isAutoRunLockedState(state)) {
+ throw new Error('自动流程运行中,当前不能手动修改邮箱。');
+ }
+ const email = String(message.payload?.email || '').trim() || null;
+ await setEmailStateSilently(email);
+ return { ok: true, email };
+ }
+
case 'SAVE_EMAIL': {
const state = await getState();
if (isAutoRunLockedState(state)) {
diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css
index 8324132..7b1a9a7 100644
--- a/sidepanel/sidepanel.css
+++ b/sidepanel/sidepanel.css
@@ -238,6 +238,20 @@ header {
box-shadow: var(--shadow-sm);
}
+.data-card.is-locked {
+ position: relative;
+ overflow: hidden;
+}
+
+.data-card.is-locked::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border-radius: inherit;
+ background: color-mix(in srgb, var(--bg-base) 18%, transparent);
+ pointer-events: none;
+}
+
.section-mini-header {
display: flex;
align-items: center;
@@ -283,6 +297,10 @@ header {
min-width: 0;
}
+.data-value-actions {
+ justify-content: space-between;
+}
+
.input-with-icon {
position: relative;
flex: 1;
@@ -307,6 +325,10 @@ header {
min-width: 0;
}
+.data-value-fill {
+ flex: 1;
+}
+
.data-value.has-value { color: var(--text-primary); }
.mono {
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 049391f..04d6332 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -62,7 +62,7 @@
-
+
来源
本地 CPA
@@ -108,14 +112,11 @@
@@ -186,7 +187,10 @@
回调
-
等待中...
+
+ 等待中...
+
+
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index 6e0018b..6f45da9 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -11,6 +11,7 @@ const STATUS_ICONS = {
};
const logArea = document.getElementById('log-area');
+const settingsCard = document.getElementById('settings-card');
const displayOauthUrl = document.getElementById('display-oauth-url');
const displayLocalhostUrl = document.getElementById('display-localhost-url');
const displayStatus = document.getElementById('display-status');
@@ -549,6 +550,48 @@ function setLocalCpaStep9Mode(mode) {
});
}
+function setSettingsCardLocked(locked) {
+ if (!settingsCard) {
+ return;
+ }
+ settingsCard.classList.toggle('is-locked', locked);
+ settingsCard.toggleAttribute('inert', locked);
+}
+
+async function setRuntimeEmailState(email) {
+ const normalizedEmail = String(email || '').trim() || null;
+ const response = await chrome.runtime.sendMessage({
+ type: 'SET_EMAIL_STATE',
+ source: 'sidepanel',
+ payload: { email: normalizedEmail },
+ });
+
+ if (response?.error) {
+ throw new Error(response.error);
+ }
+
+ return normalizedEmail;
+}
+
+async function clearRegistrationEmail(options = {}) {
+ const { silent = false } = options;
+ if (!inputEmail.value.trim() && !latestState?.email) {
+ return;
+ }
+
+ inputEmail.value = '';
+ syncLatestState({ email: null });
+
+ try {
+ await setRuntimeEmailState(null);
+ } catch (err) {
+ if (!silent) {
+ showToast(`清空邮箱失败:${err.message}`, 'error');
+ }
+ throw err;
+ }
+}
+
function markSettingsDirty(isDirty = true) {
settingsDirty = isDirty;
updateSaveButtonState();
@@ -615,6 +658,9 @@ function applyAutoRunStatus(payload = currentAutoRun) {
const locked = isAutoRunLockedPhase();
const paused = isAutoRunPausedPhase();
const scheduled = isAutoRunScheduledPhase();
+ const settingsCardLocked = scheduled || locked;
+
+ setSettingsCardLocked(settingsCardLocked);
inputRunCount.disabled = currentAutoRun.autoRunning;
btnAutoRun.disabled = currentAutoRun.autoRunning;
@@ -1915,8 +1961,18 @@ inputEmail.addEventListener('change', async () => {
return;
}
const email = inputEmail.value.trim();
- if (email) {
- await chrome.runtime.sendMessage({ type: 'SAVE_EMAIL', source: 'sidepanel', payload: { email } });
+ inputEmail.value = email;
+ try {
+ if (email) {
+ const response = await chrome.runtime.sendMessage({ type: 'SAVE_EMAIL', source: 'sidepanel', payload: { email } });
+ if (response?.error) {
+ throw new Error(response.error);
+ }
+ } else {
+ await setRuntimeEmailState(null);
+ }
+ } catch (err) {
+ showToast(err.message, 'error');
}
});
inputEmail.addEventListener('input', updateButtonStates);
@@ -1953,6 +2009,7 @@ selectMailProvider.addEventListener('change', () => {
selectEmailGenerator.addEventListener('change', () => {
updateMailProviderUI();
+ clearRegistrationEmail({ silent: true }).catch(() => { });
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
});
@@ -2140,6 +2197,9 @@ chrome.runtime.onMessage.addListener((message) => {
if (message.payload.password !== undefined) {
inputPassword.value = message.payload.password || '';
}
+ if (message.payload.localCpaStep9Mode !== undefined) {
+ setLocalCpaStep9Mode(message.payload.localCpaStep9Mode);
+ }
if (message.payload.oauthUrl !== undefined) {
displayOauthUrl.textContent = message.payload.oauthUrl || '等待中...';
displayOauthUrl.classList.toggle('has-value', Boolean(message.payload.oauthUrl));
From 3e3d92de7abf2cc8a072ef5898bfff61b194d00b Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Mon, 13 Apr 2026 00:24:30 +0800
Subject: [PATCH 5/6] =?UTF-8?q?feat(sidepanel):=20=E6=B7=BB=E5=8A=A0=20VPS?=
=?UTF-8?q?=20=E5=AF=86=E7=A0=81=E5=88=87=E6=8D=A2=E6=8C=89=E9=92=AE?=
=?UTF-8?q?=E5=8F=8A=E5=85=B6=E6=A0=87=E7=AD=BE=E5=90=8C=E6=AD=A5=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
sidepanel/sidepanel.js | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index 6f45da9..8f9129b 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -19,6 +19,7 @@ const statusBar = document.getElementById('status-bar');
const inputEmail = document.getElementById('input-email');
const inputPassword = document.getElementById('input-password');
const btnToggleVpsUrl = document.getElementById('btn-toggle-vps-url');
+const btnToggleVpsPassword = document.getElementById('btn-toggle-vps-password');
const btnFetchEmail = document.getElementById('btn-fetch-email');
const btnTogglePassword = document.getElementById('btn-toggle-password');
const btnSaveSettings = document.getElementById('btn-save-settings');
@@ -1463,6 +1464,13 @@ function syncVpsUrlToggleLabel() {
});
}
+function syncVpsPasswordToggleLabel() {
+ syncToggleButtonLabel(btnToggleVpsPassword, inputVpsPassword, {
+ show: '显示管理密钥',
+ hide: '隐藏管理密钥',
+ });
+}
+
async function maybeTakeoverAutoRun(actionLabel) {
if (!isAutoRunPausedPhase()) {
return true;
@@ -1799,6 +1807,11 @@ btnToggleVpsUrl.addEventListener('click', () => {
syncVpsUrlToggleLabel();
});
+btnToggleVpsPassword.addEventListener('click', () => {
+ inputVpsPassword.type = inputVpsPassword.type === 'password' ? 'text' : 'password';
+ syncVpsPasswordToggleLabel();
+});
+
localCpaStep9ModeButtons.forEach((button) => {
button.addEventListener('click', () => {
const nextMode = button.dataset.localCpaStep9Mode;
@@ -2279,6 +2292,7 @@ setLocalCpaStep9Mode(DEFAULT_LOCAL_CPA_STEP9_MODE);
restoreState().then(() => {
syncPasswordToggleLabel();
syncVpsUrlToggleLabel();
+ syncVpsPasswordToggleLabel();
updatePanelModeUI();
updateButtonStates();
updateStatusDisplay(latestState);
From b382cbf02f91a247ce0e25f4353fb5ece775bfdb Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Mon, 13 Apr 2026 01:18:14 +0800
Subject: [PATCH 6/6] =?UTF-8?q?feat(sidepanel):=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=8F=9C=E5=8D=95=EF=BC=8C=E6=94=AF=E6=8C=81?=
=?UTF-8?q?=E5=AF=BC=E5=85=A5=E5=92=8C=E5=AF=BC=E5=87=BA=E8=AE=BE=E7=BD=AE?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 1 +
background.js | 231 +++++++++++++++++++++++------
sidepanel/sidepanel.css | 61 ++++++++
sidepanel/sidepanel.html | 9 ++
sidepanel/sidepanel.js | 303 ++++++++++++++++++++++++++++++++-------
5 files changed, 503 insertions(+), 102 deletions(-)
diff --git a/README.md b/README.md
index 584b08d..b120186 100644
--- a/README.md
+++ b/README.md
@@ -215,6 +215,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
- 手动输入:使用你自定义的密码
- 可通过输入框右侧的眼睛图标切换显示
- 配置会自动保存,也可以点击右侧 `保存` 按钮手动保存一次
+- 右上角 `配置` 按钮支持导出当前配置到 JSON 文件,也支持从 JSON 文件覆盖导入配置
扩展会把本轮实际使用的密码同步回侧边栏,便于查看和复制。
diff --git a/background.js b/background.js
index ab1bf59..1902e12 100644
--- a/background.js
+++ b/background.js
@@ -70,6 +70,8 @@ const PERSISTED_SETTING_DEFAULTS = {
};
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
+const SETTINGS_EXPORT_SCHEMA_VERSION = 1;
+const SETTINGS_EXPORT_FILENAME_PREFIX = 'multipage-settings';
const DEFAULT_STATE = {
currentStep: 0, // 当前流程执行到的步骤编号。
@@ -141,6 +143,24 @@ function normalizeEmailGenerator(value = '') {
return String(value || '').trim().toLowerCase() === 'cloudflare' ? 'cloudflare' : 'duck';
}
+function normalizePanelMode(value = '') {
+ return String(value || '').trim().toLowerCase() === 'sub2api' ? 'sub2api' : 'cpa';
+}
+
+function normalizeMailProvider(value = '') {
+ const normalized = String(value || '').trim().toLowerCase();
+ switch (normalized) {
+ case HOTMAIL_PROVIDER:
+ case '163':
+ case '163-vip':
+ case 'qq':
+ case 'inbucket':
+ return normalized;
+ default:
+ return PERSISTED_SETTING_DEFAULTS.mailProvider;
+ }
+}
+
function normalizeLocalCpaStep9Mode(value = '') {
return String(value || '').trim().toLowerCase() === 'bypass'
? 'bypass'
@@ -157,18 +177,99 @@ function normalizeCloudflareDomain(rawValue = '') {
return value;
}
+function normalizeCloudflareDomains(values) {
+ const normalizedDomains = [];
+ const seen = new Set();
+
+ for (const value of Array.isArray(values) ? values : []) {
+ const normalized = normalizeCloudflareDomain(value);
+ if (!normalized || seen.has(normalized)) continue;
+ seen.add(normalized);
+ normalizedDomains.push(normalized);
+ }
+
+ return normalizedDomains;
+}
+
+function normalizePersistentSettingValue(key, value) {
+ switch (key) {
+ case 'panelMode':
+ return normalizePanelMode(value);
+ case 'vpsUrl':
+ return String(value || '').trim();
+ case 'vpsPassword':
+ return String(value || '');
+ case 'localCpaStep9Mode':
+ return normalizeLocalCpaStep9Mode(value);
+ case 'sub2apiUrl':
+ return String(value || '').trim();
+ case 'sub2apiEmail':
+ return String(value || '').trim();
+ case 'sub2apiPassword':
+ return String(value || '');
+ case 'sub2apiGroupName':
+ return String(value || '').trim();
+ case 'customPassword':
+ return String(value || '');
+ case 'autoRunSkipFailures':
+ case 'autoRunDelayEnabled':
+ return Boolean(value);
+ case 'autoRunDelayMinutes':
+ return normalizeAutoRunDelayMinutes(value);
+ case 'mailProvider':
+ return normalizeMailProvider(value);
+ case 'emailGenerator':
+ return normalizeEmailGenerator(value);
+ case 'inbucketHost':
+ return String(value || '').trim();
+ case 'inbucketMailbox':
+ return String(value || '').trim();
+ case 'cloudflareDomain':
+ return normalizeCloudflareDomain(value);
+ case 'cloudflareDomains':
+ return normalizeCloudflareDomains(value);
+ case 'hotmailAccounts':
+ return normalizeHotmailAccounts(value);
+ default:
+ return value;
+ }
+}
+
+function buildPersistentSettingsPayload(input = {}, options = {}) {
+ const { fillDefaults = false, requireKnownKeys = false } = options;
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
+ throw new Error('\u914d\u7f6e\u5185\u5bb9\u683c\u5f0f\u65e0\u6548\u3002');
+ }
+
+ const payload = {};
+ let matchedKeyCount = 0;
+ for (const key of PERSISTED_SETTING_KEYS) {
+ if (input[key] !== undefined) {
+ payload[key] = normalizePersistentSettingValue(key, input[key]);
+ matchedKeyCount += 1;
+ } else if (fillDefaults) {
+ payload[key] = normalizePersistentSettingValue(key, PERSISTED_SETTING_DEFAULTS[key]);
+ }
+ }
+
+ if (requireKnownKeys && matchedKeyCount === 0) {
+ throw new Error('\u914d\u7f6e\u6587\u4ef6\u4e2d\u6ca1\u6709\u53ef\u8bc6\u522b\u7684\u914d\u7f6e\u5185\u5bb9\u3002');
+ }
+
+ if (payload.cloudflareDomains) {
+ const domains = normalizeCloudflareDomains(payload.cloudflareDomains);
+ if (payload.cloudflareDomain && !domains.includes(payload.cloudflareDomain)) {
+ domains.unshift(payload.cloudflareDomain);
+ }
+ payload.cloudflareDomains = domains;
+ }
+
+ return payload;
+}
+
async function getPersistedSettings() {
const stored = await chrome.storage.local.get(PERSISTED_SETTING_KEYS);
- return {
- ...PERSISTED_SETTING_DEFAULTS,
- ...stored,
- 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),
- localCpaStep9Mode: normalizeLocalCpaStep9Mode(stored.localCpaStep9Mode ?? PERSISTED_SETTING_DEFAULTS.localCpaStep9Mode),
- hotmailAccounts: normalizeHotmailAccounts(stored.hotmailAccounts),
- };
+ return buildPersistentSettingsPayload(stored, { fillDefaults: true });
}
async function getState() {
@@ -200,28 +301,75 @@ async function setState(updates) {
}
async function setPersistentSettings(updates) {
- const persistedUpdates = {};
- for (const key of PERSISTED_SETTING_KEYS) {
- if (updates[key] !== undefined) {
- if (key === 'autoRunSkipFailures' || key === 'autoRunDelayEnabled') {
- persistedUpdates[key] = Boolean(updates[key]);
- } else if (key === 'autoRunDelayMinutes') {
- persistedUpdates[key] = normalizeAutoRunDelayMinutes(updates[key]);
- } else if (key === 'localCpaStep9Mode') {
- persistedUpdates[key] = normalizeLocalCpaStep9Mode(updates[key]);
- } else if (key === 'hotmailAccounts') {
- persistedUpdates[key] = normalizeHotmailAccounts(updates[key]);
- } else {
- persistedUpdates[key] = updates[key];
- }
- }
- }
+ const persistedUpdates = buildPersistentSettingsPayload(updates);
if (Object.keys(persistedUpdates).length > 0) {
await chrome.storage.local.set(persistedUpdates);
}
}
+function buildSettingsExportFilename(date = new Date()) {
+ const pad = (value) => String(value).padStart(2, '0');
+ return `${SETTINGS_EXPORT_FILENAME_PREFIX}-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}.json`;
+}
+
+async function exportSettingsBundle() {
+ const settings = await getPersistedSettings();
+ const bundle = {
+ schemaVersion: SETTINGS_EXPORT_SCHEMA_VERSION,
+ exportedAt: new Date().toISOString(),
+ extensionVersion: chrome.runtime.getManifest().version,
+ settings,
+ };
+
+ return {
+ fileName: buildSettingsExportFilename(),
+ fileContent: JSON.stringify(bundle, null, 2),
+ };
+}
+
+async function importSettingsBundle(configBundle) {
+ const state = await ensureManualInteractionAllowed('\u5bfc\u5165\u914d\u7f6e');
+ if (Object.values(state.stepStatuses || {}).some((status) => status === 'running')) {
+ throw new Error('\u5f53\u524d\u6709\u6b65\u9aa4\u6b63\u5728\u6267\u884c\uff0c\u65e0\u6cd5\u5bfc\u5165\u914d\u7f6e\u3002');
+ }
+ if (!configBundle || typeof configBundle !== 'object' || Array.isArray(configBundle)) {
+ throw new Error('\u914d\u7f6e\u6587\u4ef6\u5185\u5bb9\u65e0\u6548\u3002');
+ }
+
+ const schemaVersion = Number(configBundle.schemaVersion);
+ if (schemaVersion !== SETTINGS_EXPORT_SCHEMA_VERSION) {
+ throw new Error(`\u4ec5\u652f\u6301\u5bfc\u5165 schemaVersion=${SETTINGS_EXPORT_SCHEMA_VERSION} \u7684\u914d\u7f6e\u6587\u4ef6\u3002`);
+ }
+ if (!configBundle.settings || typeof configBundle.settings !== 'object' || Array.isArray(configBundle.settings)) {
+ throw new Error('\u914d\u7f6e\u6587\u4ef6\u7f3a\u5c11 settings \u914d\u7f6e\u6bb5\u3002');
+ }
+
+ const importedSettings = buildPersistentSettingsPayload(configBundle.settings, {
+ fillDefaults: true,
+ requireKnownKeys: true,
+ });
+
+ await setPersistentSettings(importedSettings);
+
+ const sessionUpdates = {
+ ...importedSettings,
+ currentHotmailAccountId: null,
+ };
+ if (importedSettings.mailProvider === HOTMAIL_PROVIDER) {
+ sessionUpdates.email = null;
+ }
+
+ await setState(sessionUpdates);
+ broadcastDataUpdate({
+ ...importedSettings,
+ currentHotmailAccountId: null,
+ ...(sessionUpdates.email !== undefined ? { email: sessionUpdates.email } : {}),
+ });
+
+ return getState();
+}
+
function broadcastDataUpdate(payload) {
chrome.runtime.sendMessage({
type: 'DATA_UPDATED',
@@ -2459,32 +2607,21 @@ async function handleMessage(message, sender) {
}
case 'SAVE_SETTING': {
- const updates = {};
- if (message.payload.panelMode !== undefined) updates.panelMode = message.payload.panelMode;
- if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
- if (message.payload.vpsPassword !== undefined) updates.vpsPassword = message.payload.vpsPassword;
- if (message.payload.localCpaStep9Mode !== undefined) updates.localCpaStep9Mode = normalizeLocalCpaStep9Mode(message.payload.localCpaStep9Mode);
- if (message.payload.sub2apiUrl !== undefined) updates.sub2apiUrl = message.payload.sub2apiUrl;
- if (message.payload.sub2apiEmail !== undefined) updates.sub2apiEmail = message.payload.sub2apiEmail;
- if (message.payload.sub2apiPassword !== undefined) updates.sub2apiPassword = message.payload.sub2apiPassword;
- if (message.payload.sub2apiGroupName !== undefined) updates.sub2apiGroupName = message.payload.sub2apiGroupName;
- if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
- if (message.payload.autoRunSkipFailures !== undefined) updates.autoRunSkipFailures = Boolean(message.payload.autoRunSkipFailures);
- 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)
- : [];
+ const updates = buildPersistentSettingsPayload(message.payload || {});
await setPersistentSettings(updates);
await setState(updates);
return { ok: true };
}
+ case 'EXPORT_SETTINGS': {
+ return { ok: true, ...(await exportSettingsBundle()) };
+ }
+
+ case 'IMPORT_SETTINGS': {
+ const state = await importSettingsBundle(message.payload?.config || null);
+ return { ok: true, state };
+ }
+
case 'UPSERT_HOTMAIL_ACCOUNT': {
const account = await upsertHotmailAccount(message.payload || {});
return { ok: true, account };
diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css
index 7b1a9a7..b627d9d 100644
--- a/sidepanel/sidepanel.css
+++ b/sidepanel/sidepanel.css
@@ -142,6 +142,67 @@ header {
[data-theme="dark"] .theme-toggle .icon-moon { display: none; }
[data-theme="dark"] .theme-toggle .icon-sun { display: block; }
+.header-menu {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.header-config-btn {
+ min-width: 0;
+ padding-inline: 10px;
+}
+
+.header-config-btn[aria-expanded="true"] {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.header-dropdown {
+ position: absolute;
+ top: calc(100% + 6px);
+ right: 0;
+ min-width: 120px;
+ padding: 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ background: var(--bg-base);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ box-shadow: var(--shadow-md);
+ z-index: 1300;
+}
+
+.header-dropdown[hidden] {
+ display: none !important;
+}
+
+.header-dropdown-item {
+ width: 100%;
+ padding: 8px 10px;
+ border: none;
+ border-radius: var(--radius-sm);
+ background: transparent;
+ color: var(--text-secondary);
+ font: inherit;
+ font-size: 12px;
+ font-weight: 600;
+ text-align: left;
+ cursor: pointer;
+ transition: background var(--transition), color var(--transition);
+}
+
+.header-dropdown-item:hover:not(:disabled) {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.header-dropdown-item:disabled {
+ color: var(--text-muted);
+ cursor: not-allowed;
+}
+
/* ============================================================
Buttons
============================================================ */
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 04d6332..af607f4 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -58,6 +58,14 @@
+
@@ -343,6 +351,7 @@
+