fix: improve step 2 email switch handling
This commit is contained in:
+17
@@ -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 变体,优先考虑把按钮识别继续升级成“关键语义词 + 排除词 + 结构位置”三段式判断,抗漂移会更强。
|
||||
|
||||
+4
-1
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
+129
-3
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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 停止。
|
||||
|
||||
+1
-1
@@ -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 地址获取所需的页面打开、脚本注入和通信。
|
||||
|
||||
Reference in New Issue
Block a user