From 6b3917d4b759ee272bc12fafacbd7f7a92bf7cb6 Mon Sep 17 00:00:00 2001 From: QLHazyCoder Date: Mon, 4 May 2026 22:49:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E5=8F=B7=E7=99=BB=E5=BD=95=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=89=8B=E6=9C=BA=E5=8F=B7=E8=BE=93=E5=85=A5=E6=A1=86?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E9=80=BB=E8=BE=91=E5=8F=8A=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/signup-page.js | 70 ++++++++++++++++++++++++++- tests/step7-phone-login-entry.test.js | 66 +++++++++++++++++++++++++ 项目完整链路说明.md | 2 +- 项目文件结构说明.md | 2 +- 4 files changed, 136 insertions(+), 4 deletions(-) diff --git a/content/signup-page.js b/content/signup-page.js index 124d45f..8a08a0f 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -1822,6 +1822,68 @@ function toE164PhoneNumber(value, dialCode) { return `+${normalizedDialCode}${digits}`; } +function getPhoneInputRenderedValue(phoneInput) { + return String(phoneInput?.value ?? phoneInput?.getAttribute?.('value') ?? '').trim(); +} + +function isPhoneInputValueComplete(phoneInput, phoneNumber, dialCode, expectedLocalNumber = '') { + const renderedDigits = normalizePhoneDigits(getPhoneInputRenderedValue(phoneInput)); + const targetDigits = normalizePhoneDigits(phoneNumber); + const localDigits = normalizePhoneDigits(expectedLocalNumber || toNationalPhoneNumber(phoneNumber, dialCode)); + const normalizedDialCode = normalizePhoneDigits(dialCode); + if (!renderedDigits) { + return false; + } + if (normalizedDialCode && renderedDigits === normalizedDialCode) { + return false; + } + if (localDigits && renderedDigits === localDigits) { + return true; + } + if (localDigits && renderedDigits.endsWith(localDigits) && renderedDigits.length > localDigits.length) { + return true; + } + return Boolean(targetDigits && renderedDigits === targetDigits); +} + +function getLoginPhoneFillCandidates(phoneNumber, dialCode) { + const candidates = [ + toNationalPhoneNumber(phoneNumber, dialCode), + toE164PhoneNumber(phoneNumber, dialCode), + normalizePhoneDigits(phoneNumber), + ]; + return candidates.filter((value, index, list) => value && list.indexOf(value) === index); +} + +async function fillLoginPhoneInputAndConfirm(phoneInput, options = {}) { + const { + phoneNumber = '', + dialCode = '', + visibleStep = 7, + } = options; + const localNumber = toNationalPhoneNumber(phoneNumber, dialCode); + if (!localNumber) { + throw new Error(`步骤 ${visibleStep}:手机号为空,无法填写。`); + } + + let lastRenderedValue = ''; + for (const candidate of getLoginPhoneFillCandidates(phoneNumber, dialCode)) { + fillInput(phoneInput, candidate); + await sleep(350); + lastRenderedValue = getPhoneInputRenderedValue(phoneInput); + if (isPhoneInputValueComplete(phoneInput, phoneNumber, dialCode, localNumber)) { + return { + inputValue: localNumber, + attemptedValue: candidate, + renderedValue: lastRenderedValue, + }; + } + } + + const displayedValue = lastRenderedValue || '空'; + throw new Error(`步骤 ${visibleStep}:手机号填写后页面显示为 ${displayedValue},未包含本地号码 ${localNumber},已停止提交以避免空号提交。`); +} + function resolveSignupPhoneDialCode(phoneInput, options = {}) { const { phoneNumber = '', countryLabel = '' } = options; const displayedDialCode = getSignupPhoneDisplayedDialCode(phoneInput); @@ -4588,8 +4650,12 @@ async function step6LoginFromPhonePage(payload, snapshot) { log(`步骤 ${visibleStep}:正在填写手机号 ${phoneNumber}...`, 'info', { step: visibleStep, stepKey: 'oauth-login' }); await humanPause(500, 1400); - fillInput(phoneInput, inputValue); - log(`步骤 ${visibleStep}:手机号已填写${dialCode ? `(区号 +${dialCode})` : ''}。`, 'info', { step: visibleStep, stepKey: 'oauth-login' }); + const fillResult = await fillLoginPhoneInputAndConfirm(phoneInput, { + phoneNumber, + dialCode, + visibleStep, + }); + log(`步骤 ${visibleStep}:手机号已填写${dialCode ? `(区号 +${dialCode},本地号 ${fillResult.inputValue})` : ''}。`, 'info', { step: visibleStep, stepKey: 'oauth-login' }); await sleep(500); const phoneSubmittedAt = Date.now(); diff --git a/tests/step7-phone-login-entry.test.js b/tests/step7-phone-login-entry.test.js index ccfa88d..b402b16 100644 --- a/tests/step7-phone-login-entry.test.js +++ b/tests/step7-phone-login-entry.test.js @@ -311,3 +311,69 @@ return { assert.equal(api.getVisibleCountryText(), '\u82f1\u56fd +(44)'); assert.deepEqual(api.getClicks(), ['\u6fb3\u5927\u5229\u4e9a (+61)', '\u82f1\u56fd +(44)']); }); + +function createPhoneFillApi(fillBehavior) { + return new Function('fillBehavior', ` +const fills = []; +const phoneInput = { + value: '+44', + getAttribute(name) { + return name === 'value' ? this.value : ''; + }, +}; + +function fillInput(input, value) { + fills.push(value); + fillBehavior(input, value); +} + +async function sleep() {} + +${extractFunction('normalizePhoneDigits')} +${extractFunction('toNationalPhoneNumber')} +${extractFunction('toE164PhoneNumber')} +${extractFunction('getPhoneInputRenderedValue')} +${extractFunction('isPhoneInputValueComplete')} +${extractFunction('getLoginPhoneFillCandidates')} +${extractFunction('fillLoginPhoneInputAndConfirm')} + +return { + run() { + return fillLoginPhoneInputAndConfirm(phoneInput, { + phoneNumber: '447780579093', + dialCode: '44', + visibleStep: 7, + }); + }, + getFills() { + return fills.slice(); + }, + getValue() { + return phoneInput.value; + }, +}; + `)(fillBehavior); +} + +test('step 7 retries phone fill with e164 when the auth input keeps only the dial code', async () => { + const api = createPhoneFillApi((input, value) => { + input.value = value === '7780579093' ? '+44' : value; + }); + + const result = await api.run(); + + assert.equal(result.inputValue, '7780579093'); + assert.equal(result.attemptedValue, '+447780579093'); + assert.equal(api.getValue(), '+447780579093'); + assert.deepEqual(api.getFills(), ['7780579093', '+447780579093']); +}); + +test('step 7 stops before submit when phone fill never includes the local number', async () => { + const api = createPhoneFillApi((input) => { + input.value = '+44'; + }); + + await assert.rejects(api.run, /7780579093/); + assert.equal(api.getValue(), '+44'); + assert.deepEqual(api.getFills(), ['7780579093', '+447780579093', '447780579093']); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 9d8d90e..168c9bb 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -436,7 +436,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功 2. 打开最新 OAuth 链接 3. 根据统一账号标识选择登录身份: - 邮箱账号:沿用现有邮箱登录链路 - - 手机号账号:优先探测登录页是否存在手机号登录入口,必要时展开更多选项或从邮箱页切到手机号页;进入手机号输入页后会复用 Step 2 的可视国家下拉框同步逻辑,按本轮手机号最长区号切到目标国家,避免英国 `+44` 号码仍按默认澳大利亚 `+61` 提交 + - 手机号账号:优先探测登录页是否存在手机号登录入口,必要时展开更多选项或从邮箱页切到手机号页;进入手机号输入页后会复用 Step 2 的可视国家下拉框同步逻辑,按本轮手机号最长区号切到目标国家,填写后还会读回输入框确认已包含本地号码,避免英国 `+44` 号码仍按默认澳大利亚 `+61` 提交或只提交区号 4. 登录;如果进入密码页且当前有密码,则填写并提交密码;如果当前没有密码但检测到一次性验证码入口,则直接切换到一次性验证码登录 5. 确保真正进入验证码页;手机号登录允许以 `phone-verification` 作为 Step 7 的成功落点 6. 如果页面没有可用的手机号登录入口,则 Step 7 直接抛出明确业务错误,不再误进入 Step 8 邮箱轮询 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 20a1834..a3fb19d 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -248,7 +248,7 @@ - `tests/step5-direct-complete.test.js`:测试步骤 5 在资料页点击提交后立即完成当前步骤。 - `tests/step6-login-state.test.js`:测试 OAuth 登录状态识别逻辑,当前对应步骤 7 链路,并覆盖 `phone-verification` 页面不会被误判成邮箱验证码页。 - `tests/step6-timeout-recovery.test.js`:测试 OAuth 登录超时报错页的可恢复结果构造,当前对应步骤 7 链路。 -- `tests/step7-phone-login-entry.test.js`:测试 OAuth 手机号登录入口识别、真实 add-phone 与手机号登录页区分、慢切换等待窗口,以及手机号登录页可视国家下拉框会按本轮号码区号自动切换。 +- `tests/step7-phone-login-entry.test.js`:测试 OAuth 手机号登录入口识别、真实 add-phone 与手机号登录页区分、慢切换等待窗口、手机号登录页可视国家下拉框会按本轮号码区号自动切换,以及输入框只剩区号时不会继续提交。 - `tests/step8-callback-handling.test.js`:测试 localhost 回调地址捕获逻辑,当前对应步骤 9。 - `tests/step8-debugger-stop.test.js`:测试调试器点击在 Stop 场景下的中止行为,当前对应步骤 9。 - `tests/step8-retry-page-recovery.test.js`:测试 OAuth 同意页点击后的重试页恢复分支,以及手机号验证完成后会继续回到 OAuth 同意页等待,当前对应步骤 9。