From 144b144042d06eb7fb1e719f9249ce3189591a34 Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Thu, 7 May 2026 19:34:56 +0800
Subject: [PATCH 1/5] fix: mark deprecated email options for removal in next
version
---
sidepanel/sidepanel.html | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 635c9e1..fc04f6b 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -406,10 +406,10 @@
-
-
-
-
+
+
+
+
From 6f9a9913b57a068586a205f5d5ba8b2d1303af81 Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Fri, 8 May 2026 09:11:19 +0800
Subject: [PATCH 2/5] fix: update API URLs and improve error handling for
SUB2API configuration
---
background.js | 19 ++++++++++++++-----
background/mail-2925-session.js | 2 --
background/navigation-utils.js | 1 +
background/panel-bridge.js | 3 +++
background/steps/platform-verify.js | 3 +++
gopay-utils.js | 13 ++++++++++---
hotmail-utils.js | 8 +++++---
sidepanel/sidepanel.html | 2 +-
...background-navigation-utils-module.test.js | 14 ++++++++++++++
tests/gopay-utils.test.js | 1 +
tests/hotmail-utils.test.js | 10 +++++++++-
项目完整链路说明.md | 2 +-
12 files changed, 62 insertions(+), 16 deletions(-)
diff --git a/background.js b/background.js
index 4e67c5c..dda6fda 100644
--- a/background.js
+++ b/background.js
@@ -237,7 +237,7 @@ const STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS = 8;
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';
+const DEFAULT_SUB2API_URL = '';
const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
const DEFAULT_SUB2API_GROUP_NAME = 'codex';
@@ -2305,15 +2305,23 @@ function normalizePersistentSettingValue(key, value) {
);
case 'gopayHelperApiUrl':
{
- const legacyGpcHelperApiUrl = 'https://gpc.leftcode.xyz';
const defaultGpcHelperApiUrl = PERSISTED_SETTING_DEFAULTS.gopayHelperApiUrl
|| (typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined' ? DEFAULT_GPC_HELPER_API_URL : 'https://gpc.qlhazycoder.top');
const normalizedGpcHelperApiUrl = self.GoPayUtils?.normalizeGpcHelperBaseUrl
? self.GoPayUtils.normalizeGpcHelperBaseUrl(value || defaultGpcHelperApiUrl)
: String(value || defaultGpcHelperApiUrl).trim().replace(/\/+$/g, '');
- return normalizedGpcHelperApiUrl === legacyGpcHelperApiUrl
- ? defaultGpcHelperApiUrl
- : normalizedGpcHelperApiUrl;
+ if (!self.GoPayUtils?.normalizeGpcHelperBaseUrl) {
+ try {
+ const parsed = new URL(normalizedGpcHelperApiUrl);
+ const hostname = parsed.hostname.toLowerCase();
+ if (hostname !== 'gpc.qlhazycoder.top' && hostname !== 'localhost' && hostname !== '127.0.0.1') {
+ return defaultGpcHelperApiUrl;
+ }
+ } catch {
+ return defaultGpcHelperApiUrl;
+ }
+ }
+ return normalizedGpcHelperApiUrl;
}
case 'gopayHelperApiKey':
case 'gopayHelperCardKey':
@@ -6777,6 +6785,7 @@ function normalizeSub2ApiUrl(rawUrl) {
return navigationUtils.normalizeSub2ApiUrl(rawUrl);
}
const input = (rawUrl || '').trim() || DEFAULT_SUB2API_URL;
+ if (!input) return '';
const withProtocol = /^https?:\/\//i.test(input) ? input : `https://${input}`;
const parsed = new URL(withProtocol);
if (!parsed.pathname || parsed.pathname === '/') {
diff --git a/background/mail-2925-session.js b/background/mail-2925-session.js
index e80dc4c..e0e79cd 100644
--- a/background/mail-2925-session.js
+++ b/background/mail-2925-session.js
@@ -37,12 +37,10 @@
const MAIL2925_COOKIE_DOMAINS = [
'2925.com',
'www.2925.com',
- 'mail2.xiyouji.com',
];
const MAIL2925_COOKIE_ORIGINS = [
'https://2925.com',
'https://www.2925.com',
- 'https://mail2.xiyouji.com',
];
const MAIL2925_LIMIT_ERROR_PREFIX = 'MAIL2925_LIMIT_REACHED::';
const MAIL2925_THREAD_TERMINATED_ERROR_PREFIX = 'MAIL2925_THREAD_TERMINATED::';
diff --git a/background/navigation-utils.js b/background/navigation-utils.js
index f1ceef3..9077502 100644
--- a/background/navigation-utils.js
+++ b/background/navigation-utils.js
@@ -19,6 +19,7 @@
function normalizeSub2ApiUrl(rawUrl) {
const input = (rawUrl || '').trim() || DEFAULT_SUB2API_URL;
+ if (!input) return '';
const withProtocol = /^https?:\/\//i.test(input) ? input : `https://${input}`;
const parsed = new URL(withProtocol);
if (!parsed.pathname || parsed.pathname === '/') {
diff --git a/background/panel-bridge.js b/background/panel-bridge.js
index a4dff4c..8e204b5 100644
--- a/background/panel-bridge.js
+++ b/background/panel-bridge.js
@@ -251,6 +251,9 @@
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
const groupName = (state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME).trim() || DEFAULT_SUB2API_GROUP_NAME;
+ if (!sub2apiUrl) {
+ throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.');
+ }
if (!state.sub2apiEmail) {
throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。');
}
diff --git a/background/steps/platform-verify.js b/background/steps/platform-verify.js
index e9fb06c..2d39999 100644
--- a/background/steps/platform-verify.js
+++ b/background/steps/platform-verify.js
@@ -343,6 +343,9 @@
}
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
+ if (!sub2apiUrl) {
+ throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.');
+ }
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
await addStepLog(visibleStep, '正在打开 SUB2API 后台...');
diff --git a/gopay-utils.js b/gopay-utils.js
index 0fe3e9b..b2c2eee 100644
--- a/gopay-utils.js
+++ b/gopay-utils.js
@@ -5,7 +5,7 @@
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
- const LEGACY_GPC_HELPER_API_URL = 'https://gpc.leftcode.xyz';
+ const ALLOWED_GPC_HELPER_REMOTE_HOST = 'gpc.qlhazycoder.top';
function normalizePlusPaymentMethod(value = '') {
const normalized = String(value || '').trim().toLowerCase();
@@ -67,10 +67,17 @@
normalized = normalized.replace(/\/api\/gp\/balance(?:\?.*)?$/i, '');
normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, '');
normalized = normalized.replace(/\/api\/card\/redeem-api-key(?:\?.*)?$/i, '');
- if (normalized === LEGACY_GPC_HELPER_API_URL) {
+
+ try {
+ const parsed = new URL(normalized);
+ const hostname = parsed.hostname.toLowerCase();
+ if (hostname === ALLOWED_GPC_HELPER_REMOTE_HOST || hostname === 'localhost' || hostname === '127.0.0.1') {
+ return normalized || DEFAULT_GPC_HELPER_API_URL;
+ }
+ return DEFAULT_GPC_HELPER_API_URL;
+ } catch {
return DEFAULT_GPC_HELPER_API_URL;
}
- return normalized || DEFAULT_GPC_HELPER_API_URL;
}
function buildGpcHelperApiUrl(apiUrl = '', path = '') {
diff --git a/hotmail-utils.js b/hotmail-utils.js
index 14103c6..3ff4778 100644
--- a/hotmail-utils.js
+++ b/hotmail-utils.js
@@ -6,7 +6,6 @@
root.HotmailUtils = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createHotmailUtils() {
- const HOTMAIL_MAIL_API_URL = 'https://apple.882263.xyz/api/mail-new';
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
@@ -287,8 +286,11 @@
return list.map((message) => normalizeHotmailMailApiMessage(message));
}
- function buildHotmailMailApiLatestUrl(options) {
- const apiUrl = String(options?.apiUrl || '').trim() || HOTMAIL_MAIL_API_URL;
+ function buildHotmailMailApiLatestUrl(options = {}) {
+ const apiUrl = String(options?.apiUrl || '').trim();
+ if (!apiUrl) {
+ throw new Error('Hotmail mail API URL is required.');
+ }
const url = new URL(apiUrl);
url.searchParams.set('refresh_token', String(options?.refreshToken || ''));
url.searchParams.set('client_id', String(options?.clientId || ''));
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index fc04f6b..9d7b0b2 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -179,7 +179,7 @@
SUB2API
+ placeholder="https://your-sub2api-domain/admin/accounts" />
账号
diff --git a/tests/background-navigation-utils-module.test.js b/tests/background-navigation-utils-module.test.js
index 7ce8a09..eee918b 100644
--- a/tests/background-navigation-utils-module.test.js
+++ b/tests/background-navigation-utils-module.test.js
@@ -71,6 +71,20 @@ test('navigation utils support codex2api mode and url normalization', () => {
assert.equal(utils.getPanelModeLabel('codex2api'), 'Codex2API');
});
+test('navigation utils leaves SUB2API url empty when no default is configured', () => {
+ const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
+ const globalScope = {};
+
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundNavigationUtils;`)(globalScope);
+ const utils = api.createNavigationUtils({
+ DEFAULT_CODEX2API_URL: 'http://localhost:8080/admin/accounts',
+ DEFAULT_SUB2API_URL: '',
+ normalizeLocalCpaStep9Mode: (value) => value,
+ });
+
+ assert.equal(utils.normalizeSub2ApiUrl(''), '');
+});
+
test('navigation utils recognize the iCloud mail tab family on both supported hosts', () => {
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
const globalScope = {};
diff --git a/tests/gopay-utils.test.js b/tests/gopay-utils.test.js
index 002a58f..5198d01 100644
--- a/tests/gopay-utils.test.js
+++ b/tests/gopay-utils.test.js
@@ -28,6 +28,7 @@ test('GoPay utils builds GPC queue task and balance URLs from helper endpoints',
const api = loadGoPayUtils();
assert.equal(api.DEFAULT_GPC_HELPER_API_URL, 'https://gpc.qlhazycoder.top');
assert.equal(api.normalizeGpcHelperBaseUrl(''), 'https://gpc.qlhazycoder.top');
+ assert.equal(api.normalizeGpcHelperBaseUrl('https://example.com/api/gp/tasks'), 'https://gpc.qlhazycoder.top');
assert.equal(
api.buildGpcHelperApiUrl('', '/api/checkout/start'),
'https://gpc.qlhazycoder.top/api/checkout/start'
diff --git a/tests/hotmail-utils.test.js b/tests/hotmail-utils.test.js
index 5518ff2..6744dd6 100644
--- a/tests/hotmail-utils.test.js
+++ b/tests/hotmail-utils.test.js
@@ -358,13 +358,14 @@ test('pickVerificationMessageWithTimeFallback can ignore afterTimestamp while ke
test('buildHotmailMailApiLatestUrl includes email, client id, refresh token, and mailbox', () => {
const url = new URL(buildHotmailMailApiLatestUrl({
+ apiUrl: 'https://example.com/api/mail-new',
clientId: 'client-123',
email: 'user@hotmail.com',
refreshToken: 'refresh-token-xyz',
mailbox: 'Junk',
}));
- assert.equal(url.origin + url.pathname, 'https://apple.882263.xyz/api/mail-new');
+ assert.equal(url.origin + url.pathname, 'https://example.com/api/mail-new');
assert.equal(url.searchParams.get('client_id'), 'client-123');
assert.equal(url.searchParams.get('email'), 'user@hotmail.com');
assert.equal(url.searchParams.get('refresh_token'), 'refresh-token-xyz');
@@ -372,6 +373,13 @@ test('buildHotmailMailApiLatestUrl includes email, client id, refresh token, and
assert.equal(url.searchParams.get('response_type'), 'json');
});
+test('buildHotmailMailApiLatestUrl requires an explicit api url', () => {
+ assert.throws(
+ () => buildHotmailMailApiLatestUrl({ email: 'user@hotmail.com' }),
+ /Hotmail mail API URL is required/
+ );
+});
+
test('buildHotmailMailApiLatestUrl supports custom api url and can omit response_type', () => {
const url = new URL(buildHotmailMailApiLatestUrl({
apiUrl: 'https://example.com/custom-mail-api',
diff --git a/项目完整链路说明.md b/项目完整链路说明.md
index ef4b99c..c93c212 100644
--- a/项目完整链路说明.md
+++ b/项目完整链路说明.md
@@ -549,7 +549,7 @@ Plus 模式通过 `plusModeEnabled` 开启,目标是在普通注册资料完
Plus 模式可见步骤:
1. 第 1~5 步:沿用普通注册入口、邮箱、密码、注册验证码、资料填写链路。
-2. 第 6 步 `创建 Plus Checkout`:PayPal / GoPay 打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session。PayPal 使用 `DE / EUR` 与 `https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`;GoPay 使用 `ID / IDR` 与 `https://chatgpt.com/checkout/openai_llc/{checkout_session_id}`;GPC helper 模式改为把 accessToken、手机号、国家区号、`otp_channel` 等提交到 `gopayHelperApiUrl` 的 `/api/gp/tasks`,并通过 `gopayHelperApiKey` 发送 `X-API-Key` 认证,创建后保存 `task_id`;当前默认 GPC API 地址为 `https://gpc.qlhazycoder.top`,旧的 `https://gpc.leftcode.xyz` 配置会兼容归一到新地址。
+2. 第 6 步 `创建 Plus Checkout`:PayPal / GoPay 打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session。PayPal 使用 `DE / EUR` 与 `https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`;GoPay 使用 `ID / IDR` 与 `https://chatgpt.com/checkout/openai_llc/{checkout_session_id}`;GPC helper 模式改为把 accessToken、手机号、国家区号、`otp_channel` 等提交到 `gopayHelperApiUrl` 的 `/api/gp/tasks`,并通过 `gopayHelperApiKey` 发送 `X-API-Key` 认证,创建后保存 `task_id`;当前默认 GPC API 地址为 `https://gpc.qlhazycoder.top`。
3. 第 7 步 `填写账单并提交订阅`:PayPal / GoPay 仍走 checkout 页面与 Stripe iframe 自动化;GPC helper 模式不再操作 checkout iframe,而是轮询 `/api/gp/tasks/{task_id}`,根据远端 `api_waiting_for` 依次向 `/otp` 和 `/pin` 提交验证码与 PIN。若侧栏启用本地 OTP helper,后台会轮询 `gopayHelperLocalSmsHelperUrl` 的 `/latest-otp?phone=...&consume=1` 接口,按当前 GPC 手机号读取并消费当前 OTP 通道的验证码;未开启本地 helper 时才弹出手动 OTP 输入框。仓库内置的 `scripts/gpc_sms_helper_macos.py` 是 macOS Messages/本地通知兼容读取实现,其他通道需要提供兼容的本地 `/latest-otp` 或 `/otp` 接口。
4. 仅 PayPal / GoPay 会继续显示第 8 步:该步按当前支付方式显示为 `PayPal 登录与授权` 或 `GoPay 手机验证与授权`,底层 step key 仍为 `paypal-approve`。
5. 第 9 步 `订阅回跳确认` 仅在 PayPal / GoPay 模式下等待授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。GPC helper 模式在第 7 步任务完成后会直接进入 Plus 可见第 10 步 OAuth 登录。
From b56bde0c2ee14c58e4de0834762cd8c993384c6c Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Fri, 8 May 2026 10:23:38 +0800
Subject: [PATCH 3/5] fix: enhance email input detection and add tests for
unified OpenAI login page
---
content/signup-page.js | 50 ++++++++++++++++++++++++---
tests/step7-phone-login-entry.test.js | 16 +++++++++
2 files changed, 61 insertions(+), 5 deletions(-)
diff --git a/content/signup-page.js b/content/signup-page.js
index 797236a..9f5e152 100644
--- a/content/signup-page.js
+++ b/content/signup-page.js
@@ -3484,6 +3484,10 @@ function summarizePhoneInputCandidate(element, options = {}) {
const normalizedName = summary.name.toLowerCase();
const normalizedId = summary.id.toLowerCase();
const combinedText = `${normalizedName} ${normalizedId} ${summary.placeholder} ${summary.ariaLabel}`;
+ if (isLoginEmailLikeInput(element)) {
+ summary.skipReason = 'email_like';
+ return summary;
+ }
if (
summary.type === 'tel'
|| summary.autocomplete === 'tel'
@@ -3517,14 +3521,50 @@ function findUsablePhoneInput(selector, options = {}) {
.find((element) => isUsablePhoneInputElement(element, options)) || null;
}
+function getLoginInputAttributeText(input) {
+ return {
+ type: String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase(),
+ autocomplete: String(input?.getAttribute?.('autocomplete') || '').trim().toLowerCase(),
+ name: String(input?.getAttribute?.('name') || input?.name || '').trim(),
+ id: String(input?.getAttribute?.('id') || input?.id || '').trim(),
+ placeholder: String(input?.getAttribute?.('placeholder') || '').trim(),
+ ariaLabel: String(input?.getAttribute?.('aria-label') || '').trim(),
+ };
+}
+
+function isLoginEmailLikeInput(input) {
+ const summary = getLoginInputAttributeText(input);
+ const nameId = `${summary.name} ${summary.id}`;
+ const labelText = `${summary.placeholder} ${summary.ariaLabel}`;
+ return summary.type === 'email'
+ || summary.autocomplete === 'email'
+ || /email/i.test(nameId)
+ || /email|电子邮件|邮箱/i.test(labelText);
+}
+
function getLoginEmailInput() {
- const input = document.querySelector(
- 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]'
- );
- if (isLoginPhoneUsernameKind() || isLoginPhoneEntryPageText()) {
+ const input = Array.from(document.querySelectorAll([
+ 'input[type="email"]',
+ 'input[autocomplete="email"]',
+ 'input[name="email"]',
+ 'input[name="username"]',
+ 'input[autocomplete="username"]',
+ 'input[id*="email" i]',
+ 'input[placeholder*="email" i]',
+ 'input[placeholder*="Email"]',
+ 'input[placeholder*="电子邮件"]',
+ 'input[placeholder*="邮箱"]',
+ 'input[aria-label*="email" i]',
+ 'input[aria-label*="电子邮件"]',
+ 'input[aria-label*="邮箱"]',
+ ].join(', '))).find((candidate) => isVisibleElement(candidate)) || null;
+ if (!input) {
return null;
}
- return input && isVisibleElement(input) ? input : null;
+ if ((isLoginPhoneUsernameKind() || isLoginPhoneEntryPageText()) && !isLoginEmailLikeInput(input)) {
+ return null;
+ }
+ return input;
}
function getLoginPhoneInput() {
diff --git a/tests/step7-phone-login-entry.test.js b/tests/step7-phone-login-entry.test.js
index 832ec1f..92f45d7 100644
--- a/tests/step7-phone-login-entry.test.js
+++ b/tests/step7-phone-login-entry.test.js
@@ -135,14 +135,18 @@ ${extractFunction('getPageTextSnapshot')}
${extractFunction('isLoginPhoneUsernameKind')}
${extractFunction('isLoginPhoneEntryPageText')}
${extractFunction('isInsideHiddenPhoneControl')}
+${extractFunction('getLoginInputAttributeText')}
+${extractFunction('isLoginEmailLikeInput')}
${extractFunction('summarizePhoneInputCandidate')}
${extractFunction('isUsablePhoneInputElement')}
${extractFunction('collectPhoneInputCandidates')}
${extractFunction('findUsablePhoneInput')}
+${extractFunction('getLoginEmailInput')}
${extractFunction('getLoginPhoneInput')}
${extractFunction('isAddPhonePageReady')}
return {
+ getLoginEmailInput,
getLoginPhoneInput,
isAddPhonePageReady,
};
@@ -168,6 +172,18 @@ test('step 7 does not mistake email entry with a phone switch action for phone i
assert.equal(api.isAddPhonePageReady(), false);
});
+test('step 7 treats unified OpenAI login page as email input despite phone option text', () => {
+ const api = createPhoneLoginEntryApi({
+ href: 'https://auth.openai.com/log-in-or-create-account',
+ pathname: '/log-in-or-create-account',
+ pageText: '\u767b\u5f55\u6216\u6ce8\u518c \u7535\u5b50\u90ae\u4ef6\u5730\u5740 \u7ee7\u7eed \u4f7f\u7528\u7535\u8bdd\u53f7\u7801\u7ee7\u7eed',
+ inputAttributes: { type: 'text', placeholder: '\u7535\u5b50\u90ae\u4ef6\u5730\u5740' },
+ });
+
+ assert.ok(api.getLoginEmailInput(), 'unified login email input should be detected');
+ assert.equal(api.getLoginPhoneInput(), null, 'phone option button must not turn email input into phone input');
+});
+
test('step 7 detects username text input when usernameKind is phone_number', () => {
const api = createPhoneLoginEntryApi({
phoneUsernameKind: true,
From 83c5a6237cd3cfaa8338fde30398f7a1f9d884bb Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Fri, 8 May 2026 10:31:20 +0800
Subject: [PATCH 4/5] fix: enhance step 7 login identifier resolution and add
tests for phone/email handling
---
background/steps/oauth-login.js | 111 +++++++++++++--------
content/signup-page.js | 6 +-
tests/background-step6-retry-limit.test.js | 100 +++++++++++++++++++
tests/step7-phone-login-entry.test.js | 44 ++++++++
4 files changed, 219 insertions(+), 42 deletions(-)
diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js
index 07eb041..7c8e0bd 100644
--- a/background/steps/oauth-login.js
+++ b/background/steps/oauth-login.js
@@ -40,29 +40,64 @@
return /缺少|未配置|请输入|无效|错误|失败|401|认证失败|未授权|unauthorized|invalid/i.test(message);
}
+ function normalizeStep7IdentifierType(value = '') {
+ const normalized = String(value || '').trim().toLowerCase();
+ return normalized === 'phone' || normalized === 'email' ? normalized : '';
+ }
+
+ function normalizeStep7SignupMethod(value = '') {
+ return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
+ }
+
+ function canUseConfiguredPhoneSignup(state = {}) {
+ return normalizeStep7SignupMethod(state?.signupMethod) === 'phone'
+ && Boolean(state?.phoneVerificationEnabled)
+ && !Boolean(state?.plusModeEnabled)
+ && !Boolean(state?.contributionMode);
+ }
+
+ function resolveStep7LoginIdentifierType(state = {}, fallbackType = '') {
+ const explicitIdentifierType = normalizeStep7IdentifierType(state?.accountIdentifierType);
+ if (explicitIdentifierType) {
+ return explicitIdentifierType;
+ }
+
+ const frozenSignupMethod = normalizeStep7IdentifierType(state?.resolvedSignupMethod);
+ if (frozenSignupMethod) {
+ return frozenSignupMethod;
+ }
+
+ if (canUseConfiguredPhoneSignup(state)) {
+ return 'phone';
+ }
+
+ return normalizeStep7IdentifierType(fallbackType) || 'email';
+ }
+
async function executeStep7(state) {
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
const completionStep = visibleStep > 0 ? visibleStep : 7;
- const resolvedIdentifierType = String(
- state?.accountIdentifierType
- || (state?.signupPhoneNumber ? 'phone' : '')
- || ''
- ).trim().toLowerCase() === 'phone'
- ? 'phone'
- : 'email';
- const phoneNumber = String(
- state?.signupPhoneNumber
- || (resolvedIdentifierType === 'phone' ? state?.accountIdentifier : '')
- || state?.signupPhoneCompletedActivation?.phoneNumber
- || state?.signupPhoneActivation?.phoneNumber
- || ''
- ).trim();
- const email = String(
- state?.email
- || (resolvedIdentifierType === 'email' ? state?.accountIdentifier : '')
- || ''
- ).trim();
- if (!email && !phoneNumber) {
+ const resolvedIdentifierType = resolveStep7LoginIdentifierType(state);
+ const phoneNumber = resolvedIdentifierType === 'phone'
+ ? String(
+ state?.signupPhoneNumber
+ || (normalizeStep7IdentifierType(state?.accountIdentifierType) === 'phone' ? state?.accountIdentifier : '')
+ || state?.signupPhoneCompletedActivation?.phoneNumber
+ || state?.signupPhoneActivation?.phoneNumber
+ || ''
+ ).trim()
+ : '';
+ const email = resolvedIdentifierType === 'email'
+ ? String(
+ state?.email
+ || (normalizeStep7IdentifierType(state?.accountIdentifierType) === 'email' ? state?.accountIdentifier : '')
+ || ''
+ ).trim()
+ : '';
+ if (
+ (resolvedIdentifierType === 'phone' && !phoneNumber)
+ || (resolvedIdentifierType !== 'phone' && !email)
+ ) {
throw new Error('缺少登录账号:请先完成步骤 2,或在侧栏“注册邮箱/注册手机号”中手动填写账号后再执行当前步骤。');
}
@@ -75,25 +110,23 @@
try {
const currentState = attempt === 1 ? state : await getState();
const password = currentState.password || currentState.customPassword || '';
- const currentIdentifierType = String(
- currentState?.accountIdentifierType
- || (currentState?.signupPhoneNumber ? 'phone' : '')
- || resolvedIdentifierType
- ).trim().toLowerCase() === 'phone'
- ? 'phone'
- : 'email';
- const currentPhoneNumber = String(
- currentState?.signupPhoneNumber
- || (currentIdentifierType === 'phone' ? currentState?.accountIdentifier : '')
- || currentState?.signupPhoneCompletedActivation?.phoneNumber
- || currentState?.signupPhoneActivation?.phoneNumber
- || phoneNumber
- ).trim();
- const currentEmail = String(
- currentState?.email
- || (currentIdentifierType === 'email' ? currentState?.accountIdentifier : '')
- || email
- ).trim();
+ const currentIdentifierType = resolveStep7LoginIdentifierType(currentState, resolvedIdentifierType);
+ const currentPhoneNumber = currentIdentifierType === 'phone'
+ ? String(
+ currentState?.signupPhoneNumber
+ || (normalizeStep7IdentifierType(currentState?.accountIdentifierType) === 'phone' ? currentState?.accountIdentifier : '')
+ || currentState?.signupPhoneCompletedActivation?.phoneNumber
+ || currentState?.signupPhoneActivation?.phoneNumber
+ || phoneNumber
+ ).trim()
+ : '';
+ const currentEmail = currentIdentifierType === 'email'
+ ? String(
+ currentState?.email
+ || (normalizeStep7IdentifierType(currentState?.accountIdentifierType) === 'email' ? currentState?.accountIdentifier : '')
+ || email
+ ).trim()
+ : '';
const accountIdentifier = currentIdentifierType === 'phone'
? currentPhoneNumber
: currentEmail;
diff --git a/content/signup-page.js b/content/signup-page.js
index 9f5e152..d78aea6 100644
--- a/content/signup-page.js
+++ b/content/signup-page.js
@@ -5202,9 +5202,9 @@ async function step6OpenLoginEntry(payload, snapshot) {
const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7;
const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
const preferPhoneLogin = String(payload?.loginIdentifierType || '').trim() === 'phone' || (!payload?.email && payload?.phoneNumber);
- const trigger = preferPhoneLogin
- ? (currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger())
- : (currentSnapshot.loginEntryTrigger || findLoginEntryTrigger());
+ const genericEntryTrigger = currentSnapshot.loginEntryTrigger || findLoginEntryTrigger();
+ const phoneEntryTrigger = currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger();
+ const trigger = genericEntryTrigger || (preferPhoneLogin ? phoneEntryTrigger : null);
if (!trigger || !isActionEnabled(trigger)) {
return createStep6RecoverableResult('missing_login_entry_trigger', currentSnapshot, {
message: preferPhoneLogin
diff --git a/tests/background-step6-retry-limit.test.js b/tests/background-step6-retry-limit.test.js
index 09cfaaf..579977b 100644
--- a/tests/background-step6-retry-limit.test.js
+++ b/tests/background-step6-retry-limit.test.js
@@ -358,6 +358,106 @@ test('step 7 forwards phone login identity payload when account identifier is ph
]);
});
+test('step 7 keeps Plus email login even when phone sms runtime exists', async () => {
+ const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
+ const globalScope = {};
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
+
+ const events = {
+ payloads: [],
+ };
+
+ const executor = api.createStep7Executor({
+ addLog: async () => {},
+ completeStepFromBackground: async () => {},
+ getErrorMessage: (error) => error?.message || String(error || ''),
+ getLoginAuthStateLabel: (state) => state || 'unknown',
+ getState: async () => ({
+ plusModeEnabled: true,
+ phoneVerificationEnabled: true,
+ signupMethod: 'phone',
+ email: 'plus.user@example.com',
+ password: 'secret',
+ signupPhoneNumber: '+441111111111',
+ }),
+ isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
+ isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
+ refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
+ reuseOrCreateTab: async () => {},
+ sendToContentScriptResilient: async (_sourceName, message) => {
+ events.payloads.push(message.payload);
+ return {
+ step6Outcome: 'success',
+ state: 'verification_page',
+ loginVerificationRequestedAt: 123456,
+ };
+ },
+ STEP6_MAX_ATTEMPTS: 3,
+ throwIfStopped: () => {},
+ });
+
+ await executor.executeStep7({
+ plusModeEnabled: true,
+ phoneVerificationEnabled: true,
+ signupMethod: 'phone',
+ email: 'plus.user@example.com',
+ password: 'secret',
+ signupPhoneNumber: '+441111111111',
+ visibleStep: 10,
+ });
+
+ assert.equal(events.payloads[0].loginIdentifierType, 'email');
+ assert.equal(events.payloads[0].email, 'plus.user@example.com');
+ assert.equal(events.payloads[0].phoneNumber, '');
+ assert.equal(events.payloads[0].accountIdentifier, 'plus.user@example.com');
+});
+
+test('step 7 can infer phone login from an available phone signup configuration before step 2 finishes', async () => {
+ const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
+ const globalScope = {};
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
+
+ const events = {
+ payloads: [],
+ };
+
+ const executor = api.createStep7Executor({
+ addLog: async () => {},
+ completeStepFromBackground: async () => {},
+ getErrorMessage: (error) => error?.message || String(error || ''),
+ getLoginAuthStateLabel: (state) => state || 'unknown',
+ getState: async () => ({
+ phoneVerificationEnabled: true,
+ signupMethod: 'phone',
+ signupPhoneNumber: '+447780579093',
+ }),
+ isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
+ isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
+ refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
+ reuseOrCreateTab: async () => {},
+ sendToContentScriptResilient: async (_sourceName, message) => {
+ events.payloads.push(message.payload);
+ return {
+ step6Outcome: 'success',
+ state: 'phone_verification_page',
+ loginVerificationRequestedAt: 987654,
+ };
+ },
+ STEP6_MAX_ATTEMPTS: 3,
+ throwIfStopped: () => {},
+ });
+
+ await executor.executeStep7({
+ phoneVerificationEnabled: true,
+ signupMethod: 'phone',
+ signupPhoneNumber: '+447780579093',
+ });
+
+ assert.equal(events.payloads[0].loginIdentifierType, 'phone');
+ assert.equal(events.payloads[0].phoneNumber, '+447780579093');
+ assert.equal(events.payloads[0].email, '');
+});
+
test('step 7 can start from a manually filled signup phone without completed step 2 or step 3 state', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
diff --git a/tests/step7-phone-login-entry.test.js b/tests/step7-phone-login-entry.test.js
index 92f45d7..b1e1a0a 100644
--- a/tests/step7-phone-login-entry.test.js
+++ b/tests/step7-phone-login-entry.test.js
@@ -184,6 +184,50 @@ test('step 7 treats unified OpenAI login page as email input despite phone optio
assert.equal(api.getLoginPhoneInput(), null, 'phone option button must not turn email input into phone input');
});
+test('step 7 clicks the generic other-account entry before phone entry', async () => {
+ const api = new Function(`
+const clicks = [];
+const genericEntry = { id: 'generic', textContent: '\\u767b\\u5f55\\u81f3\\u53e6\\u4e00\\u4e2a\\u5e10\\u6237' };
+const phoneEntry = { id: 'phone', textContent: '\\u4f7f\\u7528\\u7535\\u8bdd\\u53f7\\u7801\\u7ee7\\u7eed' };
+
+function normalizeStep6Snapshot(snapshot) { return snapshot; }
+function inspectLoginAuthState() {
+ return { state: 'entry_page', loginEntryTrigger: genericEntry, phoneEntryTrigger: phoneEntry };
+}
+function findLoginEntryTrigger() { return genericEntry; }
+function findLoginPhoneEntryTrigger() { return phoneEntry; }
+function isActionEnabled() { return true; }
+function getActionText(el) { return el.textContent || ''; }
+function log() {}
+async function humanPause() {}
+function simulateClick(el) { clicks.push(el.id); }
+async function waitForLoginEntryOpenTransition() { return { state: 'phone_entry_page' }; }
+async function switchFromEmailPageToPhoneLogin() { return { routed: 'switch-phone' }; }
+async function step6LoginFromEmailPage() { return { routed: 'email' }; }
+async function step6LoginFromPasswordPage() { return { routed: 'password' }; }
+async function step6LoginFromPhonePage() { return { routed: 'phone' }; }
+async function finalizeStep6VerificationReady() { return { routed: 'verification' }; }
+function createStep6OAuthConsentSuccessResult() { return { routed: 'oauth' }; }
+function createStep6AddEmailSuccessResult() { return { routed: 'add-email' }; }
+async function createStep6LoginTimeoutRecoveryTransition() { return { action: 'recoverable', result: { routed: 'recoverable' } }; }
+function createStep6RecoverableResult(reason, snapshot, options = {}) {
+ return { step6Outcome: 'recoverable', reason, state: snapshot?.state, message: options.message || '' };
+}
+
+${extractFunction('step6OpenLoginEntry')}
+
+return { clicks, step6OpenLoginEntry };
+ `)();
+
+ const result = await api.step6OpenLoginEntry(
+ { loginIdentifierType: 'phone', phoneNumber: '+441111111111' },
+ { state: 'entry_page', loginEntryTrigger: { id: 'generic', textContent: '\u767b\u5f55\u81f3\u53e6\u4e00\u4e2a\u5e10\u6237' }, phoneEntryTrigger: { id: 'phone', textContent: '\u4f7f\u7528\u7535\u8bdd\u53f7\u7801\u7ee7\u7eed' } }
+ );
+
+ assert.deepStrictEqual(api.clicks, ['generic']);
+ assert.equal(result.routed, 'phone');
+});
+
test('step 7 detects username text input when usernameKind is phone_number', () => {
const api = createPhoneLoginEntryApi({
phoneUsernameKind: true,
From d5a972b297f88270a518cce4c2168e115e55da60 Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Fri, 8 May 2026 11:07:47 +0800
Subject: [PATCH 5/5] chore(release): bump version to Ultra7.3
---
manifest.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/manifest.json b/manifest.json
index 0532b5f..33d7996 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,8 +1,8 @@
{
"manifest_version": 3,
"name": "codex-oauth-automation-extension",
- "version": "7.0",
- "version_name": "Ultra7.0",
+ "version": "7.3",
+ "version_name": "Ultra7.3",
"description": "用于自动执行多步骤 OAuth 注册流程",
"permissions": [
"sidePanel",