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] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9C=AC=E5=9C=B0=20?=
=?UTF-8?q?CPA=20=E7=AC=AC=209=20=E6=AD=A5=E7=AD=96=E7=95=A5=E5=88=87?=
=?UTF-8?q?=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 @@
管理密钥
+
+
本地 CPA
+
+
+
+
+
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');