From e732dd2a202eb0aa787b16b9e85d895729f8fbe4 Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Wed, 22 Apr 2026 13:21:34 +0800
Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E6=B3=A8?=
=?UTF-8?q?=E5=86=8C=E8=AF=8A=E6=96=AD=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=B7=BB?=
=?UTF-8?q?=E5=8A=A0=E5=AF=86=E7=A0=81=E9=A1=B5=E5=92=8C=E5=8F=AF=E8=A7=81?=
=?UTF-8?q?=E6=80=A7=E5=85=83=E6=95=B0=E6=8D=AE=E6=8D=95=E8=8E=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
content/signup-page.js | 230 +++++++++++++++++-
docs/使用教程.md | 155 ++++++++++++
manifest.json | 2 +-
sidepanel/sidepanel.html | 2 +-
tests/signup-entry-diagnostics.test.js | 314 +++++++++++++++++++++++++
5 files changed, 691 insertions(+), 12 deletions(-)
create mode 100644 docs/使用教程.md
diff --git a/content/signup-page.js b/content/signup-page.js
index 6f68fba..53881b7 100644
--- a/content/signup-page.js
+++ b/content/signup-page.js
@@ -385,13 +385,82 @@ function getSignupEntryStateSummary(snapshot = inspectSignupEntryState()) {
}
function getSignupEntryDiagnostics() {
+ const view = typeof window !== 'undefined' ? window : globalThis;
+ const safeGetComputedStyle = (el) => {
+ if (!el || typeof view?.getComputedStyle !== 'function') {
+ return null;
+ }
+ try {
+ return view.getComputedStyle(el);
+ } catch {
+ return null;
+ }
+ };
+ const buildRectSummary = (el) => {
+ const rect = typeof el?.getBoundingClientRect === 'function'
+ ? el.getBoundingClientRect()
+ : null;
+ return rect
+ ? {
+ width: Math.round(rect.width || 0),
+ height: Math.round(rect.height || 0),
+ }
+ : null;
+ };
+ const buildVisibilityMeta = (el) => {
+ const style = safeGetComputedStyle(el);
+ return {
+ className: String(el?.className || '').slice(0, 200),
+ hidden: Boolean(el?.hidden),
+ ariaHidden: el?.getAttribute?.('aria-hidden') || '',
+ inert: typeof el?.hasAttribute === 'function' ? el.hasAttribute('inert') : false,
+ display: style?.display || '',
+ visibility: style?.visibility || '',
+ opacity: style?.opacity || '',
+ pointerEvents: style?.pointerEvents || '',
+ };
+ };
+ const findBlockingAncestor = (el) => {
+ let current = el?.parentElement || null;
+ while (current) {
+ const style = safeGetComputedStyle(current);
+ const rect = buildRectSummary(current);
+ const hidden = Boolean(current.hidden);
+ const ariaHidden = current.getAttribute?.('aria-hidden') || '';
+ const inert = typeof current.hasAttribute === 'function' ? current.hasAttribute('inert') : false;
+ const blockedByStyle = Boolean(
+ style
+ && (
+ style.display === 'none'
+ || style.visibility === 'hidden'
+ || style.opacity === '0'
+ || style.pointerEvents === 'none'
+ )
+ );
+ const blockedByRect = Boolean(rect && (rect.width === 0 || rect.height === 0));
+ if (hidden || ariaHidden === 'true' || inert || blockedByStyle || blockedByRect) {
+ return {
+ tag: (current.tagName || '').toLowerCase(),
+ id: current.id || '',
+ className: String(current.className || '').slice(0, 200),
+ hidden,
+ ariaHidden,
+ inert,
+ display: style?.display || '',
+ visibility: style?.visibility || '',
+ opacity: style?.opacity || '',
+ pointerEvents: style?.pointerEvents || '',
+ rect,
+ };
+ }
+ current = current.parentElement;
+ }
+ return null;
+ };
const actionCandidates = document.querySelectorAll(
'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
);
const allActions = Array.from(actionCandidates).map((el) => {
- const rect = typeof el?.getBoundingClientRect === 'function'
- ? el.getBoundingClientRect()
- : null;
const text = getActionText(el);
return {
tag: (el.tagName || '').toLowerCase(),
@@ -399,12 +468,7 @@ function getSignupEntryDiagnostics() {
text: text.slice(0, 80),
visible: isVisibleElement(el),
enabled: isActionEnabled(el),
- rect: rect
- ? {
- width: Math.round(rect.width || 0),
- height: Math.round(rect.height || 0),
- }
- : null,
+ rect: buildRectSummary(el),
};
});
const visibleActions = Array.from(actionCandidates)
@@ -417,7 +481,20 @@ function getSignupEntryDiagnostics() {
enabled: isActionEnabled(el),
}))
.filter((item) => item.text);
- const signupLikeActions = allActions
+ const signupLikeActions = Array.from(actionCandidates)
+ .map((el) => {
+ const text = getActionText(el);
+ return {
+ tag: (el.tagName || '').toLowerCase(),
+ type: el.getAttribute?.('type') || '',
+ text: text.slice(0, 80),
+ visible: isVisibleElement(el),
+ enabled: isActionEnabled(el),
+ rect: buildRectSummary(el),
+ ...buildVisibilityMeta(el),
+ blockingAncestor: findBlockingAncestor(el),
+ };
+ })
.filter((item) => item.text && SIGNUP_ENTRY_TRIGGER_PATTERN.test(item.text))
.slice(0, 12);
@@ -425,15 +502,133 @@ function getSignupEntryDiagnostics() {
url: location.href,
title: document.title || '',
readyState: document.readyState || '',
+ viewport: {
+ innerWidth: Math.round(Number(view?.innerWidth) || 0),
+ innerHeight: Math.round(Number(view?.innerHeight) || 0),
+ outerWidth: Math.round(Number(view?.outerWidth) || 0),
+ outerHeight: Math.round(Number(view?.outerHeight) || 0),
+ devicePixelRatio: Number(view?.devicePixelRatio) || 0,
+ },
hasEmailInput: Boolean(getSignupEmailInput()),
hasPasswordInput: Boolean(getSignupPasswordInput()),
bodyContainsSignupText: SIGNUP_ENTRY_TRIGGER_PATTERN.test(getPageTextSnapshot()),
+ signupLikeActionCounts: {
+ total: signupLikeActions.length,
+ visible: signupLikeActions.filter((item) => item.visible).length,
+ hidden: signupLikeActions.filter((item) => !item.visible).length,
+ },
signupLikeActions,
visibleActions,
bodyTextPreview: getPageTextSnapshot().slice(0, 240),
};
}
+function getSignupPasswordDiagnostics() {
+ const view = typeof window !== 'undefined' ? window : globalThis;
+ const safeGetComputedStyle = (el) => {
+ if (!el || typeof view?.getComputedStyle !== 'function') {
+ return null;
+ }
+ try {
+ return view.getComputedStyle(el);
+ } catch {
+ return null;
+ }
+ };
+ const buildRectSummary = (el) => {
+ const rect = typeof el?.getBoundingClientRect === 'function'
+ ? el.getBoundingClientRect()
+ : null;
+ return rect
+ ? {
+ width: Math.round(rect.width || 0),
+ height: Math.round(rect.height || 0),
+ }
+ : null;
+ };
+ const buildInputSummary = (el) => {
+ const style = safeGetComputedStyle(el);
+ return {
+ tag: (el?.tagName || '').toLowerCase(),
+ type: el?.getAttribute?.('type') || el?.type || '',
+ name: el?.getAttribute?.('name') || el?.name || '',
+ id: el?.id || '',
+ autocomplete: el?.getAttribute?.('autocomplete') || '',
+ placeholder: String(el?.getAttribute?.('placeholder') || '').slice(0, 80),
+ visible: isVisibleElement(el),
+ enabled: isActionEnabled(el),
+ valueLength: String(el?.value || '').length,
+ rect: buildRectSummary(el),
+ className: String(el?.className || '').slice(0, 200),
+ display: style?.display || '',
+ visibility: style?.visibility || '',
+ opacity: style?.opacity || '',
+ pointerEvents: style?.pointerEvents || '',
+ formAction: el?.form?.action || '',
+ };
+ };
+ const buildActionSummary = (el) => {
+ const style = safeGetComputedStyle(el);
+ return {
+ tag: (el?.tagName || '').toLowerCase(),
+ type: el?.getAttribute?.('type') || el?.type || '',
+ role: el?.getAttribute?.('role') || '',
+ text: getActionText(el).slice(0, 120),
+ visible: isVisibleElement(el),
+ enabled: isActionEnabled(el),
+ rect: buildRectSummary(el),
+ className: String(el?.className || '').slice(0, 200),
+ display: style?.display || '',
+ visibility: style?.visibility || '',
+ opacity: style?.opacity || '',
+ pointerEvents: style?.pointerEvents || '',
+ dataDdActionName: el?.getAttribute?.('data-dd-action-name') || '',
+ formAction: el?.form?.action || '',
+ };
+ };
+ const passwordInputs = Array.from(document.querySelectorAll(
+ 'input[type="password"], input[name*="password" i], input[autocomplete="new-password"], input[autocomplete="current-password"]'
+ ))
+ .map(buildInputSummary)
+ .slice(0, 8);
+ const actionCandidates = Array.from(document.querySelectorAll(
+ 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
+ ))
+ .map(buildActionSummary)
+ .filter((item) => item.text)
+ .slice(0, 16);
+ const visibleActions = actionCandidates.filter((item) => item.visible).slice(0, 12);
+ const submitButton = getSignupPasswordSubmitButton({ allowDisabled: true });
+ const oneTimeCodeTrigger = findOneTimeCodeLoginTrigger();
+ const retryState = getSignupPasswordTimeoutErrorPageState();
+
+ return {
+ url: location.href,
+ title: document.title || '',
+ readyState: document.readyState || '',
+ displayedEmail: getSignupPasswordDisplayedEmail(),
+ hasVisiblePasswordInput: Boolean(getSignupPasswordInput()),
+ passwordInputCount: passwordInputs.length,
+ visiblePasswordInputCount: passwordInputs.filter((item) => item.visible).length,
+ passwordInputs,
+ submitButton: submitButton ? buildActionSummary(submitButton) : null,
+ oneTimeCodeTrigger: oneTimeCodeTrigger ? buildActionSummary(oneTimeCodeTrigger) : null,
+ retryPage: Boolean(retryState),
+ retryEnabled: Boolean(retryState?.retryEnabled),
+ userAlreadyExistsBlocked: Boolean(retryState?.userAlreadyExistsBlocked),
+ visibleActions,
+ bodyTextPreview: getPageTextSnapshot().slice(0, 240),
+ };
+}
+
+function logSignupPasswordDiagnostics(context, level = 'warn') {
+ try {
+ log(`${context}:密码页诊断快照:${JSON.stringify(getSignupPasswordDiagnostics())}`, level);
+ } catch (error) {
+ console.warn('[MultiPage:signup-page] failed to build signup password diagnostics:', error?.message || error);
+ }
+}
+
async function waitForSignupEntryState(options = {}) {
const {
timeout = 15000,
@@ -620,6 +815,10 @@ async function step3_fillEmailPassword(payload) {
snapshot = inspectSignupEntryState();
}
+ if (snapshot.state !== 'password_page' || !snapshot.passwordInput) {
+ logSignupPasswordDiagnostics('步骤 3:未能识别可填写的密码输入框');
+ }
+
if (snapshot.state !== 'password_page' || !snapshot.passwordInput) {
throw new Error('在密码页未找到密码输入框。URL: ' + location.href);
}
@@ -635,6 +834,12 @@ async function step3_fillEmailPassword(payload) {
|| getSignupPasswordSubmitButton({ allowDisabled: true })
|| await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
+ if (!submitBtn) {
+ logSignupPasswordDiagnostics('步骤 3:未找到可提交的密码页按钮');
+ } else if (typeof findOneTimeCodeLoginTrigger === 'function' && findOneTimeCodeLoginTrigger()) {
+ logSignupPasswordDiagnostics('步骤 3:当前密码页同时存在一次性验证码入口', 'info');
+ }
+
// Report complete BEFORE submit, because submit causes page navigation
// which kills the content script connection
const signupVerificationRequestedAt = submitBtn ? Date.now() : null;
@@ -1640,6 +1845,7 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
const start = Date.now();
let recoveryRound = 0;
const maxRecoveryRounds = 3;
+ let passwordPageDiagnosticsLogged = false;
while (Date.now() - start < timeout && recoveryRound < maxRecoveryRounds) {
throwIfStopped();
@@ -1678,6 +1884,10 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
}
if (snapshot.state === 'password') {
+ if (!passwordPageDiagnosticsLogged) {
+ passwordPageDiagnosticsLogged = true;
+ logSignupPasswordDiagnostics(`${prepareLogLabel}:页面仍停留在密码页`);
+ }
if (!password) {
throw new Error('当前回到了密码页,但没有可用密码,无法自动重新提交。');
}
diff --git a/docs/使用教程.md b/docs/使用教程.md
new file mode 100644
index 0000000..90eda7b
--- /dev/null
+++ b/docs/使用教程.md
@@ -0,0 +1,155 @@
+# Codex 注册扩展更新与 Clash Verge 非港轮询配置教程
+
+本教程用于说明如何更新 `Codex 注册扩展`,以及如何在 `Clash Verge` 中配置 `🔁 非港轮询`。
+
+## 适用场景
+
+- 已经安装本扩展,想更新到最新版本
+- 想用更方便的方式长期同步扩展更新
+- 需要在 `Clash Verge` 中启用 `🔁 非港轮询`
+
+## 准备内容
+
+- 已安装好的扩展文件夹
+- 可以打开浏览器的 `扩展程序管理` 页面
+- 如需使用 `git pull`,本地已安装 `git`
+- 如需使用 `GitHub Desktop`,本地已安装 `GitHub Desktop`
+- 已安装 `Clash Verge`,并已导入可用订阅
+
+## 操作步骤
+
+### 第一部分:更新扩展
+
+1. 使用 `GitHub Desktop` 更新
+ 先安装 `GitHub Desktop`。
+ 把当前扩展仓库交给 `GitHub Desktop` 管理后,之后就可以随时更新。
+ `GitHub Desktop` 的具体使用方法本教程不展开,需要时可直接询问豆包。
+
+2. 使用 `git pull` 更新(最方便,最推荐)
+ 先在本地安装 `git`。
+ 打开终端后执行 `cd 扩展文件夹路径` 进入当前扩展目录。
+ 接着执行 `git pull` 拉取最新更新。
+ 这是最方便、最推荐的更新方式。
+
+3. 手动下载最新版本覆盖更新
+ 点击左上角的版本号或 `更新` 按钮。
+ 下载最新版本到本地后,直接覆盖现有扩展文件夹即可。
+
+4. 更新完成后重新加载扩展
+ 不论使用哪种更新方式,更新完成后都必须打开浏览器的 `扩展程序管理` 页面。
+ 找到该扩展后,手动点击一次 `重新加载`。
+ 这一步一定要做,否则浏览器可能仍在使用旧版本。
+
+### 第二部分:配置 `Clash Verge` 的 `🔁 非港轮询`
+
+#### 第一步:添加扩展脚本
+
+1. 打开 `Clash Verge`,进入左侧的 `订阅`(`Profiles`)界面。
+2. 在右上角或对应位置找到并双击打开 `全局扩展脚本`(`Merge/Script`)。
+3. 将里面的内容全部清空,替换为下方格式化好的代码。
+4. 点击保存,使用右上角保存按钮或按 `Ctrl+S`。
+
+💻 格式化后的代码(如果遇到格式错误,则让豆包修复一下)
+
+```javascript
+function uniqPrepend(arr, items) {
+ if (!Array.isArray(arr)) arr = [];
+ for (var i = items.length - 1; i >= 0; i--) {
+ var item = items[i];
+ var exists = false;
+ for (var j = 0; j < arr.length; j++) {
+ if (arr[j] === item) {
+ exists = true;
+ break;
+ }
+ }
+ if (!exists) arr.unshift(item);
+ }
+ return arr;
+}
+
+function upsertGroup(groups, group) {
+ for (var i = 0; i < groups.length; i++) {
+ if (groups[i] && groups[i].name === group.name) {
+ groups[i] = group;
+ return groups;
+ }
+ }
+ groups.unshift(group);
+ return groups;
+}
+
+function main(config, profileName) {
+ if (!config) return config;
+
+ if (!Array.isArray(config["proxy-groups"])) {
+ config["proxy-groups"] = [];
+ }
+
+ var groups = config["proxy-groups"];
+ var LB_NAME = "🔁 非港轮询";
+
+ var excludeRegex =
+ "(?i)(" +
+ "香港|hong[ -]?kong|\\bhk\\b|\\bhkg\\b|🇭🇰" +
+ "|剩余流量|套餐到期|下次重置剩余|重置剩余|到期时间|流量重置" +
+ "|traffic|expire|expiration|subscription|subscribe|reset|plan" +
+ ")";
+
+ groups = upsertGroup(groups, {
+ name: LB_NAME,
+ type: "load-balance",
+ strategy: "round-robin",
+ "include-all-proxies": true,
+ "exclude-filter": excludeRegex,
+ url: "https://www.gstatic.com/generate_204",
+ interval: 300,
+ lazy: true,
+ "expected-status": 204
+ });
+
+ var injected = false;
+ var entryNameRegex = /节点选择|代理|Proxy|PROXY|默认|GLOBAL|全局|选择/i;
+
+ for (var i = 0; i < groups.length; i++) {
+ var g = groups[i];
+ if (!g || g.type !== "select") continue;
+
+ if (entryNameRegex.test(g.name || "")) {
+ if (!Array.isArray(g.proxies)) g.proxies = [];
+ g.proxies = uniqPrepend(g.proxies, [LB_NAME]);
+ injected = true;
+ }
+ }
+
+ if (!injected) {
+ for (var k = 0; k < groups.length; k++) {
+ var g2 = groups[k];
+ if (g2 && g2.type === "select") {
+ if (!Array.isArray(g2.proxies)) g2.proxies = [];
+ g2.proxies = uniqPrepend(g2.proxies, [LB_NAME]);
+ break;
+ }
+ }
+ }
+
+ config["proxy-groups"] = groups;
+ return config;
+}
+```
+
+#### 第二步:应用设置
+
+1. 切换到左侧的 `代理`(`Proxies`)界面,也就是首页。
+2. 在顶部的分类中,通常会看到 `节点选择`、`Proxy` 或 `当前节点` 之类的分组。
+3. 在对应下拉框中选择 `🔁 非港轮询`。
+4. 确认右上角的 `代理模式`(`Mode`)已经设置为 `规则模式`(`Rule`)。
+5. 确认左侧 `设置` 中的 `系统代理`(`System Proxy`)已经开启。
+
+
+## 注意事项
+
+- 不论使用哪种方式更新扩展,更新完成后都必须在浏览器的 `扩展程序管理` 页面重新加载一次该扩展。
+- 如果你长期更新扩展,优先使用 `git pull`,这是最方便、最推荐的方式。
+- 使用手动覆盖更新时,请直接覆盖当前扩展文件夹,避免同时保留多份不同版本的目录。
+- 在 `Clash Verge` 中粘贴脚本时,请先清空旧内容,再完整粘贴新代码并保存。
diff --git a/manifest.json b/manifest.json
index c58c1fa..ced3774 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,6 +1,6 @@
{
"manifest_version": 3,
- "name": "多页面自动化",
+ "name": "codex-oauth-automation-extension",
"version": "5.8",
"version_name": "Pro5.8",
"description": "用于自动执行多步骤 OAuth 注册流程",
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 6b75d35..6b99430 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -4,7 +4,7 @@
Date: Wed, 22 Apr 2026 16:23:16 +0800
Subject: [PATCH 02/10] feat: improve Cloudflare Temp Email random subdomain
flow and sidepanel UX
---
background.js | 3 +
background/generated-email-helpers.js | 1 +
sidepanel/sidepanel.css | 13 +
sidepanel/sidepanel.html | 75 +++--
sidepanel/sidepanel.js | 126 +++++++-
...und-cloudflare-temp-email-settings.test.js | 145 +++++++++
.../background-generated-email-module.test.js | 169 +++++++++-
...dflare-temp-email-random-subdomain.test.js | 297 ++++++++++++++++++
tests/sidepanel-contribution-mode.test.js | 3 +
9 files changed, 792 insertions(+), 40 deletions(-)
create mode 100644 tests/background-cloudflare-temp-email-settings.test.js
create mode 100644 tests/sidepanel-cloudflare-temp-email-random-subdomain.test.js
diff --git a/background.js b/background.js
index 7297909..3b68339 100644
--- a/background.js
+++ b/background.js
@@ -280,6 +280,7 @@ const PERSISTED_SETTING_DEFAULTS = {
cloudflareTempEmailAdminAuth: '',
cloudflareTempEmailCustomAuth: '',
cloudflareTempEmailReceiveMailbox: '',
+ cloudflareTempEmailUseRandomSubdomain: false,
cloudflareTempEmailDomain: '',
cloudflareTempEmailDomains: [],
hotmailAccounts: [],
@@ -827,6 +828,7 @@ function getCloudflareTempEmailConfig(state = {}) {
adminAuth: String(state.cloudflareTempEmailAdminAuth || ''),
customAuth: String(state.cloudflareTempEmailCustomAuth || ''),
receiveMailbox: normalizeCloudflareTempEmailReceiveMailbox(state.cloudflareTempEmailReceiveMailbox),
+ useRandomSubdomain: Boolean(state.cloudflareTempEmailUseRandomSubdomain),
domain: normalizeCloudflareTempEmailDomain(state.cloudflareTempEmailDomain),
domains: normalizeCloudflareTempEmailDomains(state.cloudflareTempEmailDomains),
};
@@ -895,6 +897,7 @@ function normalizePersistentSettingValue(key, value) {
return normalizeEmailGenerator(value);
case 'autoDeleteUsedIcloudAlias':
case 'accountRunHistoryTextEnabled':
+ case 'cloudflareTempEmailUseRandomSubdomain':
return Boolean(value);
case 'icloudHostPreference':
return normalizeIcloudHost(value) || 'auto';
diff --git a/background/generated-email-helpers.js b/background/generated-email-helpers.js
index 3d924a7..7db5c98 100644
--- a/background/generated-email-helpers.js
+++ b/background/generated-email-helpers.js
@@ -146,6 +146,7 @@
const requestedName = String(options.localPart || options.name || '').trim().toLowerCase() || generateCloudflareAliasLocalPart();
const payload = {
enablePrefix: true,
+ enableRandomSubdomain: Boolean(config.useRandomSubdomain),
name: requestedName,
domain: config.domain,
};
diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css
index f40f857..aa9e607 100644
--- a/sidepanel/sidepanel.css
+++ b/sidepanel/sidepanel.css
@@ -2393,6 +2393,19 @@ header {
line-height: 1.55;
color: var(--text-secondary);
margin-bottom: 14px;
+ white-space: pre-line;
+}
+
+.modal-message a,
+.modal-alert a {
+ color: var(--blue);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+
+.modal-message a:hover,
+.modal-alert a:hover {
+ color: var(--cyan);
}
.modal-alert {
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 6b75d35..7cb4fba 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -235,31 +235,6 @@
-
- Temp API
-
-
-
- Admin Auth
-
-
-
- Custom Auth
-
-
-
- 邮件接收
-
-
-
CF 域名
@@ -389,6 +364,56 @@
+
+
+
+ Temp API
+
+
+
+ Admin Auth
+
+
+
+ Custom Auth
+
+
+
+ 邮件接收
+
+
+
+
随机子域
+
+
+ 依赖后端 RANDOM_SUBDOMAIN_DOMAINS
+
+
+
+
@@ -192,19 +195,19 @@ function isLocalhostOAuthCallbackUrl(rawUrl) {
function getStatusBadgeSelectors() {
return [
- '#root > div > div > div > main > div > div > div > div > div:nth-child(1) > div > div.OAuthPage-module__cardContent___1sXLA > div.status-badge',
- '#root .OAuthPage-module__cardContent___1sXLA > .status-badge',
- '.OAuthPage-module__cardContent___1sXLA > .status-badge',
+ '#root .OAuthPage-module__cardContent___1sXLA .status-badge',
+ '[class*="cardContent"] .status-badge',
'.status-badge',
];
}
function getStatusBadgeEntries() {
+ const searchRoot = findCodexOAuthCard() || document;
const seen = new Set();
const entries = [];
for (const selector of getStatusBadgeSelectors()) {
- const candidates = document.querySelectorAll(selector);
+ const candidates = searchRoot.querySelectorAll(selector);
for (const candidate of candidates) {
if (seen.has(candidate)) continue;
seen.add(candidate);
@@ -238,21 +241,63 @@ function normalizeStep9StatusText(statusText) {
}
function isOAuthCallbackTimeoutFailure(statusText) {
- return /认证失败:\s*(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded)/i.test(statusText || '');
+ return /(?:认证失败\s*[::]?\s*)?(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded|OAuth flow timed out)/i.test(statusText || '');
+}
+
+function getStep10StatusBadgeLocation(element) {
+ if (element?.closest?.('[class*="callbackSection"]')) {
+ return 'callback';
+ }
+ if (element?.closest?.('[class*="cardContent"]')) {
+ return 'main';
+ }
+ return 'page';
+}
+
+function isStep10CallbackSubmittedStatus(statusText) {
+ const text = normalizeStep9StatusText(statusText);
+ return /回调\s*url\s*已提交.*等待认证中/i.test(text)
+ || /callback\s*url\s*submitted.*waiting/i.test(text);
+}
+
+function isStep10CallbackFailureText(statusText) {
+ const text = normalizeStep9StatusText(statusText);
+ if (!text) return false;
+ return /(?:回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[::,,]?\s*/i.test(text)
+ || /请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(text);
+}
+
+function isStep10MainWaitingStatus(statusText) {
+ const text = normalizeStep9StatusText(statusText);
+ return /等待认证中/i.test(text);
+}
+
+function isStep10MainFailureText(statusText) {
+ const text = normalizeStep9StatusText(statusText);
+ if (!text) return false;
+ if (/^认证失败\s*[::]?\s*/i.test(text)) return true;
+ return /bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out|request failed with status code \d+|timeout of \d+ms exceeded|network error|failed to fetch/i.test(text);
}
function isStep9FailureText(statusText) {
const text = normalizeStep9StatusText(statusText);
if (!text) return false;
if (isOAuthCallbackTimeoutFailure(text)) return true;
+ if (isStep10CallbackFailureText(text)) return true;
+ if (isStep10MainFailureText(text)) return true;
if (typeof isRecoverableStep9AuthFailure === 'function' && isRecoverableStep9AuthFailure(text)) {
return true;
}
- return /回调\s*url\s*提交失败|callback\s*url\s*submit\s*failed|oauth flow is not pending/i.test(text);
+ return /callback\s*url\s*submit\s*failed|oauth flow is not pending/i.test(text);
}
function isStep9SuccessStatus(statusText) {
- return STEP9_SUCCESS_STATUSES.has(normalizeStep9StatusText(statusText));
+ const text = normalizeStep9StatusText(statusText);
+ if (!text) return false;
+ return STEP9_SUCCESS_STATUSES.has(text)
+ || /^认证成功[!!]?$/i.test(text)
+ || /^Authentication successful!?$/i.test(text)
+ || /^Аутентификация успешна!?$/i.test(text);
}
function isStep9SuccessLikeStatus(statusText) {
@@ -340,6 +385,7 @@ function createStep9Entry(candidate, selector) {
return {
element: candidate,
selector,
+ location: getStep10StatusBadgeLocation(candidate),
visible: isVisibleElement(candidate),
text: normalizeStep9StatusText(candidate?.textContent || ''),
className,
@@ -364,36 +410,58 @@ function getStep9PageErrorSelectors() {
}
function getStep9PageErrorEntries() {
+ const cardRoot = findCodexOAuthCard();
+ const searchRoots = [cardRoot, document].filter(Boolean);
const seen = new Set();
const entries = [];
- for (const selector of getStep9PageErrorSelectors()) {
- const candidates = document.querySelectorAll(selector);
- for (const candidate of candidates) {
- if (seen.has(candidate)) continue;
- seen.add(candidate);
- if (!isVisibleElement(candidate)) continue;
+ for (const root of searchRoots) {
+ for (const selector of getStep9PageErrorSelectors()) {
+ const candidates = root.querySelectorAll(selector);
+ for (const candidate of candidates) {
+ if (seen.has(candidate)) continue;
+ seen.add(candidate);
+ if (!isVisibleElement(candidate)) continue;
- const entry = createStep9Entry(candidate, selector);
- if (!isStep9FailureText(entry.text)) continue;
- entries.push(entry);
+ const entry = createStep9Entry(candidate, selector);
+ if (/\bstatus-badge\b/i.test(entry.className)) continue;
+ if (!isStep9FailureText(entry.text)) continue;
+ entries.push(entry);
+ }
}
}
return entries;
}
+function formatStep10StatusSummaryValue(text, emptyText = '无') {
+ return text ? `"${getInlineTextSnippet(text, 80)}"` : emptyText;
+}
+
function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSnippet = '') {
const visibleEntries = entries.filter((entry) => entry.visible);
- const successLikeEntries = visibleEntries.filter((entry) => isStep9SuccessLikeStatus(entry.text));
- const exactSuccessEntries = visibleEntries.filter((entry) => isStep9SuccessStatus(entry.text) && !entry.hasErrorVisualSignal);
- const failureEntries = visibleEntries.filter((entry) => isStep9FailureText(entry.text));
+ const callbackEntries = visibleEntries.filter((entry) => entry.location === 'callback');
+ const mainEntries = visibleEntries.filter((entry) => entry.location === 'main');
+ const successLikeEntries = mainEntries.filter((entry) => isStep9SuccessLikeStatus(entry.text));
+ const exactSuccessEntries = mainEntries.filter((entry) => isStep9SuccessStatus(entry.text) && !entry.hasErrorVisualSignal);
+ const callbackSubmittedEntries = callbackEntries.filter((entry) => isStep10CallbackSubmittedStatus(entry.text) && !entry.hasErrorVisualSignal);
+ const callbackFailureEntries = callbackEntries.filter((entry) => isStep10CallbackFailureText(entry.text));
+ const mainWaitingEntries = mainEntries.filter((entry) => isStep10MainWaitingStatus(entry.text) && !entry.hasErrorVisualSignal);
+ const mainFailureEntries = mainEntries.filter((entry) => isStep10MainFailureText(entry.text));
+ const failureEntries = [...callbackFailureEntries, ...mainFailureEntries];
const errorStyledEntries = visibleEntries.filter((entry) => entry.hasErrorVisualSignal);
const allFailureEntries = [...failureEntries, ...pageErrorEntries];
const decisiveFailureEntry = allFailureEntries[0] || null;
- const selectedEntry = decisiveFailureEntry || exactSuccessEntries[0] || visibleEntries[0] || null;
+ const selectedEntry = decisiveFailureEntry
+ || exactSuccessEntries[0]
+ || callbackSubmittedEntries[0]
+ || mainWaitingEntries[0]
+ || visibleEntries[0]
+ || null;
const selectedText = selectedEntry?.text || '';
const visibleSummary = summarizeStatusBadgeEntries(visibleEntries);
+ const callbackSummary = summarizeStatusBadgeEntries(callbackEntries);
+ const mainSummary = summarizeStatusBadgeEntries(mainEntries);
const successLikeSummary = summarizeStatusBadgeEntries(successLikeEntries);
const exactSuccessSummary = summarizeStatusBadgeEntries(exactSuccessEntries);
const failureSummary = summarizeStatusBadgeEntries(failureEntries);
@@ -406,10 +474,20 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn
selectedText,
exactSuccessText: exactSuccessEntries[0]?.text || '',
failureText: decisiveFailureEntry?.text || '',
+ failureSource: decisiveFailureEntry?.location || (pageErrorEntries.length ? 'page' : ''),
visibleCount: visibleEntries.length,
visibleSummary,
+ callbackSummary,
+ mainSummary,
+ callbackStatusText: callbackEntries[0]?.text || '',
+ callbackSubmittedText: callbackSubmittedEntries[0]?.text || '',
+ callbackFailureText: callbackFailureEntries[0]?.text || '',
+ mainStatusText: mainEntries[0]?.text || '',
+ mainWaitingText: mainWaitingEntries[0]?.text || '',
+ mainFailureText: mainFailureEntries[0]?.text || '',
hasSuccessLikeVisibleBadge: successLikeEntries.length > 0,
hasExactSuccessVisibleBadge: exactSuccessEntries.length > 0,
+ hasCallbackSubmittedBadge: callbackSubmittedEntries.length > 0,
hasFailureVisibleBadge: allFailureEntries.length > 0,
hasErrorStyledVisibleBadge: errorStyledEntries.length > 0,
successLikeSummary,
@@ -422,6 +500,8 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn
selectedText,
visibleCount: visibleEntries.length,
visibleSummary,
+ callbackSummary,
+ mainSummary,
successLikeSummary,
exactSuccessSummary,
failureSummary,
@@ -429,8 +509,8 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn
errorStyledSummary,
}),
summary: selectedText
- ? `当前聚焦状态="${getInlineTextSnippet(selectedText, 80)}";可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix}`
- : `当前未选中任何可见状态徽标;可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`,
+ ? `当前聚焦状态=${formatStep10StatusSummaryValue(selectedText)};回调提示=${formatStep10StatusSummaryValue(callbackEntries[0]?.text || '')};主状态=${formatStep10StatusSummaryValue(mainEntries[0]?.text || '')};页面错误=${formatStep10StatusSummaryValue(pageErrorEntries[0]?.text || '')};可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix}`
+ : `当前未选中任何可见状态;回调提示=${formatStep10StatusSummaryValue(callbackEntries[0]?.text || '')};主状态=${formatStep10StatusSummaryValue(mainEntries[0]?.text || '')};页面错误=${formatStep10StatusSummaryValue(pageErrorEntries[0]?.text || '')};可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`,
};
}
@@ -452,11 +532,129 @@ function getStatusBadgeText() {
return diagnostics.selectedText;
}
+function extractStep10FailureDetail(statusText, sourceKind = '') {
+ const text = normalizeStep9StatusText(statusText);
+ if (!text) return '';
+ if (sourceKind === 'callback' || isStep10CallbackFailureText(text)) {
+ return text.replace(/^(?:回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[::,,]?\s*/i, '').trim();
+ }
+ if (sourceKind === 'main' || isStep10MainFailureText(text)) {
+ return text.replace(/^认证失败\s*[::]?\s*/i, '').trim();
+ }
+ return text;
+}
+
+function explainStep10Failure(statusText, sourceKind = 'unknown') {
+ const rawText = normalizeStep9StatusText(statusText);
+ const detail = extractStep10FailureDetail(rawText, sourceKind) || rawText;
+ const phaseLabel = sourceKind === 'callback'
+ ? '回调提交阶段'
+ : sourceKind === 'main'
+ ? '认证结果阶段'
+ : '页面状态阶段';
+
+ const rules = [
+ {
+ code: 'callback_submit_api_unavailable',
+ pattern: /请更新\s*cli\s*proxy\s*api\s*或检查连接/i,
+ message: 'CPA 面板无法把回调提交给后台,通常是 CLI Proxy API 版本过旧、管理接口未启动,或当前面板与后端连接异常。',
+ },
+ {
+ code: 'oauth_state_expired',
+ pattern: /unknown or expired state/i,
+ message: '当前 OAuth 会话在 CPA 中已不存在或已过期,通常是使用了旧回调链接、刷新过新的授权链接后仍提交旧链接,或 CPA 刚重启过。',
+ },
+ {
+ code: 'oauth_not_pending',
+ pattern: /oauth flow is not pending/i,
+ message: '当前 OAuth 会话已经不在等待状态,通常是重复提交、提交过慢,或这轮认证此前已经结束。',
+ },
+ {
+ code: 'callback_state_invalid',
+ pattern: /invalid state|state is required|missing_state/i,
+ message: '回调链接里的 state 缺失或无效,通常是复制了不完整的 localhost 回调链接,或提交了不属于这一轮的旧链接。',
+ },
+ {
+ code: 'callback_missing_result',
+ pattern: /code or error is required/i,
+ message: '回调链接里既没有授权码,也没有错误信息,通常是复制的 localhost 回调链接不完整。',
+ },
+ {
+ code: 'callback_invalid_url',
+ pattern: /invalid redirect_url/i,
+ message: '提交给 CPA 的回调链接格式无法解析,通常是粘贴内容不完整、带了多余字符,或并不是 localhost OAuth 回调地址。',
+ },
+ {
+ code: 'callback_provider_mismatch',
+ pattern: /provider does not match state/i,
+ message: '这条回调不属于当前这次 Codex OAuth,会话与回调来源对不上,通常是混用了其他轮次或其他提供方的回调。',
+ },
+ {
+ code: 'callback_persist_failed',
+ pattern: /failed to persist oauth callback/i,
+ message: 'CPA 已收到回调,但无法把回调结果写入本地缓存文件,通常是认证目录权限、磁盘或运行环境异常。',
+ },
+ {
+ code: 'oauth_bad_request',
+ pattern: /^bad request$/i,
+ message: 'CPA 已收到回调,但 OpenAI OAuth 回调本身返回了错误。常见于用户取消授权、请求过期,或这条回调已经失效。',
+ },
+ {
+ code: 'oauth_state_mismatch',
+ pattern: /state code error/i,
+ message: 'CPA 校验到回调里的 state 与当前 OAuth 会话不一致,通常是步骤 1 已刷新过新的授权链接,但步骤 10 仍提交旧回调。',
+ },
+ {
+ code: 'oauth_code_exchange_failed',
+ pattern: /failed to exchange authorization code for tokens/i,
+ message: 'CPA 已收到授权码,但向 OpenAI 交换令牌失败。常见于 CPA 到 OpenAI 的网络或代理异常,或授权码已过期。',
+ },
+ {
+ code: 'oauth_token_save_failed',
+ pattern: /failed to save authentication tokens/i,
+ message: 'CPA 已完成认证,但保存认证文件失败。常见于认证目录权限、磁盘写入,或 post-auth hook 异常。',
+ },
+ {
+ code: 'oauth_callback_timeout',
+ pattern: /timeout waiting for oauth callback|oauth flow timed out/i,
+ message: 'CPA 长时间没有把这轮 OAuth 流程走完。常见于提交太晚、面板轮询异常,或后端状态没有及时刷新。',
+ },
+ {
+ code: 'oauth_http_timeout',
+ pattern: /timeout of \d+ms exceeded/i,
+ message: 'CPA 面板在请求后台接口时超时,通常是 CLI Proxy API 响应过慢、接口未启动,或网络连接不稳定。',
+ },
+ {
+ code: 'oauth_http_status_error',
+ pattern: /request failed with status code \d+/i,
+ message: 'CPA 面板请求后台接口时收到了异常 HTTP 状态码,通常是接口异常、反向代理配置错误,或当前会话已失效。',
+ },
+ {
+ code: 'oauth_network_error',
+ pattern: /network error|failed to fetch/i,
+ message: 'CPA 面板与后台通信失败,通常是网络不通、管理接口未启动,或浏览器当前连接已断开。',
+ },
+ ];
+
+ const matchedRule = rules.find((rule) => rule.pattern.test(detail) || rule.pattern.test(rawText));
+ const message = matchedRule
+ ? matchedRule.message
+ : `CPA 在${phaseLabel}返回了未归类的失败,请结合面板原文进一步排查。`;
+
+ return {
+ code: matchedRule?.code || 'oauth_unknown_failure',
+ phaseLabel,
+ rawText,
+ detail,
+ userMessage: `CPA 在${phaseLabel}返回失败:${message} 面板原文:${rawText}`,
+ };
+}
+
async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS) {
const start = Date.now();
let lastDiagnosticsSignature = '';
let lastHeartbeatLoggedAt = 0;
- let lastSuccessLikeMismatchSignature = '';
+ let lastCallbackSubmittedSignature = '';
let lastSuccessFailureConflictSignature = '';
while (Date.now() - start < timeout) {
@@ -475,23 +673,18 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
console.log(LOG_PREFIX, '[Step 9] still waiting for success badge', diagnostics);
}
- if (diagnostics.hasSuccessLikeVisibleBadge && !diagnostics.hasExactSuccessVisibleBadge) {
- const mismatchSignature = JSON.stringify({
- selectedText: diagnostics.selectedText,
- successLikeSummary: diagnostics.successLikeSummary,
- visibleSummary: diagnostics.visibleSummary,
- errorStyledSummary: diagnostics.errorStyledSummary,
+ if (diagnostics.hasCallbackSubmittedBadge && !diagnostics.hasExactSuccessVisibleBadge) {
+ const callbackSubmittedSignature = JSON.stringify({
+ callbackStatusText: diagnostics.callbackStatusText,
+ mainStatusText: diagnostics.mainStatusText,
});
- if (mismatchSignature !== lastSuccessLikeMismatchSignature) {
- lastSuccessLikeMismatchSignature = mismatchSignature;
- const errorStyledSuffix = diagnostics.hasErrorStyledVisibleBadge
- ? `;错误样式徽标:${diagnostics.errorStyledSummary}`
- : '';
+ if (callbackSubmittedSignature !== lastCallbackSubmittedSignature) {
+ lastCallbackSubmittedSignature = callbackSubmittedSignature;
log(
- `步骤 10:检测到“认证成功”相关徽标,但未命中精确成功条件。当前聚焦="${getInlineTextSnippet(diagnostics.selectedText || '(空)', 80)}";成功相关徽标:${diagnostics.successLikeSummary}${errorStyledSuffix}`,
- 'warn'
+ `步骤 10:CPA 已接受 localhost 回调,正在等待后台完成认证。回调提示=${formatStep10StatusSummaryValue(diagnostics.callbackStatusText)};主状态=${formatStep10StatusSummaryValue(diagnostics.mainStatusText)}`,
+ 'info'
);
- console.warn(LOG_PREFIX, '[Step 9] success-like badge detected without exact match', diagnostics);
+ console.info(LOG_PREFIX, '[Step 9] callback accepted and waiting for auth completion', diagnostics);
}
}
@@ -515,10 +708,11 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
}
if (diagnostics.failureText) {
+ const failureExplanation = explainStep10Failure(diagnostics.failureText, diagnostics.failureSource || 'unknown');
if (isOAuthCallbackTimeoutFailure(diagnostics.failureText)) {
- throw new Error(`STEP9_OAUTH_TIMEOUT::${diagnostics.failureText}`);
+ throw new Error(`STEP9_OAUTH_TIMEOUT::${failureExplanation.userMessage}`);
}
- throw new Error(`STEP9_OAUTH_RETRY::${diagnostics.failureText}`);
+ throw new Error(`STEP9_OAUTH_RETRY::${failureExplanation.userMessage}`);
}
if (diagnostics.exactSuccessText) {
return diagnostics.exactSuccessText;
@@ -530,10 +724,18 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
const finalText = finalDiagnostics.failureText || finalDiagnostics.selectedText;
const diagnosticsSuffix = ` 当前诊断:${finalDiagnostics.summary}`;
if (isOAuthCallbackTimeoutFailure(finalText)) {
- throw new Error(`STEP9_OAUTH_TIMEOUT::${finalText}${diagnosticsSuffix}`);
+ const failureExplanation = explainStep10Failure(finalText, finalDiagnostics.failureSource || 'main');
+ throw new Error(`STEP9_OAUTH_TIMEOUT::${failureExplanation.userMessage}${diagnosticsSuffix}`);
}
if (isStep9FailureText(finalText)) {
- throw new Error(`STEP9_OAUTH_RETRY::${finalText}${diagnosticsSuffix}`);
+ const failureExplanation = explainStep10Failure(finalText, finalDiagnostics.failureSource || 'unknown');
+ throw new Error(`STEP9_OAUTH_RETRY::${failureExplanation.userMessage}${diagnosticsSuffix}`);
+ }
+ if (finalDiagnostics.hasCallbackSubmittedBadge || finalDiagnostics.mainWaitingText) {
+ throw new Error(
+ 'STEP9_OAUTH_TIMEOUT::CPA 已接受回调,但 120 秒内仍未进入认证成功状态。通常是 CPA 后台处理过慢、面板轮询异常,或 CPA 到 OpenAI 的网络/代理存在问题。'
+ + diagnosticsSuffix
+ );
}
throw new Error(finalText
? `CPA 面板状态未进入成功状态,当前为“${finalText}”。${diagnosticsSuffix}`
@@ -583,6 +785,11 @@ function findCodexOAuthHeader() {
}) || null;
}
+function findCodexOAuthCard() {
+ const header = findCodexOAuthHeader();
+ return header?.closest('.card, [class*="card"]') || header || null;
+}
+
function findOAuthCardLoginButton(header) {
const card = header?.closest('.card, [class*="card"]') || header?.parentElement || document;
const candidates = card.querySelectorAll('button.btn.btn-primary, button.btn-primary, button.btn');
diff --git a/docs/使用教程.md b/docs/使用教程.md
index 90eda7b..431d716 100644
--- a/docs/使用教程.md
+++ b/docs/使用教程.md
@@ -1,24 +1,41 @@
-# Codex 注册扩展更新与 Clash Verge 非港轮询配置教程
-
-本教程用于说明如何更新 `Codex 注册扩展`,以及如何在 `Clash Verge` 中配置 `🔁 非港轮询`。
+本教程用于说明相关项目地址、扩展更新方式,以及 `Clash Verge` 的 `🔁 非港轮询` 配置方法。
## 适用场景
+- 需要拉取并部署 `cpa` 或 `sub2api` 项目
- 已经安装本扩展,想更新到最新版本
- 想用更方便的方式长期同步扩展更新
- 需要在 `Clash Verge` 中启用 `🔁 非港轮询`
## 准备内容
+- 可以访问 `GitHub`
- 已安装好的扩展文件夹
- 可以打开浏览器的 `扩展程序管理` 页面
-- 如需使用 `git pull`,本地已安装 `git`
-- 如需使用 `GitHub Desktop`,本地已安装 `GitHub Desktop`
+- 如需部署 `cpa`,部署环境必须可以访问 `OpenAI`
- 已安装 `Clash Verge`,并已导入可用订阅
## 操作步骤
-### 第一部分:更新扩展
+### 第一部分:相关项目地址与部署说明
+
+1. 查看项目地址
+ `cpa` 项目地址:
+ `sub2api` 项目地址:
+
+2. 拉取项目到本地
+ 先将你需要的项目拉取到本地目录。
+ 可以使用 `git clone`,也可以直接下载项目压缩包后解压。
+
+3. 让 AI 直接协助部署
+ 部署教程这里不单独展开。
+ 将项目拉取下来后,直接让 AI 帮你部署即可。
+
+4. 确认 `cpa` 的部署环境
+ 如果你部署的是 `cpa`,部署所在环境必须可以访问 `OpenAI`。
+ 否则可能会出现第十步显示认证成功,但实际上没有生成认证文件的情况。
+
+### 第二部分:更新扩展
1. 使用 `GitHub Desktop` 更新
先安装 `GitHub Desktop`。
@@ -40,16 +57,16 @@
找到该扩展后,手动点击一次 `重新加载`。
这一步一定要做,否则浏览器可能仍在使用旧版本。
-### 第二部分:配置 `Clash Verge` 的 `🔁 非港轮询`
+### 第三部分:配置 `Clash Verge` 的 `🔁 非港轮询`
#### 第一步:添加扩展脚本
1. 打开 `Clash Verge`,进入左侧的 `订阅`(`Profiles`)界面。
2. 在右上角或对应位置找到并双击打开 `全局扩展脚本`(`Merge/Script`)。
-3. 将里面的内容全部清空,替换为下方格式化好的代码。
+3. 将里面的内容全部清空,替换为下方脚本代码。
4. 点击保存,使用右上角保存按钮或按 `Ctrl+S`。
-💻 格式化后的代码(如果遇到格式错误,则让豆包修复一下)
+💻 脚本代码(如果遇到格式错误,可让豆包帮你修复后再粘贴)
```javascript
function uniqPrepend(arr, items) {
@@ -146,10 +163,28 @@ function main(config, profileName) {
4. 确认右上角的 `代理模式`(`Mode`)已经设置为 `规则模式`(`Rule`)。
5. 确认左侧 `设置` 中的 `系统代理`(`System Proxy`)已经开启。
+## 常见问题
+
+### 为什么第十步显示认证成功,但没有认证文件?
+
+这通常是因为 `cpa` 部署所在环境无法访问 `OpenAI`。请先确认部署环境可以正常访问 `OpenAI`,然后再重新执行相关认证步骤。
+
+### 为什么更新完扩展后还是旧版本?
+
+如果你只是替换了本地文件,但没有去浏览器的 `扩展程序管理` 页面点击 `重新加载`,浏览器不会立即启用新版本。请更新完成后手动重新加载一次该扩展。
+
+### `git pull` 提示不是 Git 仓库怎么办?
+
+这通常说明你进入的不是正确目录,或者当前文件夹不是通过 `git` 管理的版本。此时可以改用 `GitHub Desktop`,或者直接使用手动下载覆盖的方式更新。
+
+### 为什么没有看到 `🔁 非港轮询`?
+
+请先确认脚本已经完整粘贴并保存,然后回到 `代理` 页面重新查看。若仍未出现,请继续确认当前订阅可用、`代理模式` 为 `规则模式`,并且 `系统代理` 已开启。
## 注意事项
- 不论使用哪种方式更新扩展,更新完成后都必须在浏览器的 `扩展程序管理` 页面重新加载一次该扩展。
+- 如需部署 `cpa`,请先确认部署环境可以访问 `OpenAI`,否则可能出现第十步认证成功但没有认证文件的问题。
- 如果你长期更新扩展,优先使用 `git pull`,这是最方便、最推荐的方式。
- 使用手动覆盖更新时,请直接覆盖当前扩展文件夹,避免同时保留多份不同版本的目录。
-- 在 `Clash Verge` 中粘贴脚本时,请先清空旧内容,再完整粘贴新代码并保存。
+- 在 `Clash Verge` 中粘贴脚本时,请先清空旧内容,再完整粘贴新代码并保存。
\ No newline at end of file
diff --git a/tests/activation-utils.test.js b/tests/activation-utils.test.js
index 181c597..6f3a4ba 100644
--- a/tests/activation-utils.test.js
+++ b/tests/activation-utils.test.js
@@ -81,6 +81,16 @@ test('isRecoverableStep9AuthFailure matches timeout and CPA auth failure statuse
true
);
+ assert.equal(
+ isRecoverableStep9AuthFailure('提交回调失败:请更新CLI Proxy API或检查连接'),
+ true
+ );
+
+ assert.equal(
+ isRecoverableStep9AuthFailure('认证失败:Bad Request'),
+ true
+ );
+
assert.equal(
isRecoverableStep9AuthFailure('认证成功!'),
false
diff --git a/tests/step9-status-diagnostics.test.js b/tests/step9-status-diagnostics.test.js
index 17512c5..2f9c9a1 100644
--- a/tests/step9-status-diagnostics.test.js
+++ b/tests/step9-status-diagnostics.test.js
@@ -58,23 +58,32 @@ const bundle = [
extractFunction('summarizeStatusBadgeEntries'),
extractFunction('normalizeStep9StatusText'),
extractFunction('isOAuthCallbackTimeoutFailure'),
+ extractFunction('isStep10CallbackSubmittedStatus'),
+ extractFunction('isStep10CallbackFailureText'),
+ extractFunction('isStep10MainWaitingStatus'),
+ extractFunction('isStep10MainFailureText'),
extractFunction('isStep9FailureText'),
extractFunction('isStep9SuccessStatus'),
extractFunction('isStep9SuccessLikeStatus'),
+ extractFunction('formatStep10StatusSummaryValue'),
extractFunction('buildStep9StatusDiagnostics'),
+ extractFunction('extractStep10FailureDetail'),
+ extractFunction('explainStep10Failure'),
].join('\n');
function createApi() {
return new Function(`
function isRecoverableStep9AuthFailure(text) {
- return /(?:认证失败|回调 URL 提交失败):\\s*/i.test(String(text || '').trim())
- || /oauth flow is not pending/i.test(String(text || '').trim());
+ const normalized = String(text || '').trim();
+ return /(?:认证失败|回调\\s*url\\s*提交失败|回调url提交失败|提交回调失败)\\s*[::]?/i.test(normalized)
+ || /oauth flow is not pending|请更新\\s*cli\\s*proxy\\s*api\\s*或检查连接|bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out/i.test(normalized);
}
${bundle}
return {
buildStep9StatusDiagnostics,
+ explainStep10Failure,
};
`)();
}
@@ -86,6 +95,7 @@ test('step 9 does not treat red success badges as exact success', () => {
visible: true,
text: '认证成功!',
className: 'status-badge text-danger',
+ location: 'main',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
@@ -104,23 +114,89 @@ test('step 9 keeps failure state dominant when success badge and error banner co
visible: true,
text: '认证成功!',
className: 'status-badge',
+ location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
- ],
- [
{
visible: true,
text: '回调 URL 提交失败: oauth flow is not pending',
- className: 'alert alert-danger',
+ className: 'status-badge error',
+ location: 'callback',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
],
+ [],
'page'
);
assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
assert.equal(diagnostics.hasFailureVisibleBadge, true);
assert.equal(diagnostics.failureText, '回调 URL 提交失败: oauth flow is not pending');
+ assert.equal(diagnostics.failureSource, 'callback');
+});
+
+test('step 10 treats plain 认证成功 as success when no failure is visible', () => {
+ const api = createApi();
+ const diagnostics = api.buildStep9StatusDiagnostics(
+ [
+ {
+ visible: true,
+ text: '认证成功',
+ className: 'status-badge',
+ location: 'main',
+ hasErrorVisualSignal: false,
+ errorVisualSummary: '',
+ },
+ ],
+ [],
+ 'page'
+ );
+
+ assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
+ assert.equal(diagnostics.exactSuccessText, '认证成功');
+});
+
+test('step 10 recognizes callback accepted badge as in-progress signal', () => {
+ const api = createApi();
+ const diagnostics = api.buildStep9StatusDiagnostics(
+ [
+ {
+ visible: true,
+ text: '回调 URL 已提交,等待认证中...',
+ className: 'status-badge success',
+ location: 'callback',
+ hasErrorVisualSignal: false,
+ errorVisualSummary: '',
+ },
+ {
+ visible: true,
+ text: '等待认证中...',
+ className: 'status-badge',
+ location: 'main',
+ hasErrorVisualSignal: false,
+ errorVisualSummary: '',
+ },
+ ],
+ [],
+ 'page'
+ );
+
+ assert.equal(diagnostics.hasCallbackSubmittedBadge, true);
+ assert.equal(diagnostics.callbackSubmittedText, '回调 URL 已提交,等待认证中...');
+ assert.equal(diagnostics.mainWaitingText, '等待认证中...');
+ assert.equal(diagnostics.hasExactSuccessVisibleBadge, false);
+});
+
+test('step 10 explains callback upgrade hint with user-friendly reason', () => {
+ const api = createApi();
+ const explanation = api.explainStep10Failure(
+ '回调 URL 提交失败: 请更新CLI Proxy API或检查连接',
+ 'callback'
+ );
+
+ assert.equal(explanation.code, 'callback_submit_api_unavailable');
+ assert.match(explanation.userMessage, /CLI Proxy API 版本过旧|管理接口未启动|连接异常/);
+ assert.match(explanation.userMessage, /回调提交阶段/);
});
From a6967240c7e5a4f394925643fc9a0161a8a50ec6 Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Wed, 22 Apr 2026 18:19:17 +0800
Subject: [PATCH 04/10] =?UTF-8?q?fix:=20=E5=B0=86=20OAuth=20=E6=B5=81?=
=?UTF-8?q?=E7=A8=8B=E8=B6=85=E6=97=B6=E8=AE=BE=E7=BD=AE=E4=BB=8E=206=20?=
=?UTF-8?q?=E5=88=86=E9=92=9F=E8=B0=83=E6=95=B4=E4=B8=BA=205=20=E5=88=86?=
=?UTF-8?q?=E9=92=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
background.js | 4 ++--
tests/background-auth-chain-guard.test.js | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/background.js b/background.js
index 7297909..a07a4d9 100644
--- a/background.js
+++ b/background.js
@@ -151,7 +151,7 @@ const HUMAN_STEP_DELAY_MIN = 700;
const HUMAN_STEP_DELAY_MAX = 2200;
const STEP6_MAX_ATTEMPTS = 3;
const STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS = 8;
-const OAUTH_FLOW_TIMEOUT_MS = 6 * 60 * 1000;
+const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000;
const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000;
const DEFAULT_SUB2API_URL = 'https://sub2api.hisence.fun/admin/accounts';
@@ -6836,7 +6836,7 @@ async function startOAuthFlowTimeoutWindow(options = {}) {
oauthFlowDeadlineAt: deadlineAt,
oauthFlowDeadlineSourceUrl: normalizeOAuthFlowSourceUrl(options.oauthUrl),
});
- await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,开始 6 分钟倒计时。`, 'info');
+ await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,开始 ${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟倒计时。`, 'info');
return deadlineAt;
}
diff --git a/tests/background-auth-chain-guard.test.js b/tests/background-auth-chain-guard.test.js
index 3fec903..0353613 100644
--- a/tests/background-auth-chain-guard.test.js
+++ b/tests/background-auth-chain-guard.test.js
@@ -162,7 +162,7 @@ return {
test('oauth timeout budget ignores stale deadlines from an old oauth url', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
-const OAUTH_FLOW_TIMEOUT_MS = 6 * 60 * 1000;
+const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${extractFunction('normalizeOAuthFlowDeadlineAt')}
${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('getOAuthFlowRemainingMs')}
From 79e917345e7326ec669d0223ac8f40dff70af3e6 Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Wed, 22 Apr 2026 18:33:54 +0800
Subject: [PATCH 05/10] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=B5=8F?=
=?UTF-8?q?=E8=A7=88=E5=99=A8=E5=88=87=E6=8D=A2=E6=89=80=E9=9C=80=E9=94=99?=
=?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86=EF=BC=8C=E5=A2=9E=E5=BC=BA=E6=AD=A5?=
=?UTF-8?q?=E9=AA=A4=2010=20=E7=9A=84=E8=AF=8A=E6=96=AD=E4=BF=A1=E6=81=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
background.js | 23 ++++++
content/vps-panel.js | 21 +++++
tests/background-auth-chain-guard.test.js | 94 +++++++++++++++++++++++
tests/step9-status-diagnostics.test.js | 33 ++++++++
4 files changed, 171 insertions(+)
diff --git a/background.js b/background.js
index a07a4d9..93511a7 100644
--- a/background.js
+++ b/background.js
@@ -147,6 +147,7 @@ const HOTMAIL_MAILBOXES = ['INBOX', 'Junk'];
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX = 'CF_SECURITY_BLOCKED::';
const CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE = '您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器';
+const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::';
const HUMAN_STEP_DELAY_MIN = 700;
const HUMAN_STEP_DELAY_MAX = 2200;
const STEP6_MAX_ATTEMPTS = 3;
@@ -4000,6 +4001,17 @@ function getTerminalSecurityBlockedTitle(error) {
return 'Cloudflare 风控拦截';
}
+function isBrowserSwitchRequiredError(error) {
+ return getErrorMessage(error).startsWith(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX);
+}
+
+function getBrowserSwitchRequiredMessage(error) {
+ const message = getErrorMessage(error);
+ return message.startsWith(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX)
+ ? message.slice(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX.length).trim()
+ : message;
+}
+
function broadcastSecurityBlockedAlert(title = '流程已完全停止', message = CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE, alertText = '检测到 Cloudflare 风控,请暂停当前操作。') {
chrome.runtime.sendMessage({
type: 'SECURITY_BLOCKED_ALERT',
@@ -4023,6 +4035,13 @@ async function handleCloudflareSecurityBlocked(error) {
return message;
}
+async function handleBrowserSwitchRequired(error) {
+ const message = getBrowserSwitchRequiredMessage(error)
+ || '检测到第 10 步的特殊冲突状态,请更换浏览器后重新进行注册登录。';
+ await requestStop({ logMessage: message });
+ return message;
+}
+
function isVerificationMailPollingError(error) {
if (typeof loggingStatus !== 'undefined' && loggingStatus?.isVerificationMailPollingError) {
return loggingStatus.isVerificationMailPollingError(error);
@@ -5326,6 +5345,10 @@ async function executeStep(step, options = {}) {
await handleCloudflareSecurityBlocked(err);
throw new Error(STOP_ERROR_MESSAGE);
}
+ if (isBrowserSwitchRequiredError(err)) {
+ await handleBrowserSwitchRequired(err);
+ throw new Error(STOP_ERROR_MESSAGE);
+ }
if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step) && isRetryableContentScriptTransportError(err))) {
await setStepStatus(step, 'failed');
await addLog(`步骤 ${step} 失败:${err.message}`, 'error');
diff --git a/content/vps-panel.js b/content/vps-panel.js
index eef4337..63af31c 100644
--- a/content/vps-panel.js
+++ b/content/vps-panel.js
@@ -438,6 +438,20 @@ function formatStep10StatusSummaryValue(text, emptyText = '无') {
return text ? `"${getInlineTextSnippet(text, 80)}"` : emptyText;
}
+function isStep10BrowserSwitchRequiredConflict(diagnostics = {}) {
+ return Boolean(diagnostics?.hasExactSuccessVisibleBadge)
+ && /请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(String(diagnostics?.callbackFailureText || ''));
+}
+
+function getStep10BrowserSwitchRequiredMessage(diagnostics = {}) {
+ const callbackFailureText = normalizeStep9StatusText(diagnostics?.callbackFailureText || '');
+ return [
+ '检测到 CPA 页面同时显示“认证成功”和“回调 URL 提交失败: 请更新CLI Proxy API或检查连接”。',
+ '这类冲突状态通常通过更换浏览器可以解决,请更换浏览器后重新进行注册登录。',
+ callbackFailureText ? `面板原文:${callbackFailureText}` : '',
+ ].filter(Boolean).join(' ');
+}
+
function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSnippet = '') {
const visibleEntries = entries.filter((entry) => entry.visible);
const callbackEntries = visibleEntries.filter((entry) => entry.location === 'callback');
@@ -688,6 +702,13 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
}
}
+ if (isStep10BrowserSwitchRequiredConflict(diagnostics)) {
+ const browserSwitchMessage = getStep10BrowserSwitchRequiredMessage(diagnostics);
+ log(`步骤 10:${browserSwitchMessage}`, 'error');
+ console.error(LOG_PREFIX, '[Step 9] browser-switch conflict detected', diagnostics);
+ throw new Error(`BROWSER_SWITCH_REQUIRED::${browserSwitchMessage}`);
+ }
+
if (diagnostics.hasExactSuccessVisibleBadge && diagnostics.hasFailureVisibleBadge) {
const conflictSignature = JSON.stringify({
exactSuccessSummary: diagnostics.exactSuccessSummary,
diff --git a/tests/background-auth-chain-guard.test.js b/tests/background-auth-chain-guard.test.js
index 0353613..b781570 100644
--- a/tests/background-auth-chain-guard.test.js
+++ b/tests/background-auth-chain-guard.test.js
@@ -71,6 +71,7 @@ test('executeStep reuses the active top-level auth chain instead of starting a d
const api = new Function(`
const LOG_PREFIX = '[test]';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
+const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::';
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
let activeTopLevelAuthChainExecution = null;
let stopRequested = false;
@@ -106,6 +107,10 @@ function isTerminalSecurityBlockedError() {
return false;
}
async function handleCloudflareSecurityBlocked() {}
+function isBrowserSwitchRequiredError() {
+ return false;
+}
+async function handleBrowserSwitchRequired() {}
function doesStepUseCompletionSignal() {
return false;
}
@@ -159,6 +164,95 @@ return {
assert.ok(events.logs.some(({ message }) => /复用当前授权链/.test(message)));
});
+test('executeStep stops flow when browser-switch-required error is raised', async () => {
+ const api = new Function(`
+const LOG_PREFIX = '[test]';
+const STOP_ERROR_MESSAGE = '流程已被用户停止。';
+const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::';
+const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
+let activeTopLevelAuthChainExecution = null;
+let stopRequested = false;
+const events = {
+ logs: [],
+ statusCalls: [],
+ stopRequests: [],
+ appendRecords: [],
+};
+
+async function addLog(message, level = 'info') {
+ events.logs.push({ message, level });
+}
+async function setStepStatus(step, status) {
+ events.statusCalls.push({ step, status });
+}
+async function humanStepDelay() {}
+async function getState() {
+ return {
+ flowStartTime: null,
+ stepStatuses: {},
+ };
+}
+function getErrorMessage(error) {
+ return error?.message || String(error || '');
+}
+async function appendManualAccountRunRecordIfNeeded(status, _state, reason) {
+ events.appendRecords.push({ status, reason });
+}
+function isTerminalSecurityBlockedError() {
+ return false;
+}
+async function handleCloudflareSecurityBlocked() {}
+async function requestStop(options = {}) {
+ events.stopRequests.push(options);
+}
+function doesStepUseCompletionSignal() {
+ return false;
+}
+function isRetryableContentScriptTransportError() {
+ return false;
+}
+const stepRegistry = {
+ async executeStep() {
+ throw new Error('BROWSER_SWITCH_REQUIRED::请更换浏览器进行注册登录。');
+ },
+};
+
+${extractFunction('isStopError')}
+${extractFunction('throwIfStopped')}
+${extractFunction('isAuthChainStep')}
+${extractFunction('acquireTopLevelAuthChainExecution')}
+${extractFunction('isBrowserSwitchRequiredError')}
+${extractFunction('getBrowserSwitchRequiredMessage')}
+${extractFunction('handleBrowserSwitchRequired')}
+${extractFunction('executeStep')}
+
+return {
+ executeStep,
+ snapshot() {
+ return events;
+ },
+};
+`)();
+
+ await assert.rejects(
+ () => api.executeStep(10),
+ /流程已被用户停止。/
+ );
+
+ const events = api.snapshot();
+ assert.deepStrictEqual(events.stopRequests, [
+ { logMessage: '请更换浏览器进行注册登录。' },
+ ]);
+ assert.deepStrictEqual(events.statusCalls, [
+ { step: 10, status: 'running' },
+ ]);
+ assert.equal(
+ events.logs.some(({ message }) => /步骤 10 失败/.test(message)),
+ false,
+ 'browser-switch-required error should stop the flow before generic failed logging'
+ );
+});
+
test('oauth timeout budget ignores stale deadlines from an old oauth url', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
diff --git a/tests/step9-status-diagnostics.test.js b/tests/step9-status-diagnostics.test.js
index 2f9c9a1..d342666 100644
--- a/tests/step9-status-diagnostics.test.js
+++ b/tests/step9-status-diagnostics.test.js
@@ -66,6 +66,8 @@ const bundle = [
extractFunction('isStep9SuccessStatus'),
extractFunction('isStep9SuccessLikeStatus'),
extractFunction('formatStep10StatusSummaryValue'),
+ extractFunction('isStep10BrowserSwitchRequiredConflict'),
+ extractFunction('getStep10BrowserSwitchRequiredMessage'),
extractFunction('buildStep9StatusDiagnostics'),
extractFunction('extractStep10FailureDetail'),
extractFunction('explainStep10Failure'),
@@ -84,6 +86,8 @@ ${bundle}
return {
buildStep9StatusDiagnostics,
explainStep10Failure,
+ isStep10BrowserSwitchRequiredConflict,
+ getStep10BrowserSwitchRequiredMessage,
};
`)();
}
@@ -200,3 +204,32 @@ test('step 10 explains callback upgrade hint with user-friendly reason', () => {
assert.match(explanation.userMessage, /CLI Proxy API 版本过旧|管理接口未启动|连接异常/);
assert.match(explanation.userMessage, /回调提交阶段/);
});
+
+test('step 10 requests browser switch when success badge and callback upgrade failure coexist', () => {
+ const api = createApi();
+ const diagnostics = api.buildStep9StatusDiagnostics(
+ [
+ {
+ visible: true,
+ text: '认证成功',
+ className: 'status-badge success',
+ location: 'main',
+ hasErrorVisualSignal: false,
+ errorVisualSummary: '',
+ },
+ {
+ visible: true,
+ text: '回调 URL 提交失败: 请更新CLI Proxy API或检查连接',
+ className: 'status-badge error',
+ location: 'callback',
+ hasErrorVisualSignal: true,
+ errorVisualSummary: 'color=rgb(220, 38, 38)',
+ },
+ ],
+ [],
+ 'page'
+ );
+
+ assert.equal(api.isStep10BrowserSwitchRequiredConflict(diagnostics), true);
+ assert.match(api.getStep10BrowserSwitchRequiredMessage(diagnostics), /更换浏览器后重新进行注册登录/);
+});
From 69434d67eed960e26edc9382b4760fa10f3edeff Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Wed, 22 Apr 2026 20:09:47 +0800
Subject: [PATCH 06/10] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=9B=9E?=
=?UTF-8?q?=E8=B0=83=E6=96=B9=E5=BC=8F=E9=80=89=E9=A1=B9=EF=BC=8C=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E6=AD=A5=E9=AA=A4=2010=20=E7=AD=96=E7=95=A5=E9=80=89?=
=?UTF-8?q?=E6=8B=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 6 +++---
sidepanel/sidepanel.html | 8 ++++----
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 43468ac..3589b79 100644
--- a/README.md
+++ b/README.md
@@ -639,9 +639,9 @@ Step 8 默认要求当前认证页已经处于登录验证码页。
- 步骤 10 会拒绝任何不是真实 `/auth/callback`,或缺少 `code` / `state` 的本地回调地址
- 成功后的清理只会针对 `/auth` 这一类真实回调标签页,不会再泛化清理任意 localhost 路径
-- 侧边栏可切换“本地 CPA”策略,默认是 `全部回调`
-- 选择 `全部回调` 时,即使 CPA 部署在本地,也会执行步骤 10
-- 选择 `跳过第10步` 时,仅当本地 CPA 且步骤 9 已拿到回调地址时,才会直接跳过步骤 10
+- 侧边栏可切换“回调方式”,默认是 `服务器部署`
+- 选择 `服务器部署` 时,即使 CPA 部署在本地,也会执行步骤 10
+- 选择 `本地部署` 时,仅当本地 CPA 且步骤 9 已拿到回调地址时,才会直接跳过步骤 10
回到 CPA 面板:
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 6b99430..835ae4b 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -164,10 +164,10 @@
-
本地 CPA
-
-
-
+
回调方式
+
+
+
From 48af6e7d1b517d91672ef3a132a24f8b5fe3c740 Mon Sep 17 00:00:00 2001
From: Q3CC
Date: Wed, 22 Apr 2026 20:50:17 +0800
Subject: [PATCH 07/10] fix: restore readable signup diagnostics regex and sync
docs index
---
tests/signup-entry-diagnostics.test.js | 2 +-
项目文件结构说明.md | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/tests/signup-entry-diagnostics.test.js b/tests/signup-entry-diagnostics.test.js
index 9fdf0b5..79323c1 100644
--- a/tests/signup-entry-diagnostics.test.js
+++ b/tests/signup-entry-diagnostics.test.js
@@ -147,7 +147,7 @@ return {
test('signup entry diagnostics captures hidden signup button style and blocking ancestor details', () => {
const api = new Function(`
-const SIGNUP_ENTRY_TRIGGER_PATTERN = /鍏嶈垂娉ㄥ唽|绔嬪嵆娉ㄥ唽|娉ㄥ唽|sign\\s*up|register|create\\s*account|create\\s+account/i;
+const SIGNUP_ENTRY_TRIGGER_PATTERN = /免费注册|立即注册|注册|sign\\s*up|register|create\\s*account|create\\s+account/i;
const location = { href: 'https://chatgpt.com/' };
const hiddenSection = {
tagName: 'DIV',
diff --git a/项目文件结构说明.md b/项目文件结构说明.md
index 7974da3..97db58c 100644
--- a/项目文件结构说明.md
+++ b/项目文件结构说明.md
@@ -92,6 +92,7 @@
## `docs/`
- `docs/仓库协作者AI分析PR与合并标准流程.md`:仓库协作者进行 AI 分析 PR 与合并时的流程说明。
+- `docs/使用教程.md`:面向使用者的补充教程文档,当前说明扩展更新流程与 Clash Verge 的“非港轮询”配置步骤。
- `docs/images/交流群.jpg`:README 中展示的交流群图片资源。
- `docs/images/十轮自动.png`:README 中展示的自动运行效果图。
- `docs/images/微信.png`:README 中展示的微信收款码图片。
From d50d8b5305780f571530e4ed6e67baadd39a838e Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Wed, 22 Apr 2026 20:56:49 +0800
Subject: [PATCH 08/10] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E6=AD=A5?=
=?UTF-8?q?=E9=AA=A4=209=20=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=EF=BC=8C?=
=?UTF-8?q?=E7=9B=B4=E6=8E=A5=E6=8A=A5=E9=94=99=E4=B8=8D=E5=86=8D=E9=87=8D?=
=?UTF-8?q?=E8=AF=95=E8=AE=A4=E8=AF=81=E9=A1=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 2 +-
background.js | 29 +-----------
background/steps/confirm-oauth.js | 6 ---
tests/step8-retry-page-recovery.test.js | 63 +++++++++++++++++--------
项目完整链路说明.md | 2 +-
5 files changed, 48 insertions(+), 54 deletions(-)
diff --git a/README.md b/README.md
index 3589b79..ffef8ff 100644
--- a/README.md
+++ b/README.md
@@ -623,7 +623,7 @@ Step 8 默认要求当前认证页已经处于登录验证码页。
- 等待按钮可点击
- 获取按钮坐标
- 通过 Chrome `debugger` 的输入事件点击该按钮
-- 点击后会持续检查页面是否真正离开当前状态;如果出现认证页 `重试` 页面,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再重新执行当前轮的“继续”点击
+- 点击后会持续检查页面是否真正离开当前状态;如果点击后出现认证页 `重试` 页面,则直接报错,不会在 Step 9 内部点击 `重试`
- 同时监听 `chrome.webNavigation.onBeforeNavigate`
- 一旦捕获本地回调地址,就把结果保存到 `Callback`
diff --git a/background.js b/background.js
index 93511a7..0f5aea1 100644
--- a/background.js
+++ b/background.js
@@ -7194,7 +7194,6 @@ async function getStep8PageState(tabId, responseTimeoutMs = 1500) {
async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) {
const start = Date.now();
let recovered = false;
- let retryRecovered = false;
while (Date.now() - start < timeoutMs) {
throwIfStopped();
@@ -7206,20 +7205,9 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS)
throw new Error('步骤 9:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。');
}
if (pageState?.retryPage) {
- await recoverAuthRetryPageOnTab(tabId, {
- flow: 'auth',
- logLabel: '步骤 9:检测到认证页重试页,正在点击“重试”恢复',
- step: 8,
- timeoutMs: Math.max(1000, Math.min(12000, timeoutMs)),
- });
- retryRecovered = true;
- await sleepWithStop(250);
- continue;
+ throw new Error(`步骤 9:当前认证页已进入重试页,当前流程将直接报错。URL: ${pageState.url || 'unknown'}`);
}
if (pageState?.consentReady) {
- if (retryRecovered) {
- await addLog('步骤 9:认证页重试页已恢复,准备重新定位“继续”按钮...', 'info');
- }
return pageState;
}
if (pageState === null && !recovered) {
@@ -7384,18 +7372,7 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI
throw new Error('步骤 9:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。');
}
if (pageState?.retryPage) {
- await recoverAuthRetryPageOnTab(tabId, {
- flow: 'auth',
- logLabel: '步骤 9:点击“继续”后进入重试页,正在点击“重试”恢复',
- step: 8,
- timeoutMs: Math.max(1000, Math.min(12000, timeoutMs)),
- });
- return {
- progressed: false,
- reason: 'retry_page_recovered',
- restartCurrentStep: true,
- url: pageState.url || baselineUrl || '',
- };
+ throw new Error(`步骤 9:点击“继续”后页面进入认证页重试页,当前流程将直接报错。URL: ${pageState.url || baselineUrl || 'unknown'}`);
}
if (pageState === null) {
if (!recovered) {
@@ -7429,8 +7406,6 @@ function getStep8EffectLabel(effect) {
switch (effect?.reason) {
case 'url_changed':
return `URL 已变化:${effect.url}`;
- case 'retry_page_recovered':
- return '页面进入重试页并已恢复,需要重新执行当前步骤';
case 'page_reloading':
return '页面正在跳转或重载';
case 'left_consent_page':
diff --git a/background/steps/confirm-oauth.js b/background/steps/confirm-oauth.js
index 1c9076c..d61f8b2 100644
--- a/background/steps/confirm-oauth.js
+++ b/background/steps/confirm-oauth.js
@@ -200,12 +200,6 @@
break;
}
- if (effect.restartCurrentStep) {
- await addLog(`步骤 9:${getStep8EffectLabel(effect)},准备重新定位“继续”按钮并重试...`, 'warn');
- await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
- continue;
- }
-
if (round >= STEP8_MAX_ROUNDS) {
throw new Error(`步骤 9:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`);
}
diff --git a/tests/step8-retry-page-recovery.test.js b/tests/step8-retry-page-recovery.test.js
index 34d76aa..bdc8ac5 100644
--- a/tests/step8-retry-page-recovery.test.js
+++ b/tests/step8-retry-page-recovery.test.js
@@ -51,10 +51,8 @@ function extractFunction(name) {
return source.slice(start, end);
}
-test('step 8 click effect returns restart_current_step when retry page is recovered', async () => {
+test('step 8 click effect throws when retry page appears after clicking continue', async () => {
const api = new Function(`
-let recoverCalls = 0;
-
const chrome = {
tabs: {
async get() {
@@ -69,10 +67,6 @@ const chrome = {
function throwIfStopped() {}
async function sleepWithStop() {}
async function ensureStep8SignupPageReady() {}
-async function recoverAuthRetryPageOnTab() {
- recoverCalls += 1;
- return { recovered: true };
-}
async function getStep8PageState() {
return {
url: 'https://auth.openai.com/authorize',
@@ -89,20 +83,51 @@ return {
async run() {
return waitForStep8ClickEffect(88, 'https://auth.openai.com/authorize', 1000);
},
- snapshot() {
- return { recoverCalls };
+};
+`)();
+
+ await assert.rejects(
+ () => api.run(),
+ /点击“继续”后页面进入认证页重试页/
+ );
+});
+
+test('step 8 ready check throws when consent page is already a retry page before clicking', async () => {
+ const api = new Function(`
+const chrome = {
+ tabs: {
+ async get() {
+ return {
+ id: 88,
+ url: 'https://auth.openai.com/authorize',
+ };
+ },
+ },
+};
+
+function throwIfStopped() {}
+async function sleepWithStop() {}
+async function ensureStep8SignupPageReady() {}
+async function getStep8PageState() {
+ return {
+ url: 'https://auth.openai.com/authorize',
+ retryPage: true,
+ addPhonePage: false,
+ consentReady: false,
+ };
+}
+
+${extractFunction('waitForStep8Ready')}
+
+return {
+ async run() {
+ return waitForStep8Ready(88, 1000);
},
};
`)();
- const result = await api.run();
- const snapshot = api.snapshot();
-
- assert.deepStrictEqual(result, {
- progressed: false,
- reason: 'retry_page_recovered',
- restartCurrentStep: true,
- url: 'https://auth.openai.com/authorize',
- });
- assert.equal(snapshot.recoverCalls, 1);
+ await assert.rejects(
+ () => api.run(),
+ /当前认证页已进入重试页/
+ );
});
diff --git a/项目完整链路说明.md b/项目完整链路说明.md
index 7f4f5c6..3fa355c 100644
--- a/项目完整链路说明.md
+++ b/项目完整链路说明.md
@@ -709,7 +709,7 @@
- Step 8 如果发现认证页已经进入登录超时报错/重试页,会直接报错并回到 Step 7 重新开始,而不是在 Step 8 内部点击 `重试`。
- Step 8 的登录重试页判定也覆盖 `/email-verification` 上的 `405 / Route Error`,避免这类页面被误当成普通未知页。
- 任意认证页重试页如果正文中出现 `max_check_attempts`,会被视为 Cloudflare 风控触发:后台立刻完全停止流程,侧边栏会复用现有确认弹窗提示等待 15~30 分钟后再试,避免继续刷新或反复重试加重风控,确认按钮显示为“我知道了”。
-- Step 9 在点击 OAuth 同意页 `继续` 后,会额外检查是否进入认证页重试页;若命中则先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再重新执行当前轮的 `继续` 点击。
+- Step 9 在点击 OAuth 同意页 `继续` 后,会持续等待页面跳转;若点击后命中认证页重试页,则直接报错,不会在 Step 9 内部点击 `重试`。
## 2026-04-21 2925 邮件时间窗补充
- `2925` 在 Step 4 / Step 8 现在会携带固定的步骤开始时间窗口,实际筛选下限为“步骤开始时间向前回看 10 分钟”。
From e2ec2c7381af6fa9eb5c047af0080f630df81cd0 Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Wed, 22 Apr 2026 21:14:12 +0800
Subject: [PATCH 09/10] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20currentMail2?=
=?UTF-8?q?925AccountId=20=E4=BB=A5=E6=94=AF=E6=8C=81=E8=B4=A6=E5=8F=B7?=
=?UTF-8?q?=E6=B1=A0=E6=81=A2=E5=A4=8D=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
background.js | 2 +
background/mail-2925-session.js | 5 ++
sidepanel/sidepanel.js | 1 +
...background-mail2925-session-module.test.js | 43 ++++++++++++
tests/sidepanel-mail2925-base-email.test.js | 70 +++++++++++++++++++
项目完整链路说明.md | 3 +-
6 files changed, 123 insertions(+), 1 deletion(-)
diff --git a/background.js b/background.js
index 0f5aea1..8c8eb1c 100644
--- a/background.js
+++ b/background.js
@@ -269,6 +269,7 @@ const PERSISTED_SETTING_DEFAULTS = {
accountRunHistoryHelperBaseUrl: DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL,
gmailBaseEmail: '',
mail2925BaseEmail: '',
+ currentMail2925AccountId: '',
emailPrefix: '',
inbucketHost: '',
inbucketMailbox: '',
@@ -903,6 +904,7 @@ function normalizePersistentSettingValue(key, value) {
return normalizeAccountRunHistoryHelperBaseUrl(value);
case 'gmailBaseEmail':
case 'mail2925BaseEmail':
+ case 'currentMail2925AccountId':
case 'emailPrefix':
return String(value || '').trim();
case 'inbucketHost':
diff --git a/background/mail-2925-session.js b/background/mail-2925-session.js
index c78a350..07ab952 100644
--- a/background/mail-2925-session.js
+++ b/background/mail-2925-session.js
@@ -186,6 +186,7 @@
await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item)));
}
+ await setPersistentSettings({ currentMail2925AccountId: nextAccount.id });
await setState({ currentMail2925AccountId: nextAccount.id });
broadcastDataUpdate({ currentMail2925AccountId: nextAccount.id });
if (logMessage) {
@@ -210,6 +211,7 @@
await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item)));
if (state.currentMail2925AccountId === account.id && nextAccount.enabled === false) {
+ await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
}
@@ -224,6 +226,7 @@
await syncMail2925Accounts(nextAccounts);
if (state.currentMail2925AccountId === accountId) {
+ await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
}
@@ -239,6 +242,7 @@
await syncMail2925Accounts(nextAccounts);
if (state.currentMail2925AccountId && !findMail2925Account(nextAccounts, state.currentMail2925AccountId)) {
+ await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
}
@@ -613,6 +617,7 @@
});
if (!nextAccount) {
+ await setPersistentSettings({ currentMail2925AccountId: '' });
await setState({ currentMail2925AccountId: null });
broadcastDataUpdate({ currentMail2925AccountId: null });
if (typeof requestStop === 'function') {
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index 37a6d3e..430444d 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -1485,6 +1485,7 @@ function collectSettingsPayload() {
mailProvider: selectMailProvider.value,
mail2925Mode: getSelectedMail2925Mode(),
mail2925UseAccountPool,
+ currentMail2925AccountId: String(latestState?.currentMail2925AccountId || '').trim(),
emailGenerator: selectEmailGenerator.value,
autoDeleteUsedIcloudAlias: checkboxAutoDeleteIcloud?.checked,
icloudHostPreference: selectIcloudHostPreference?.value || 'auto',
diff --git a/tests/background-mail2925-session-module.test.js b/tests/background-mail2925-session-module.test.js
index 19188a7..331bf45 100644
--- a/tests/background-mail2925-session-module.test.js
+++ b/tests/background-mail2925-session-module.test.js
@@ -294,3 +294,46 @@ test('handleMail2925LimitReachedError stops immediately when account pool is off
assert.equal(events.stopCalls.length, 1);
assert.equal(currentState.currentMail2925AccountId, 'current');
});
+
+test('setCurrentMail2925Account persists currentMail2925AccountId for browser restart restore', async () => {
+ const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
+ const globalScope = {};
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
+
+ let currentState = {
+ mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
+ { id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
+ ]),
+ currentMail2925AccountId: null,
+ };
+ const persistedUpdates = [];
+
+ const manager = api.createMail2925SessionManager({
+ addLog: async () => {},
+ broadcastDataUpdate: () => {},
+ chrome: {},
+ findMail2925Account: mail2925Utils.findMail2925Account,
+ getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
+ getState: async () => currentState,
+ isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
+ MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
+ normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
+ normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
+ pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
+ setPersistentSettings: async (payload) => {
+ persistedUpdates.push(payload);
+ },
+ setState: async (updates) => {
+ currentState = { ...currentState, ...updates };
+ },
+ throwIfStopped: () => {},
+ upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
+ });
+
+ await manager.setCurrentMail2925Account('acc-1');
+
+ assert.equal(currentState.currentMail2925AccountId, 'acc-1');
+ assert.deepStrictEqual(persistedUpdates, [
+ { currentMail2925AccountId: 'acc-1' },
+ ]);
+});
diff --git a/tests/sidepanel-mail2925-base-email.test.js b/tests/sidepanel-mail2925-base-email.test.js
index 340534d..5e24210 100644
--- a/tests/sidepanel-mail2925-base-email.test.js
+++ b/tests/sidepanel-mail2925-base-email.test.js
@@ -142,3 +142,73 @@ return {
assert.equal(api.getLatestState().mail2925BaseEmail, 'new@2925.com');
assert.equal(api.getSaveCalls(), 1);
});
+
+test('collectSettingsPayload persists currentMail2925AccountId for 2925 account pool restore', () => {
+ const bundle = [
+ extractFunction('collectSettingsPayload'),
+ ].join('\n');
+
+ const api = new Function(`
+let latestState = {
+ contributionMode: false,
+ mail2925UseAccountPool: true,
+ currentMail2925AccountId: 'acc-2',
+};
+let cloudflareDomainEditMode = false;
+let cloudflareTempEmailDomainEditMode = false;
+const selectCfDomain = { value: 'example.com' };
+const selectTempEmailDomain = { value: 'mail.example.com' };
+const selectPanelMode = { value: 'cpa' };
+const inputVpsUrl = { value: '' };
+const inputVpsPassword = { value: '' };
+const inputSub2ApiUrl = { value: '' };
+const inputSub2ApiEmail = { value: '' };
+const inputSub2ApiPassword = { value: '' };
+const inputSub2ApiGroup = { value: '' };
+const inputSub2ApiDefaultProxy = { value: '' };
+const inputPassword = { value: '' };
+const selectMailProvider = { value: '2925' };
+const selectEmailGenerator = { value: 'duck' };
+const checkboxAutoDeleteIcloud = { checked: false };
+const selectIcloudHostPreference = { value: 'auto' };
+const inputAccountRunHistoryTextEnabled = { checked: false };
+const inputAccountRunHistoryHelperBaseUrl = { value: '' };
+const inputMail2925UseAccountPool = { checked: true };
+const inputInbucketHost = { value: '' };
+const inputInbucketMailbox = { value: '' };
+const inputLuckmailApiKey = { value: '' };
+const inputLuckmailBaseUrl = { value: '' };
+const selectLuckmailEmailType = { value: 'ms_graph' };
+const inputLuckmailDomain = { value: '' };
+const inputTempEmailBaseUrl = { value: '' };
+const inputTempEmailAdminAuth = { value: '' };
+const inputTempEmailCustomAuth = { value: '' };
+const inputTempEmailReceiveMailbox = { value: '' };
+function getCloudflareDomainsFromState() {
+ return { domains: [], activeDomain: '' };
+}
+function normalizeCloudflareDomainValue(value) { return String(value || '').trim(); }
+function getCloudflareTempEmailDomainsFromState() {
+ return { domains: [], activeDomain: '' };
+}
+function normalizeCloudflareTempEmailDomainValue(value) { return String(value || '').trim(); }
+function getSelectedLocalCpaStep9Mode() { return 'submit'; }
+function getSelectedMail2925Mode() { return 'provide'; }
+function getSelectedHotmailServiceMode() { return 'local'; }
+function buildManagedAliasBaseEmailPayload() {
+ return { gmailBaseEmail: '', mail2925BaseEmail: 'demo@2925.com', emailPrefix: '' };
+}
+function normalizeLuckmailBaseUrl(value) { return String(value || '').trim(); }
+function normalizeLuckmailEmailType(value) { return String(value || '').trim() || 'ms_graph'; }
+function normalizeCloudflareTempEmailBaseUrlValue(value) { return String(value || '').trim(); }
+function normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); }
+function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); }
+${bundle}
+return { collectSettingsPayload };
+`)();
+
+ const payload = api.collectSettingsPayload();
+
+ assert.equal(payload.currentMail2925AccountId, 'acc-2');
+ assert.equal(payload.mail2925UseAccountPool, true);
+});
diff --git a/项目完整链路说明.md b/项目完整链路说明.md
index 3fa355c..6183c2d 100644
--- a/项目完整链路说明.md
+++ b/项目完整链路说明.md
@@ -141,7 +141,7 @@
- 标签注册表
- 最近打开的来源地址
- LuckMail 当前运行时选择
-- 2925 当前运行时账号选择 `currentMail2925AccountId`
+- 2925 当前选中的账号 ID `currentMail2925AccountId`(运行时会同步到持久配置,用于重开浏览器后恢复同一个号池账号)
补充:
@@ -156,6 +156,7 @@
- Hotmail 账号池
- 2925 账号池
- 2925 是否启用号池模式 `mail2925UseAccountPool`
+- 2925 当前选中的号池账号 ID `currentMail2925AccountId`
- Cloudflare / Temp Email 设置
- iCloud 相关偏好
- LuckMail API 配置
From 8eec4409fc77efde1c4857a6bc34be5875b5d5f5 Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Wed, 22 Apr 2026 21:46:27 +0800
Subject: [PATCH 10/10] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA2925=E9=82=AE?=
=?UTF-8?q?=E7=AE=B1=E7=99=BB=E5=BD=95=E6=80=81=E5=A4=84=E7=90=86=EF=BC=8C?=
=?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=82=AE=E7=AE=B1=E4=B8=80=E8=87=B4=E6=80=A7?=
=?UTF-8?q?=E6=A0=A1=E9=AA=8C=E4=B8=8E=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
background/mail-2925-session.js | 42 ++++-
background/steps/fetch-signup-code.js | 15 ++
content/mail-2925.js | 83 ++++++++-
...background-mail2925-entry-behavior.test.js | 160 ++++++++++++++++++
tests/mail-2925-content.test.js | 55 ++++++
tests/sidepanel-mail2925-base-email.test.js | 13 ++
项目完整链路说明.md | 4 +-
7 files changed, 362 insertions(+), 10 deletions(-)
diff --git a/background/mail-2925-session.js b/background/mail-2925-session.js
index 07ab952..410fa97 100644
--- a/background/mail-2925-session.js
+++ b/background/mail-2925-session.js
@@ -168,6 +168,10 @@
}
}
+ function normalizeMailboxEmail(value = '') {
+ return String(value || '').trim().toLowerCase();
+ }
+
async function setCurrentMail2925Account(accountId, options = {}) {
const { logMessage = '', updateLastUsedAt = false } = options;
const state = await getState();
@@ -401,10 +405,13 @@
forceRelogin = false,
actionLabel = '确保 2925 邮箱登录态',
allowLoginWhenOnLoginPage = true,
+ expectedMailboxEmail = '',
} = options;
+ const normalizedExpectedMailboxEmail = normalizeMailboxEmail(expectedMailboxEmail);
+
let account = null;
- if (forceRelogin) {
+ if (forceRelogin || (allowLoginWhenOnLoginPage && normalizedExpectedMailboxEmail)) {
account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: accountId,
@@ -481,16 +488,16 @@
}
}
- if (!forceRelogin && !isMail2925LoginUrl(openedUrl)) {
+ if (!forceRelogin && !isMail2925LoginUrl(openedUrl) && !normalizedExpectedMailboxEmail) {
await addLog('2925:当前邮箱页未跳转到登录页,将直接复用已登录会话。', 'info');
return buildSuccessPayload();
}
- if (!forceRelogin && !allowLoginWhenOnLoginPage) {
+ if (!forceRelogin && isMail2925LoginUrl(openedUrl) && !allowLoginWhenOnLoginPage) {
await failMailboxSession(`2925:${actionLabel}失败,当前页面已跳转到登录页,且当前未启用 2925 账号池,不执行自动登录。`);
}
- if (!account) {
+ if (!account && (forceRelogin || allowLoginWhenOnLoginPage)) {
account = await ensureMail2925AccountForFlow({
allowAllocate: true,
preferredAccountId: accountId,
@@ -523,9 +530,10 @@
step: 0,
source: 'background',
payload: {
- email: account.email,
- password: account.password,
+ email: account?.email || '',
+ password: account?.password || '',
forceLogin: forceRelogin,
+ allowLoginWhenOnLoginPage,
},
},
{
@@ -550,6 +558,28 @@
if (result?.limitReached) {
throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '子邮箱已达上限邮箱'}`);
}
+ const actualMailboxEmail = normalizeMailboxEmail(result?.mailboxEmail || '');
+ if (normalizedExpectedMailboxEmail && actualMailboxEmail && actualMailboxEmail !== normalizedExpectedMailboxEmail) {
+ if (allowLoginWhenOnLoginPage) {
+ await addLog(
+ `2925:当前邮箱页显示账号 ${actualMailboxEmail},与目标账号 ${normalizedExpectedMailboxEmail} 不一致,准备登出当前账号并登录目标账号。`,
+ 'warn'
+ );
+ return ensureMail2925MailboxSession({
+ accountId: account?.id || accountId || null,
+ forceRelogin: true,
+ allowLoginWhenOnLoginPage: true,
+ expectedMailboxEmail: normalizedExpectedMailboxEmail,
+ actionLabel,
+ });
+ }
+ await failMailboxSession(
+ `2925:${actionLabel}失败,当前邮箱页显示账号 ${actualMailboxEmail},与目标账号 ${normalizedExpectedMailboxEmail} 不一致,且当前未启用 2925 账号池。`
+ );
+ }
+ if (normalizedExpectedMailboxEmail && !actualMailboxEmail && result?.currentView === 'mailbox') {
+ await addLog('2925:未能识别当前邮箱页顶部邮箱地址,已跳过邮箱一致性校验。', 'warn');
+ }
if (!result?.loggedIn) {
await failMailboxSession(`2925:${actionLabel}失败,登录后仍未进入收件箱。`);
}
diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js
index c1d9124..066baee 100644
--- a/background/steps/fetch-signup-code.js
+++ b/background/steps/fetch-signup-code.js
@@ -24,6 +24,20 @@
throwIfStopped,
} = deps;
+ function getExpectedMail2925MailboxEmail(state = {}) {
+ if (Boolean(state?.mail2925UseAccountPool)) {
+ const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
+ const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
+ const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
+ const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
+ if (accountEmail) {
+ return accountEmail;
+ }
+ }
+
+ return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
+ }
+
async function focusOrOpenMailTab(mail) {
const alive = await isTabAlive(mail.source);
if (alive) {
@@ -111,6 +125,7 @@
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
+ expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
} else {
diff --git a/content/mail-2925.js b/content/mail-2925.js
index 0bbb977..09e9581 100644
--- a/content/mail-2925.js
+++ b/content/mail-2925.js
@@ -515,8 +515,9 @@ function detectMail2925ViewState() {
return { view: 'limit', limitMessage };
}
- if (findMailItems().length > 0) {
- return { view: 'mailbox', limitMessage: '' };
+ const mailboxEmail = getMail2925DisplayedMailboxEmail();
+ if (findMailItems().length > 0 || mailboxEmail) {
+ return { view: 'mailbox', limitMessage: '', mailboxEmail };
}
if (findMail2925LoginPasswordInput() && findMail2925LoginEmailInput()) {
@@ -531,6 +532,62 @@ function detectMail2925ViewState() {
return { view: 'unknown', limitMessage: '' };
}
+function getMail2925DisplayedMailboxEmail() {
+ const directSelectors = [
+ '[class*="user"] [class*="mail"]',
+ '[class*="user"] [class*="email"]',
+ '[class*="account"] [class*="mail"]',
+ '[class*="account"] [class*="email"]',
+ '[class*="header"] [class*="mail"]',
+ '[class*="header"] [class*="email"]',
+ ];
+
+ for (const selector of directSelectors) {
+ const candidates = document.querySelectorAll(selector);
+ for (const candidate of candidates) {
+ if (!isVisibleNode(candidate) || isMailItemNode(candidate)) {
+ continue;
+ }
+ const email = extractEmails(candidate.textContent || candidate.innerText || '')[0] || '';
+ if (email) {
+ return email;
+ }
+ }
+ }
+
+ const topCandidates = Array.from(document.querySelectorAll('body *'))
+ .filter((node) => {
+ if (!isVisibleNode(node) || isMailItemNode(node)) {
+ return false;
+ }
+ const rect = typeof node.getBoundingClientRect === 'function'
+ ? node.getBoundingClientRect()
+ : null;
+ if (!rect) return false;
+ return rect.top >= 0 && rect.top <= Math.max(window.innerHeight * 0.35, 280);
+ })
+ .map((node) => {
+ const email = extractEmails(node.textContent || node.innerText || '')[0] || '';
+ return { node, email };
+ })
+ .filter((entry) => entry.email);
+
+ if (!topCandidates.length) {
+ return '';
+ }
+
+ topCandidates.sort((left, right) => {
+ const leftRect = left.node.getBoundingClientRect();
+ const rightRect = right.node.getBoundingClientRect();
+ if (leftRect.top !== rightRect.top) {
+ return leftRect.top - rightRect.top;
+ }
+ return leftRect.left - rightRect.left;
+ });
+
+ return topCandidates[0]?.email || '';
+}
+
function isCheckboxChecked(node) {
const checkbox = node?.matches?.('input[type="checkbox"], [role="checkbox"]')
? node
@@ -873,6 +930,7 @@ async function ensureMail2925Session(payload = {}) {
const email = String(payload?.email || '').trim();
const password = String(payload?.password || '');
const forceLogin = Boolean(payload?.forceLogin);
+ const allowLoginWhenOnLoginPage = payload?.allowLoginWhenOnLoginPage !== false;
log(`步骤 0:2925 登录态检查开始,当前地址 ${location.href},forceLogin=${forceLogin ? 'true' : 'false'}`, 'info');
for (let attempt = 0; attempt < 10; attempt += 1) {
@@ -893,9 +951,19 @@ async function ensureMail2925Session(payload = {}) {
ok: true,
loggedIn: true,
currentView: 'mailbox',
+ mailboxEmail: currentState.mailboxEmail || '',
};
}
if (currentState.view === 'login') {
+ if (!forceLogin && !allowLoginWhenOnLoginPage) {
+ return {
+ ok: false,
+ loggedIn: false,
+ currentView: 'login',
+ requiresLogin: true,
+ mailboxEmail: '',
+ };
+ }
break;
}
await sleep(500);
@@ -908,6 +976,7 @@ async function ensureMail2925Session(payload = {}) {
ok: true,
loggedIn: true,
currentView: 'mailbox',
+ mailboxEmail: loginState.mailboxEmail || '',
};
}
if (loginState.view === 'limit') {
@@ -919,6 +988,15 @@ async function ensureMail2925Session(payload = {}) {
limitMessage: loginState.limitMessage,
};
}
+ if (!forceLogin && !allowLoginWhenOnLoginPage && loginState.view === 'login') {
+ return {
+ ok: false,
+ loggedIn: false,
+ currentView: 'login',
+ requiresLogin: true,
+ mailboxEmail: '',
+ };
+ }
const emailInput = findMail2925LoginEmailInput();
const passwordInput = findMail2925LoginPasswordInput();
@@ -951,6 +1029,7 @@ async function ensureMail2925Session(payload = {}) {
loggedIn: true,
currentView: 'mailbox',
usedCredentials: true,
+ mailboxEmail: finalState.mailboxEmail || getMail2925DisplayedMailboxEmail() || '',
};
}
diff --git a/tests/background-mail2925-entry-behavior.test.js b/tests/background-mail2925-entry-behavior.test.js
index 58fb798..cd9cc07 100644
--- a/tests/background-mail2925-entry-behavior.test.js
+++ b/tests/background-mail2925-entry-behavior.test.js
@@ -266,3 +266,163 @@ test('ensureMail2925MailboxSession logs in when login page is detected and accou
assert.equal(readyCalls, 1);
assert.equal(result.result.loggedIn, true);
});
+
+test('ensureMail2925MailboxSession relogs with selected account when mailbox page email mismatches and pool is on', async () => {
+ let currentState = {
+ autoRunning: false,
+ mail2925UseAccountPool: true,
+ mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
+ { id: 'acc-1', email: 'target@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
+ ]),
+ currentMail2925AccountId: 'acc-1',
+ };
+ const openedUrls = [];
+ const sendPayloads = [];
+
+ const manager = api.createMail2925SessionManager({
+ addLog: async () => {},
+ broadcastDataUpdate: () => {},
+ chrome: {
+ tabs: {
+ get: async () => ({ id: 9, url: openedUrls.at(-1) || 'https://2925.com/#/mailList' }),
+ },
+ cookies: {
+ getAll: async () => [],
+ remove: async () => ({ ok: true }),
+ },
+ browsingData: {
+ removeCookies: async () => {},
+ },
+ },
+ ensureContentScriptReadyOnTab: async () => {},
+ findMail2925Account: mail2925Utils.findMail2925Account,
+ getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
+ getState: async () => currentState,
+ isAutoRunLockedState: () => false,
+ isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
+ MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
+ normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
+ normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
+ pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
+ reuseOrCreateTab: async (_source, url) => {
+ openedUrls.push(url);
+ return 9;
+ },
+ sendToMailContentScriptResilient: async (_mail, message) => {
+ sendPayloads.push(message.payload);
+ if (sendPayloads.length === 1) {
+ return {
+ loggedIn: true,
+ currentView: 'mailbox',
+ mailboxEmail: 'wrong@2925.com',
+ };
+ }
+ return {
+ loggedIn: true,
+ currentView: 'mailbox',
+ mailboxEmail: 'target@2925.com',
+ };
+ },
+ setPersistentSettings: async (payload) => {
+ currentState = { ...currentState, ...payload };
+ },
+ setState: async (updates) => {
+ currentState = { ...currentState, ...updates };
+ },
+ sleepWithStop: async () => {},
+ throwIfStopped: () => {},
+ upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
+ waitForTabUrlMatch: async () => ({ url: 'https://2925.com/login/' }),
+ });
+
+ const result = await manager.ensureMail2925MailboxSession({
+ accountId: 'acc-1',
+ forceRelogin: false,
+ allowLoginWhenOnLoginPage: true,
+ expectedMailboxEmail: 'target@2925.com',
+ actionLabel: '步骤 4:确认 2925 邮箱登录态',
+ });
+
+ assert.equal(result.result.loggedIn, true);
+ assert.deepStrictEqual(openedUrls, [
+ 'https://2925.com/#/mailList',
+ 'https://2925.com/login/',
+ ]);
+ assert.equal(sendPayloads.length, 2);
+ assert.equal(sendPayloads[0].allowLoginWhenOnLoginPage, true);
+ assert.equal(sendPayloads[1].forceLogin, true);
+});
+
+test('ensureMail2925MailboxSession stops when mailbox page email mismatches and pool is off', async () => {
+ let currentState = {
+ autoRunning: true,
+ autoRunPhase: 'running',
+ mail2925UseAccountPool: false,
+ mail2925Accounts: [],
+ currentMail2925AccountId: null,
+ };
+ const stopCalls = [];
+ let sendCalls = 0;
+
+ const manager = api.createMail2925SessionManager({
+ addLog: async () => {},
+ broadcastDataUpdate: () => {},
+ chrome: {
+ tabs: {
+ get: async () => ({ id: 9, url: 'https://2925.com/#/mailList' }),
+ },
+ cookies: {
+ getAll: async () => [],
+ remove: async () => ({ ok: true }),
+ },
+ browsingData: {
+ removeCookies: async () => {},
+ },
+ },
+ ensureContentScriptReadyOnTab: async () => {},
+ findMail2925Account: mail2925Utils.findMail2925Account,
+ getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
+ getState: async () => currentState,
+ isAutoRunLockedState: (state) => Boolean(state?.autoRunning) && state?.autoRunPhase === 'running',
+ isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
+ MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
+ normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
+ normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
+ pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
+ requestStop: async (options = {}) => {
+ stopCalls.push(options);
+ },
+ reuseOrCreateTab: async () => 9,
+ sendToMailContentScriptResilient: async () => {
+ sendCalls += 1;
+ return {
+ loggedIn: true,
+ currentView: 'mailbox',
+ mailboxEmail: 'wrong@2925.com',
+ };
+ },
+ setPersistentSettings: async (payload) => {
+ currentState = { ...currentState, ...payload };
+ },
+ setState: async (updates) => {
+ currentState = { ...currentState, ...updates };
+ },
+ throwIfStopped: () => {},
+ upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
+ });
+
+ await assert.rejects(
+ () => manager.ensureMail2925MailboxSession({
+ accountId: null,
+ forceRelogin: false,
+ allowLoginWhenOnLoginPage: false,
+ expectedMailboxEmail: 'target@2925.com',
+ actionLabel: '步骤 4:确认 2925 邮箱登录态',
+ }),
+ /流程已被用户停止。/
+ );
+
+ assert.equal(sendCalls, 1);
+ assert.equal(stopCalls.length, 1);
+ assert.match(stopCalls[0].logMessage, /与目标账号 target@2925\.com 不一致/);
+});
diff --git a/tests/mail-2925-content.test.js b/tests/mail-2925-content.test.js
index ddbe65d..e36e182 100644
--- a/tests/mail-2925-content.test.js
+++ b/tests/mail-2925-content.test.js
@@ -12,6 +12,61 @@ test('ensureMail2925Session waits 1 second after filling credentials before clic
assert.match(source, /fillInput\(passwordInput,\s*password\);[\s\S]*?await sleep\(200\);[\s\S]*?await sleep\(1000\);[\s\S]*?simulateClick\(loginButton\);/);
});
+test('detectMail2925ViewState treats top mailbox email as mailbox view', () => {
+ const bundle = [
+ extractFunction('normalizeNodeText'),
+ extractFunction('isVisibleNode'),
+ extractFunction('isMailItemNode'),
+ extractFunction('resolveActionTarget'),
+ extractFunction('findMailItems'),
+ extractFunction('extractEmails'),
+ extractFunction('getMail2925DisplayedMailboxEmail'),
+ extractFunction('detectMail2925ViewState'),
+ ].join('\n');
+
+ const api = new Function(`
+const MAIL_ITEM_SELECTORS = ['.mail-item'];
+const MAIL_ITEM_SELECTOR_GROUP = '.mail-item';
+const MAIL2925_REMEMBER_LOGIN_PATTERNS = [];
+const MAIL2925_AGREEMENT_PATTERNS = [];
+const document = {
+ querySelectorAll(selector) {
+ if (selector === '.mail-item') return [];
+ if (selector === 'body *') return [headerEmail];
+ if (selector.includes('[class*="user"]')) return [headerEmail];
+ return [];
+ },
+ body: {
+ innerText: 'QLHazycoder qlhazycoder@2925.com',
+ textContent: 'QLHazycoder qlhazycoder@2925.com',
+ },
+};
+const window = {
+ innerHeight: 900,
+ getComputedStyle() {
+ return { display: 'block', visibility: 'visible' };
+ },
+};
+const headerEmail = {
+ hidden: false,
+ textContent: 'qlhazycoder@2925.com',
+ innerText: 'qlhazycoder@2925.com',
+ getBoundingClientRect() { return { top: 40, left: 400, width: 120, height: 20 }; },
+ closest() { return null; },
+};
+function detectMail2925LimitMessage() { return ''; }
+function findMail2925LoginPasswordInput() { return null; }
+function findMail2925LoginEmailInput() { return null; }
+function getPageTextSample() { return 'qlhazycoder@2925.com'; }
+${bundle}
+return { detectMail2925ViewState };
+`)();
+
+ const state = api.detectMail2925ViewState();
+ assert.equal(state.view, 'mailbox');
+ assert.equal(state.mailboxEmail, 'qlhazycoder@2925.com');
+});
+
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
diff --git a/tests/sidepanel-mail2925-base-email.test.js b/tests/sidepanel-mail2925-base-email.test.js
index 5e24210..8b17251 100644
--- a/tests/sidepanel-mail2925-base-email.test.js
+++ b/tests/sidepanel-mail2925-base-email.test.js
@@ -176,6 +176,8 @@ const inputAccountRunHistoryHelperBaseUrl = { value: '' };
const inputMail2925UseAccountPool = { checked: true };
const inputInbucketHost = { value: '' };
const inputInbucketMailbox = { value: '' };
+const inputHotmailRemoteBaseUrl = { value: '' };
+const inputHotmailLocalBaseUrl = { value: '' };
const inputLuckmailApiKey = { value: '' };
const inputLuckmailBaseUrl = { value: '' };
const selectLuckmailEmailType = { value: 'ms_graph' };
@@ -184,6 +186,13 @@ const inputTempEmailBaseUrl = { value: '' };
const inputTempEmailAdminAuth = { value: '' };
const inputTempEmailCustomAuth = { value: '' };
const inputTempEmailReceiveMailbox = { value: '' };
+const inputAutoSkipFailures = { checked: false };
+const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
+const inputAutoDelayEnabled = { checked: false };
+const inputAutoDelayMinutes = { value: '30' };
+const inputAutoStepDelaySeconds = { value: '' };
+const inputVerificationResendCount = { value: '4' };
+const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
function getCloudflareDomainsFromState() {
return { domains: [], activeDomain: '' };
}
@@ -203,6 +212,10 @@ function normalizeLuckmailEmailType(value) { return String(value || '').trim() |
function normalizeCloudflareTempEmailBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); }
function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); }
+function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
+function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
+function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
+function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
${bundle}
return { collectSettingsPayload };
`)();
diff --git a/项目完整链路说明.md b/项目完整链路说明.md
index 6183c2d..889d113 100644
--- a/项目完整链路说明.md
+++ b/项目完整链路说明.md
@@ -332,7 +332,7 @@
补充行为:
- `2925` provider 会关闭 Step 4 / 8 的自动重发间隔 25 秒节流;每次“重新发送验证码”之间,会在邮箱页内部执行一轮固定 15 次的刷新轮询,不再因 OAuth 剩余时间预算而缩短。
-- 当 provider 为 `2925` 时,Step 4 会优先直接打开当前 2925 邮箱页:如果页面仍停留在收件箱,就直接复用当前已登录页面;如果页面已经跳到登录页,则只有在启用了 2925 账号池时才会自动登录,未启用账号池时会直接复用现有停止逻辑结束流程。Step 8 不再额外承接这套登录态处理。
+- 当 provider 为 `2925` 时,Step 4 会优先直接打开当前 2925 邮箱页,并先比对页面顶部显示的邮箱地址是否与当前目标邮箱一致:如果一致,就直接复用当前已登录页面;如果不一致且启用了 2925 账号池,则会先清理 cookie 再登录当前选中的账号;如果不一致且未启用账号池,则直接复用现有停止逻辑结束流程。Step 8 不再额外承接这套登录态处理。
- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 当前既不依赖时间窗,也不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中的匹配邮件。
- 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。
- 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。
@@ -560,7 +560,7 @@
1. 用户在 sidepanel 的 2925 账号池中保存 `email / password`
2. sidepanel 中会单独展示 `提供邮箱 / 接收邮箱` 模式切换,以及独立的 `2925 号池` 开关 / 当前账号下拉框;这样即使切到 receive 模式,账号池设置也不会被别名基邮箱行一起隐藏
3. 只有当 sidepanel 中的 `mail2925UseAccountPool` 开关开启时,provide 模式下的别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail`
-4. 手动点击 `登录` 或自动流程进入 Step 4 前,后台会先打开当前 2925 邮箱页:如果仍停留在收件箱则直接复用;如果跳到登录页,则仅在号池模式开启时才自动登录,关闭号池时直接调用现有停止逻辑结束流程
+4. 手动点击 `登录` 或自动流程进入 Step 4 前,后台会先打开当前 2925 邮箱页,并读取页面顶部当前邮箱地址:如果仍停留在收件箱且顶部邮箱与目标邮箱一致,则直接复用;如果顶部邮箱不一致且启用了号池模式,则先清理 cookie 后登录当前选中的账号;如果顶部邮箱不一致且未启用号池模式,则直接调用现有停止逻辑结束流程;如果页面跳到登录页,则仍然只有号池模式开启时才自动登录
5. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先判断是否启用了号池模式:若已启用且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录;若未启用,则直接调用现有停止逻辑结束流程
6. 如果登录页已经识别到账号密码输入框,内容脚本会在填完账号密码后额外等待 1 秒再点击登录;若点击登录后 40 秒内仍未进入收件箱,且当前正处于自动运行中,则后台会直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态;这类情况常见于图片验证、行为验证或其他阻断登录的中间页
7. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态