fix: recover signup email entry from phone-mode defaults

- 合并 PR #113 的核心改动:让 Step 2 在注册弹窗默认手机号模式下自动切回邮箱输入,并兼容本地化邮箱输入识别\n- 本地补充修复:保留 dev 上更完整的 163 邮箱实现,并避免把 Step 2 的 phone entry 提示误判成 auth add-phone 致命错误\n- 影响范围:signup content script、background add-phone detection、Step 2 回归测试、项目链路与结构文档
This commit is contained in:
QLHazyCoder
2026-04-25 14:55:07 +08:00
committed by GitHub
9 changed files with 635 additions and 13 deletions
+4 -1
View File
@@ -4234,7 +4234,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) {
+4 -1
View File
@@ -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) {
+4 -1
View File
@@ -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,
+142 -3
View File
@@ -291,11 +291,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 } = {}) {
@@ -351,6 +439,16 @@ function inspectSignupEntryState() {
};
}
const phoneInput = getSignupPhoneInput();
if (phoneInput) {
return {
state: 'phone_entry',
phoneInput,
switchToEmailTrigger: findSignupUseEmailTrigger(),
url: location.href,
};
}
const signupTrigger = findSignupEntryTrigger();
if (signupTrigger) {
return {
@@ -371,6 +469,7 @@ function getSignupEntryStateSummary(snapshot = inspectSignupEntryState()) {
state: snapshot?.state || 'unknown',
url: snapshot?.url || location.href,
hasEmailInput: Boolean(snapshot?.emailInput || getSignupEmailInput()),
hasPhoneInput: Boolean(snapshot?.phoneInput || getSignupPhoneInput()),
hasPasswordInput: Boolean(snapshot?.passwordInput || getSignupPasswordInput()),
};
@@ -393,6 +492,14 @@ function getSignupEntryStateSummary(snapshot = inspectSignupEntryState()) {
};
}
if (snapshot?.switchToEmailTrigger) {
summary.switchToEmailTrigger = {
tag: (snapshot.switchToEmailTrigger.tagName || '').toLowerCase(),
text: getActionText(snapshot.switchToEmailTrigger).slice(0, 80),
enabled: isActionEnabled(snapshot.switchToEmailTrigger),
};
}
return summary;
}
@@ -522,7 +629,9 @@ function getSignupEntryDiagnostics() {
devicePixelRatio: Number(view?.devicePixelRatio) || 0,
},
hasEmailInput: Boolean(getSignupEmailInput()),
hasPhoneInput: Boolean(getSignupPhoneInput()),
hasPasswordInput: Boolean(getSignupPasswordInput()),
hasSwitchToEmailAction: Boolean(findSignupUseEmailTrigger()),
bodyContainsSignupText: SIGNUP_ENTRY_TRIGGER_PATTERN.test(getPageTextSnapshot()),
signupLikeActionCounts: {
total: signupLikeActions.length,
@@ -653,6 +762,8 @@ async function waitForSignupEntryState(options = {}) {
let clickAttempts = 0;
let lastState = '';
let slowSnapshotLogged = false;
let lastSwitchToEmailAt = 0;
let loggedMissingSwitchToEmail = false;
while (Date.now() - start < timeout) {
throwIfStopped();
@@ -667,6 +778,34 @@ 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;
if (logDiagnostics) {
log(`步骤 ${step}:检测到手机号输入模式,准备点击切换邮箱入口:"${getActionText(snapshot.switchToEmailTrigger).slice(0, 80)}"`);
}
log('步骤 2:检测到手机号输入模式,正在切换到邮箱输入模式...');
await humanPause(350, 900);
simulateClick(snapshot.switchToEmailTrigger);
} else if (!snapshot.switchToEmailTrigger && !loggedMissingSwitchToEmail) {
loggedMissingSwitchToEmail = true;
log('步骤 2:检测到手机号输入模式,但暂未识别到“改用邮箱/继续使用电子邮件地址登录”按钮,继续等待界面稳定...', 'warn');
}
if (logDiagnostics && !slowSnapshotLogged && Date.now() - start >= 5000) {
slowSnapshotLogged = true;
log(`步骤 ${step}:等待手机号入口切换超过 5 秒,页面诊断快照:${JSON.stringify(getSignupEntryDiagnostics())}`, 'warn');
}
await sleep(250);
continue;
}
if (snapshot.state === 'entry_home') {
if (!autoOpenEntry) {
return snapshot;
@@ -701,7 +840,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,
@@ -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('Step 2: the signup dialog is still in phone entry mode and has not switched back to email entry. URL: https://chatgpt.com/'),
false
);
assert.equal(
loggingStatus.isAddPhoneAuthFailure('Step 8: verification submitted but the auth flow entered the phone number page. URL: https://auth.openai.com/add-phone'),
true
);
});
+16
View File
@@ -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.';
}
@@ -251,10 +259,18 @@ function getSignupEmailInput() {
return null;
}
function getSignupPhoneInput() {
return null;
}
function getSignupPasswordInput() {
return null;
}
function findSignupUseEmailTrigger() {
return null;
}
function getPageTextSnapshot() {
return 'ChatGPT 登录';
}
+431
View File
@@ -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');
});
+6 -5
View File
@@ -294,11 +294,12 @@
1. 解析本轮应使用的邮箱
2. 打开或复用注册页
3. 点击注册入口并提交邮箱
4. 以当前邮箱先写入一条“停止(流程尚未完成)”的记录占位
5. 等待邮箱提交后的真实落地页
6. 如果进入密码页,则继续执行 Step 3
7. 如果直接进入邮箱验证码页,则自动跳过 Step 3 并进入 Step 4
3. 如果注册弹窗默认停留在手机号输入模式,会先尝试点击 `继续使用电子邮件地址登录 / Continue using email address` 一类按钮切回邮箱输入模式
4. 在邮箱输入模式下提交邮箱;邮箱输入框识别同时兼容本地化占位与 `aria-label`
5. 以当前邮箱先写入一条“停止(流程尚未完成)”的记录占位
6. 等待邮箱提交后的真实落地页
7. 如果进入密码页,则继续执行 Step 3
8. 如果直接进入邮箱验证码页,则自动跳过 Step 3 并进入 Step 4
### Step 3
+3 -2
View File
@@ -45,7 +45,7 @@
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 `gmailBaseEmail``mail2925BaseEmail` 与当前 2925 账号选择,避免自动流程重置后丢失别名基邮箱与 2925 切号上下文。
- `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `apikey.qzz.io` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 iCloud 且 `icloudFetchMode = always_new` 时,会强制跳过未用别名复用并新建别名;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱。
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机
- `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;当前额外接入 2925 账号池的新增、导入、切换、登录、禁用与删除消息。
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。
@@ -81,7 +81,7 @@
- `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱自动登录态确认、收件轮询、按步骤会话隔离“已试验证码”、在每次重发验证码之间执行一轮最多 15 次的邮箱刷新轮询、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理;若页面出现“子邮箱已达上限邮箱”提示,会立即上报后台进入切号链路;当后台显式开启 `mail2925MatchTargetEmail` 时,会对邮件里显式出现的目标邮箱做弱匹配,避免 `receive` 模式误捞别人的验证码。
- `content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前还会在登录链路显式识别 `phone-verification` 页面,避免把手机验证码页误判成邮箱验证码页或普通未知页。
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 会在注册弹窗默认处于手机号输入模式时自动切回邮箱输入模式,并兼容本地化邮箱占位与 `aria-label`登录链路还会显式识别 `phone-verification` 页面,避免把手机验证码页误判成邮箱验证码页或普通未知页。
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;当前承接步骤 10。
- `content/utils.js`:内容脚本公共工具层,负责日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击。
- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做步骤 10 判定。
@@ -191,6 +191,7 @@
- `tests/sidepanel-mail2925-manager.test.js`:测试侧边栏 2925 管理器模块接线、共享号池表单脚本加载顺序、显隐交互与空态渲染。
- `tests/sidepanel-mail2925-mode.test.js`:测试侧边栏保留 `2925 provide / receive` 模式行与独立的号池配置行,并验证 sidepanel 只在 provide 模式下把 2925 视为别名邮箱 provider。
- `tests/signup-entry-diagnostics.test.js`:测试注册入口诊断快照输出。
- `tests/signup-step2-email-switch.test.js`:测试 Step 2 在手机号输入模式下切回邮箱输入模式,以及本地化邮箱输入框识别。
- `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。
- `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。
- `tests/step3-direct-complete.test.js`:测试步骤 3 在提交前先完成上报,并保留延迟提交回调的可执行性验证。