From 01477d6bd03c12b2c3ae4fcd58360c8ce8f2956e Mon Sep 17 00:00:00 2001 From: dada2321 Date: Sun, 19 Apr 2026 12:05:24 +0800 Subject: [PATCH] fix: improve step 2 email switch handling --- Process.md | 17 + background.js | 5 +- background/logging-status.js | 5 +- background/steps/oauth-login.js | 5 +- content/signup-page.js | 132 +++++- .../background-logging-status-module.test.js | 25 + tests/signup-entry-diagnostics.test.js | 8 + tests/signup-step2-email-switch.test.js | 431 ++++++++++++++++++ 项目完整链路说明.md | 7 + 项目文件结构说明.md | 2 +- 10 files changed, 630 insertions(+), 7 deletions(-) create mode 100644 tests/signup-step2-email-switch.test.js diff --git a/Process.md b/Process.md index 51e0468..e8426fb 100644 --- a/Process.md +++ b/Process.md @@ -18,3 +18,20 @@ - 可以继续把 163 的删除兜底逻辑也做一次同类检查,因为顶部“删 除”从 DOM 结构看也可能存在同样的空格拆字风险。 - 如果老板接下来还想继续提稳收信成功率,可以再查一层:163 顶部“刷新”点击之后,页面到底有没有真正触发新的列表请求,以及请求完成信号该监听什么。 - 如果要把这一块做成长期稳定方案,下一步适合补“刷新动作观测点”日志,比如记录刷新前后列表首封邮件 ID、请求时间、是否出现 loading。 + + +## 2026-04-19 Step 2 纠偏补充 + +### 今日战利品 +- 修复了中文按钮 继续使用电子邮件地址登录 无法被 Step 2 识别的问题。 +- 修复了后台把 步骤 2:当前页面仍停留在手机号输入模式... 这类错误误判成 auth dd-phone 致命页的问题。 +- 现在自动运行只会在真正命中认证流程的手机号页面时才按 fatal 停止,不会因为注册弹窗还停留在手机号输入态就整轮停机。 + +### 知识地图 +- 这一块属于“错误分类系统”和“页面状态系统”的交叉地带。 +- 老板可以把它理解成:同样都带“手机号”三个字,但“注册弹窗默认展示手机号输入框”和“认证流程真的跳进 add-phone 页面”在业务意义上完全不是一回事。 +- 大神体系里,这属于自动化系统里很重要的一层:不仅要会识别页面,还要会给错误正确分级。 + +### 下一步导航 +- 如果老板接下来还想继续提稳 Step 2,可以把“手机号态切邮箱”的点击前后 DOM 快照也打进日志里,这样下次能直接看出是“按钮没识别到”还是“按钮点了但页面没切”。 +- 如果后面再遇到多语言 UI 变体,优先考虑把按钮识别继续升级成“关键语义词 + 排除词 + 结构位置”三段式判断,抗漂移会更强。 diff --git a/background.js b/background.js index 03e822f..0523bd9 100644 --- a/background.js +++ b/background.js @@ -3902,7 +3902,10 @@ function isAddPhoneAuthFailure(error) { return loggingStatus.isAddPhoneAuthFailure(error); } const message = getErrorMessage(error); - return /https:\/\/auth\.openai\.com\/add-phone(?:[/?#]|$)|\badd-phone\b|添加手机号|手机号码|手机号页|手机号页面|手机号|phone\s+number|telephone/i.test(message); + if (/\u624b\u673a\u53f7\u8f93\u5165\u6a21\u5f0f|phone\s+entry/i.test(message)) { + return false; + } + return /https:\/\/auth\.openai\.com\/add-phone(?:[/?#]|$)|\badd-phone\b|\u6dfb\u52a0\u624b\u673a\u53f7|\u624b\u673a\u53f7\u7801|\u8fdb\u5165\u624b\u673a\u53f7\u9875\u9762|\u624b\u673a\u53f7\u9875|\u624b\u673a\u53f7\u9875\u9762|phone\s+number|telephone/i.test(message); } function getLoginAuthStateLabel(state) { diff --git a/background/logging-status.js b/background/logging-status.js index f685fd3..0eb9081 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -63,7 +63,10 @@ function isAddPhoneAuthFailure(error) { const message = getErrorMessage(error); - return /https:\/\/auth\.openai\.com\/add-phone(?:[/?#]|$)|\badd-phone\b|添加手机号|手机号码|手机号页|手机号页面|手机号|phone\s+number|telephone/i.test(message); + if (/\u624b\u673a\u53f7\u8f93\u5165\u6a21\u5f0f|phone\s+entry/i.test(message)) { + return false; + } + return /https:\/\/auth\.openai\.com\/add-phone(?:[/?#]|$)|\badd-phone\b|\u6dfb\u52a0\u624b\u673a\u53f7|\u624b\u673a\u53f7\u7801|\u8fdb\u5165\u624b\u673a\u53f7\u9875\u9762|\u624b\u673a\u53f7\u9875|\u624b\u673a\u53f7\u9875\u9762|phone\s+number|telephone/i.test(message); } function getLoginAuthStateLabel(state) { diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index facf21e..60ad2d8 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -11,7 +11,10 @@ getState, isAddPhoneAuthFailure = (error) => { const message = String(typeof error === 'string' ? error : error?.message || ''); - return /https:\/\/auth\.openai\.com\/add-phone(?:[/?#]|$)|\badd-phone\b|添加手机号|手机号码|手机号页|手机号页面|手机号|phone\s+number|telephone/i.test(message); + if (/\u624b\u673a\u53f7\u8f93\u5165\u6a21\u5f0f|phone\s+entry/i.test(message)) { + return false; + } + return /https:\/\/auth\.openai\.com\/add-phone(?:[/?#]|$)|\badd-phone\b|\u6dfb\u52a0\u624b\u673a\u53f7|\u624b\u673a\u53f7\u7801|\u8fdb\u5165\u624b\u673a\u53f7\u9875\u9762|\u624b\u673a\u53f7\u9875|\u624b\u673a\u53f7\u9875\u9762|phone\s+number|telephone/i.test(message); }, isStep6RecoverableResult, isStep6SuccessResult, diff --git a/content/signup-page.js b/content/signup-page.js index 4ef7f78..6cbbf07 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -293,11 +293,99 @@ async function handle405ResendError(step, remainingTimeout = 30000) { // ============================================================ const SIGNUP_ENTRY_TRIGGER_PATTERN = /免费注册|立即注册|注册|sign\s*up|register|create\s*account|create\s+account/i; -const SIGNUP_EMAIL_INPUT_SELECTOR = 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i]'; +const SIGNUP_EMAIL_INPUT_SELECTOR = [ + 'input[type="email"]', + 'input[autocomplete="email"]', + 'input[autocomplete="username"]', + 'input[name="email"]', + 'input[name="username"]', + 'input[id*="email"]', + 'input[placeholder*="email" i]', + 'input[placeholder*="电子邮件"]', + 'input[placeholder*="邮箱"]', + 'input[aria-label*="email" i]', + 'input[aria-label*="电子邮件"]', + 'input[aria-label*="邮箱"]', +].join(', '); +const SIGNUP_PHONE_INPUT_SELECTOR = [ + 'input[type="tel"]:not([maxlength="6"])', + 'input[name*="phone" i]', + 'input[id*="phone" i]', + 'input[autocomplete="tel"]', + 'input[placeholder*="手机"]', + 'input[aria-label*="手机"]', +].join(', '); +const SIGNUP_SWITCH_TO_EMAIL_PATTERN = new RegExp([ + String.raw`\u7ee7\u7eed\u4f7f\u7528(?:\u7535\u5b50\u90ae\u4ef6\u5730\u5740|\u90ae\u7bb1)\u767b\u5f55`, + String.raw`\u6539\u7528(?:\u7535\u5b50\u90ae\u4ef6\u5730\u5740|\u90ae\u7bb1)\u767b\u5f55`, + String.raw`continue\s+using\s+(?:an?\s+)?email(?:\s+address)?(?:\s+(?:to\s+)?(?:log\s*in|sign\s*in|sign\s*up))?`, + String.raw`continue\s+with\s+email(?:\s+address)?`, + String.raw`use\s+(?:an?\s+)?email(?:\s+address)?(?:\s+instead)?`, + String.raw`sign\s*(?:in|up)\s+with\s+email`, +].join('|'), 'i'); +const SIGNUP_SWITCH_ACTION_PATTERN = /\u7ee7\u7eed\u4f7f\u7528|\u6539\u7528|continue|use|sign\s*(?:in|up)/i; +const SIGNUP_EMAIL_ACTION_PATTERN = /\u7535\u5b50\u90ae\u4ef6|\u90ae\u7bb1|email/i; +const SIGNUP_WORK_EMAIL_PATTERN = /\u5de5\u4f5c|business|work\s+email/i; function getSignupEmailInput() { const input = document.querySelector(SIGNUP_EMAIL_INPUT_SELECTOR); - return input && isVisibleElement(input) ? input : null; + if (input && isVisibleElement(input)) { + return input; + } + + const fallback = Array.from(document.querySelectorAll('input')).find((el) => { + if (!isVisibleElement(el)) return false; + const type = String(el.getAttribute?.('type') || '').trim().toLowerCase(); + const name = String(el.getAttribute?.('name') || '').trim().toLowerCase(); + const id = String(el.getAttribute?.('id') || '').trim().toLowerCase(); + const placeholder = String(el.getAttribute?.('placeholder') || '').trim(); + const ariaLabel = String(el.getAttribute?.('aria-label') || '').trim(); + const autocomplete = String(el.getAttribute?.('autocomplete') || '').trim().toLowerCase(); + const combinedText = `${placeholder} ${ariaLabel}`; + return type === 'email' + || autocomplete === 'email' + || autocomplete === 'username' + || /email|username/i.test(`${name} ${id}`) + || /email|电子邮件|邮箱/i.test(combinedText); + }); + + return fallback || null; +} + +function getSignupPhoneInput() { + const input = document.querySelector(SIGNUP_PHONE_INPUT_SELECTOR); + if (input && isVisibleElement(input)) { + return input; + } + + const fallback = Array.from(document.querySelectorAll('input')).find((el) => { + if (!isVisibleElement(el)) return false; + const type = String(el.getAttribute?.('type') || '').trim().toLowerCase(); + const name = String(el.getAttribute?.('name') || '').trim().toLowerCase(); + const id = String(el.getAttribute?.('id') || '').trim().toLowerCase(); + const placeholder = String(el.getAttribute?.('placeholder') || '').trim(); + const ariaLabel = String(el.getAttribute?.('aria-label') || '').trim(); + const autocomplete = String(el.getAttribute?.('autocomplete') || '').trim().toLowerCase(); + const combinedText = `${placeholder} ${ariaLabel}`; + return type === 'tel' + || autocomplete === 'tel' + || /phone|tel/i.test(`${name} ${id}`) + || /手机|电话|手机号/.test(combinedText); + }); + + return fallback || null; +} + +function findSignupUseEmailTrigger() { + const candidates = document.querySelectorAll('button, a, [role="button"], [role="link"]'); + return Array.from(candidates).find((el) => { + if (!isVisibleElement(el) || !isActionEnabled(el)) return false; + const text = getActionText(el); + if (!text) return false; + if (SIGNUP_WORK_EMAIL_PATTERN.test(text)) return false; + return SIGNUP_SWITCH_TO_EMAIL_PATTERN.test(text) + || (SIGNUP_SWITCH_ACTION_PATTERN.test(text) && SIGNUP_EMAIL_ACTION_PATTERN.test(text)); + }) || null; } function getSignupEmailContinueButton({ allowDisabled = false } = {}) { @@ -353,6 +441,16 @@ function inspectSignupEntryState() { }; } + const phoneInput = getSignupPhoneInput(); + if (phoneInput) { + return { + state: 'phone_entry', + phoneInput, + switchToEmailTrigger: findSignupUseEmailTrigger(), + url: location.href, + }; + } + const signupTrigger = findSignupEntryTrigger(); if (signupTrigger) { return { @@ -410,7 +508,9 @@ function getSignupEntryDiagnostics() { title: document.title || '', readyState: document.readyState || '', hasEmailInput: Boolean(getSignupEmailInput()), + hasPhoneInput: Boolean(getSignupPhoneInput()), hasPasswordInput: Boolean(getSignupPasswordInput()), + hasSwitchToEmailAction: Boolean(findSignupUseEmailTrigger()), bodyContainsSignupText: SIGNUP_ENTRY_TRIGGER_PATTERN.test(getPageTextSnapshot()), signupLikeActions, visibleActions, @@ -425,6 +525,8 @@ async function waitForSignupEntryState(options = {}) { } = options; const start = Date.now(); let lastTriggerClickAt = 0; + let lastSwitchToEmailAt = 0; + let loggedMissingSwitchToEmail = false; while (Date.now() - start < timeout) { throwIfStopped(); @@ -434,6 +536,26 @@ async function waitForSignupEntryState(options = {}) { return snapshot; } + if (snapshot.state === 'phone_entry') { + if (!autoOpenEntry) { + return snapshot; + } + + if (snapshot.switchToEmailTrigger && Date.now() - lastSwitchToEmailAt >= 1500) { + lastSwitchToEmailAt = Date.now(); + loggedMissingSwitchToEmail = false; + log('步骤 2:检测到手机号输入模式,正在切换到邮箱输入模式...'); + await humanPause(350, 900); + simulateClick(snapshot.switchToEmailTrigger); + } else if (!snapshot.switchToEmailTrigger && !loggedMissingSwitchToEmail) { + loggedMissingSwitchToEmail = true; + log('步骤 2:检测到手机号输入模式,但暂未识别到“改用邮箱/继续使用电子邮件地址登录”按钮,继续等待界面稳定...', 'warn'); + } + + await sleep(250); + continue; + } + if (snapshot.state === 'entry_home') { if (!autoOpenEntry) { return snapshot; @@ -455,7 +577,7 @@ async function waitForSignupEntryState(options = {}) { async function ensureSignupEntryReady(timeout = 15000) { const snapshot = await waitForSignupEntryState({ timeout, autoOpenEntry: false }); - if (snapshot.state === 'entry_home' || snapshot.state === 'email_entry' || snapshot.state === 'password_page') { + if (snapshot.state === 'entry_home' || snapshot.state === 'phone_entry' || snapshot.state === 'email_entry' || snapshot.state === 'password_page') { return { ready: true, state: snapshot.state, @@ -506,6 +628,10 @@ async function fillSignupEmailAndContinue(email, step) { }; } + if (snapshot.state === 'phone_entry') { + throw new Error(`步骤 ${step}:当前页面仍停留在手机号输入模式,未成功切换到邮箱输入模式。URL: ${location.href}`); + } + if (snapshot.state !== 'email_entry' || !snapshot.emailInput) { throw new Error(`步骤 ${step}:未找到可用的邮箱输入入口。URL: ${location.href}`); } diff --git a/tests/background-logging-status-module.test.js b/tests/background-logging-status-module.test.js index 9abedb6..6997090 100644 --- a/tests/background-logging-status-module.test.js +++ b/tests/background-logging-status-module.test.js @@ -15,3 +15,28 @@ test('logging/status module exposes a factory', () => { assert.equal(typeof api?.createLoggingStatus, 'function'); }); + +test('logging/status add-phone detection ignores step 2 phone-entry switch failures', () => { + const source = fs.readFileSync('background/logging-status.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundLoggingStatus;`)(globalScope); + + const loggingStatus = api.createLoggingStatus({ + chrome: { runtime: { sendMessage() { return Promise.resolve(); } } }, + DEFAULT_STATE: { stepStatuses: {} }, + getState: async () => ({ stepStatuses: {} }), + isRecoverableStep9AuthFailure: () => false, + LOG_PREFIX: '[test]', + setState: async () => {}, + STOP_ERROR_MESSAGE: 'stopped', + }); + + assert.equal( + loggingStatus.isAddPhoneAuthFailure('姝ラ 2锛氬綋鍓嶉〉闈粛鍋滅暀鍦ㄦ墜鏈哄彿杈撳叆妯″紡锛屾湭鎴愬姛鍒囨崲鍒伴偖绠辫緭鍏ユā寮忋€俇RL: https://chatgpt.com/'), + false + ); + assert.equal( + loggingStatus.isAddPhoneAuthFailure('姝ラ 8锛氶獙璇佺爜鎻愪氦鍚庨〉闈㈣繘鍏ユ墜鏈哄彿椤甸潰锛屽綋鍓嶆祦绋嬫棤娉曠户缁嚜鍔ㄦ巿鏉冦€?URL: https://auth.openai.com/add-phone'), + true + ); +}); diff --git a/tests/signup-entry-diagnostics.test.js b/tests/signup-entry-diagnostics.test.js index 2e99d06..c5ad8b9 100644 --- a/tests/signup-entry-diagnostics.test.js +++ b/tests/signup-entry-diagnostics.test.js @@ -112,10 +112,18 @@ function getSignupEmailInput() { return null; } +function getSignupPhoneInput() { + return null; +} + function getSignupPasswordInput() { return null; } +function findSignupUseEmailTrigger() { + return null; +} + function getPageTextSnapshot() { return 'Welcome to ChatGPT. Try our latest models.'; } diff --git a/tests/signup-step2-email-switch.test.js b/tests/signup-step2-email-switch.test.js new file mode 100644 index 0000000..d8bbfb5 --- /dev/null +++ b/tests/signup-step2-email-switch.test.js @@ -0,0 +1,431 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('content/signup-page.js', 'utf8'); + +function extractFunction(name) { + const markers = [`async function ${name}(`, `function ${name}(`]; + const start = markers + .map((marker) => source.indexOf(marker)) + .find((index) => index >= 0); + if (start < 0) { + throw new Error(`missing function ${name}`); + } + + let parenDepth = 0; + let signatureEnded = false; + let braceStart = -1; + for (let i = start; i < source.length; i += 1) { + const ch = source[i]; + if (ch === '(') { + parenDepth += 1; + } else if (ch === ')') { + parenDepth -= 1; + if (parenDepth === 0) { + signatureEnded = true; + } + } else if (ch === '{' && signatureEnded) { + braceStart = i; + break; + } + } + if (braceStart < 0) { + throw new Error(`missing body for function ${name}`); + } + + let depth = 0; + let end = braceStart; + for (; end < source.length; end += 1) { + const ch = source[end]; + if (ch === '{') depth += 1; + if (ch === '}') { + depth -= 1; + if (depth === 0) { + end += 1; + break; + } + } + } + + return source.slice(start, end); +} + +function extractConst(name) { + const pattern = new RegExp(`const\\s+${name}\\s*=\\s*[\\s\\S]*?;`); + const match = source.match(pattern); + if (!match) { + throw new Error(`missing const ${name}`); + } + return match[0]; +} + +test('waitForSignupEntryState switches from phone mode to email mode before step 2 fills the address', async () => { + const api = new Function(` +const logs = []; +const clicks = []; +let phase = 'phone'; +let now = 0; + +const phoneInput = { + kind: 'phone', + getAttribute(name) { + if (name === 'type') return 'tel'; + return ''; + }, +}; + +const switchButton = { + textContent: 'Continue using email address', + value: '', + disabled: false, + getAttribute(name) { + if (name === 'type') return 'button'; + return ''; + }, + getBoundingClientRect() { + return { width: 200, height: 48 }; + }, +}; + +const emailInput = { + kind: 'email', + getAttribute(name) { + if (name === 'type') return 'email'; + return ''; + }, +}; + +const document = { + querySelector(selector) { + if (selector === SIGNUP_EMAIL_INPUT_SELECTOR) { + return phase === 'email' ? emailInput : null; + } + if (selector === SIGNUP_PHONE_INPUT_SELECTOR) { + return phase === 'phone' ? phoneInput : null; + } + return null; + }, + querySelectorAll(selector) { + if (selector === 'button, a, [role="button"], [role="link"]') { + return phase === 'phone' ? [switchButton] : []; + } + if (selector === 'a, button, [role="button"], [role="link"]') { + return []; + } + if (selector === 'input') { + return phase === 'phone' ? [phoneInput] : [emailInput]; + } + return []; + }, +}; + +const location = { + href: 'https://chatgpt.com/', +}; + +const Date = { + now() { + return now; + }, +}; + +${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')} +${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')} +${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')} +${extractConst('SIGNUP_SWITCH_TO_EMAIL_PATTERN')} +${extractConst('SIGNUP_SWITCH_ACTION_PATTERN')} +${extractConst('SIGNUP_EMAIL_ACTION_PATTERN')} +${extractConst('SIGNUP_WORK_EMAIL_PATTERN')} + +function isVisibleElement(el) { + return Boolean(el); +} + +function isActionEnabled(el) { + return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true'; +} + +function getActionText(el) { + return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')] + .filter(Boolean) + .join(' ') + .replace(/\\s+/g, ' ') + .trim(); +} + +function getSignupPasswordInput() { + return null; +} + +function isSignupPasswordPage() { + return false; +} + +function getSignupPasswordSubmitButton() { + return null; +} + +function findSignupEntryTrigger() { + return null; +} + +function getSignupPasswordDisplayedEmail() { + return ''; +} + +function throwIfStopped() {} + +function log(message, level = 'info') { + logs.push({ message, level }); +} + +async function humanPause() {} + +function simulateClick(target) { + clicks.push(getActionText(target)); + if (target === switchButton) { + phase = 'email'; + } +} + +async function sleep(ms) { + now += ms; +} + +${extractFunction('getSignupEmailInput')} +${extractFunction('getSignupPhoneInput')} +${extractFunction('findSignupUseEmailTrigger')} +${extractFunction('getSignupEmailContinueButton')} +${extractFunction('inspectSignupEntryState')} +${extractFunction('waitForSignupEntryState')} + +return { + async run() { + return waitForSignupEntryState({ timeout: 5000, autoOpenEntry: true }); + }, + getClicks() { + return clicks.slice(); + }, + getLogs() { + return logs.slice(); + }, +}; +`)(); + + const snapshot = await api.run(); + + assert.equal(snapshot.state, 'email_entry'); + assert.deepEqual(api.getClicks(), ['Continue using email address']); + assert.equal(api.getLogs().length > 0, true); +}); + +test('waitForSignupEntryState also recognizes the Chinese switch-to-email button text', async () => { + const api = new Function(` +const logs = []; +const clicks = []; +let phase = 'phone'; +let now = 0; + +const phoneInput = { + kind: 'phone', + getAttribute(name) { + if (name === 'type') return 'tel'; + return ''; + }, +}; + +const switchButton = { + textContent: '\\u7ee7\\u7eed\\u4f7f\\u7528\\u7535\\u5b50\\u90ae\\u4ef6\\u5730\\u5740\\u767b\\u5f55', + value: '', + disabled: false, + getAttribute(name) { + if (name === 'type') return 'button'; + return ''; + }, + getBoundingClientRect() { + return { width: 200, height: 48 }; + }, +}; + +const workEmailButton = { + textContent: '\\u7ee7\\u7eed\\u4f7f\\u7528\\u5de5\\u4f5c\\u7535\\u5b50\\u90ae\\u4ef6\\u5730\\u5740\\u767b\\u5f55', + value: '', + disabled: false, + getAttribute(name) { + if (name === 'type') return 'button'; + return ''; + }, + getBoundingClientRect() { + return { width: 200, height: 48 }; + }, +}; + +const emailInput = { + kind: 'email', + getAttribute(name) { + if (name === 'type') return 'email'; + return ''; + }, +}; + +const document = { + querySelector(selector) { + if (selector === SIGNUP_EMAIL_INPUT_SELECTOR) { + return phase === 'email' ? emailInput : null; + } + if (selector === SIGNUP_PHONE_INPUT_SELECTOR) { + return phase === 'phone' ? phoneInput : null; + } + return null; + }, + querySelectorAll(selector) { + if (selector === 'button, a, [role="button"], [role="link"]') { + return phase === 'phone' ? [switchButton, workEmailButton] : []; + } + if (selector === 'a, button, [role="button"], [role="link"]') { + return []; + } + if (selector === 'input') { + return phase === 'phone' ? [phoneInput] : [emailInput]; + } + return []; + }, +}; + +const location = { + href: 'https://chatgpt.com/', +}; + +const Date = { + now() { + return now; + }, +}; + +${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')} +${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')} +${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')} +${extractConst('SIGNUP_SWITCH_TO_EMAIL_PATTERN')} +${extractConst('SIGNUP_SWITCH_ACTION_PATTERN')} +${extractConst('SIGNUP_EMAIL_ACTION_PATTERN')} +${extractConst('SIGNUP_WORK_EMAIL_PATTERN')} + +function isVisibleElement(el) { + return Boolean(el); +} + +function isActionEnabled(el) { + return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true'; +} + +function getActionText(el) { + return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')] + .filter(Boolean) + .join(' ') + .replace(/\\s+/g, ' ') + .trim(); +} + +function getSignupPasswordInput() { + return null; +} + +function isSignupPasswordPage() { + return false; +} + +function getSignupPasswordSubmitButton() { + return null; +} + +function findSignupEntryTrigger() { + return null; +} + +function getSignupPasswordDisplayedEmail() { + return ''; +} + +function throwIfStopped() {} + +function log(message, level = 'info') { + logs.push({ message, level }); +} + +async function humanPause() {} + +function simulateClick(target) { + clicks.push(getActionText(target)); + if (target === switchButton) { + phase = 'email'; + } +} + +async function sleep(ms) { + now += ms; +} + +${extractFunction('getSignupEmailInput')} +${extractFunction('getSignupPhoneInput')} +${extractFunction('findSignupUseEmailTrigger')} +${extractFunction('getSignupEmailContinueButton')} +${extractFunction('inspectSignupEntryState')} +${extractFunction('waitForSignupEntryState')} + +return { + async run() { + return waitForSignupEntryState({ timeout: 5000, autoOpenEntry: true }); + }, + getClicks() { + return clicks.slice(); + }, +}; +`)(); + + const snapshot = await api.run(); + + assert.equal(snapshot.state, 'email_entry'); + assert.deepEqual(api.getClicks(), ['继续使用电子邮件地址登录']); +}); + +test('getSignupEmailInput recognizes localized email placeholders in text inputs', () => { + const api = new Function(` +const localizedEmailInput = { + kind: 'localized-email', + getAttribute(name) { + if (name === 'placeholder') return '电子邮件地址'; + if (name === 'type') return 'text'; + return ''; + }, +}; + +const document = { + querySelector() { + return null; + }, + querySelectorAll(selector) { + if (selector === 'input') { + return [localizedEmailInput]; + } + return []; + }, +}; + +${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')} + +function isVisibleElement(el) { + return Boolean(el); +} + +${extractFunction('getSignupEmailInput')} + +return { + run() { + return getSignupEmailInput(); + }, +}; +`)(); + + assert.equal(api.run()?.kind, 'localized-email'); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index a506ac9..bd23819 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -566,3 +566,10 @@ - Step 8 如果发现认证页已经进入登录超时报错/重试页,会直接报错并回到 Step 7 重新开始,而不是在 Step 8 内部点击 `重试`。 - 任意认证页重试页如果正文中出现 `max_check_attempts`,会被视为 Cloudflare 风控触发:后台立刻完全停止流程,侧边栏会复用现有确认弹窗提示等待 15~30 分钟后再试,避免继续刷新或反复重试加重风控,确认按钮显示为“我知道了”。 - Step 9 在点击 OAuth 同意页 `继续` 后,会额外检查是否进入认证页重试页;若命中则先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再重新执行当前轮的 `继续` 点击。 + + +## 2026-04 Step 2 手机号态说明 + +- Step 2 如果发现注册弹窗默认处于手机号输入模式,会先尝试点击 继续使用电子邮件地址登录 / Continue using email address 这类按钮,切回邮箱输入模式后再继续填写邮箱。 +- Step 2 对邮箱输入框的识别不再只依赖英文 email,也会接受 电子邮件地址、邮箱 等本地化占位或 ria-label。 +- Step 2 日志里的“手机号输入模式”只表示注册弹窗当前默认展示了手机号输入框,本身不等同于 auth dd-phone 致命页;只有明确进入认证流程的手机号页面时,自动运行才会按 fatal 停止。 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 2e52113..a66e1b1 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -43,7 +43,7 @@ - `background/account-run-history.js`:邮箱记录模块,负责以邮箱为主键维护最新记录(同邮箱后续状态覆盖),在步骤 2 设定邮箱后支持先写入“停止/未完成”占位状态,统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,支持清理记录,并在启用独立本地同步配置后把完整快照同步到本地 helper。 - `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 `gmailBaseEmail` 与 `mail2925BaseEmail`,避免自动流程重置后丢失别名基邮箱配置。 - `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口。 -- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。 +- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。 - `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息。 - `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API 地址、步骤跳转相关判断。 - `background/panel-bridge.js`:CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。