From 6160f4640d7f0a4bc35375add56203114c8e1a3a Mon Sep 17 00:00:00 2001 From: QLHazyCoder Date: Mon, 4 May 2026 17:09:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E5=8F=B7=E8=BE=93=E5=85=A5=E6=A1=86=E5=8F=8A=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 28 ++++ background/message-router.js | 48 ++++++ sidepanel/sidepanel.html | 6 + sidepanel/sidepanel.js | 142 ++++++++++++++++++ ...ckground-message-router-step2-skip.test.js | 42 +++++- ...epanel-phone-verification-settings.test.js | 10 ++ 项目完整链路说明.md | 7 + 7 files changed, 282 insertions(+), 1 deletion(-) diff --git a/background.js b/background.js index 2311da4..6bbac06 100644 --- a/background.js +++ b/background.js @@ -2550,6 +2550,32 @@ async function setEmailState(email) { } } +async function setSignupPhoneStateSilently(phoneNumber) { + const normalizedPhoneNumber = String(phoneNumber || '').trim(); + const currentState = await getState(); + const updates = { + signupPhoneNumber: normalizedPhoneNumber, + }; + + if (normalizedPhoneNumber) { + updates.accountIdentifierType = 'phone'; + updates.accountIdentifier = normalizedPhoneNumber; + } else if (String(currentState?.accountIdentifierType || '').trim().toLowerCase() === 'phone') { + updates.accountIdentifierType = null; + updates.accountIdentifier = ''; + } + + await setState(updates); + broadcastDataUpdate(updates); +} + +async function setSignupPhoneState(phoneNumber) { + await setSignupPhoneStateSilently(phoneNumber); + if (String(phoneNumber || '').trim()) { + await appendManualAccountRunRecordIfNeeded('step2_stopped', null, '步骤 2 已使用手机号,流程尚未完成。'); + } +} + async function setPasswordState(password) { await setState({ password }); broadcastDataUpdate({ password }); @@ -10296,6 +10322,8 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter setContributionMode, setEmailState, setEmailStateSilently, + setSignupPhoneState, + setSignupPhoneStateSilently, setIcloudAliasPreservedState, setIcloudAliasUsedState, setLuckmailPurchaseDisabledState, diff --git a/background/message-router.js b/background/message-router.js index 9f5772e..a224656 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -102,6 +102,8 @@ setContributionMode, setEmailState, setEmailStateSilently, + setSignupPhoneState, + setSignupPhoneStateSilently, setIcloudAliasPreservedState, setIcloudAliasUsedState, setLuckmailPurchaseDisabledState, @@ -162,6 +164,20 @@ return ''; } + function resolveSignupPhonePayload(payload = {}) { + const directPhone = String( + payload?.signupPhoneNumber + || payload?.phoneNumber + || '' + ).trim(); + if (directPhone) { + return directPhone; + } + return String(payload?.accountIdentifierType || '').trim().toLowerCase() === 'phone' + ? String(payload?.accountIdentifier || '').trim() + : ''; + } + function isStepProtectedFromAutoSkip(status) { return status === 'running' || status === 'completed' @@ -337,6 +353,12 @@ if (payload.email) { await setEmailState(payload.email); } + { + const signupPhoneNumber = resolveSignupPhonePayload(payload); + if (signupPhoneNumber) { + await setSignupPhoneStateSilently(signupPhoneNumber); + } + } if (payload.skipRegistrationFlow) { const latestState = await getState(); for (const skipStep of [3, 4, 5]) { @@ -361,6 +383,12 @@ break; case 3: if (payload.email) await setEmailState(payload.email); + { + const signupPhoneNumber = resolveSignupPhonePayload(payload); + if (signupPhoneNumber) { + await setSignupPhoneStateSilently(signupPhoneNumber); + } + } if (payload.signupVerificationRequestedAt) { await setState({ signupVerificationRequestedAt: payload.signupVerificationRequestedAt }); } @@ -1168,6 +1196,26 @@ return { ok: true, email: message.payload.email }; } + case 'SET_SIGNUP_PHONE_STATE': { + const state = await getState(); + if (isAutoRunLockedState(state)) { + throw new Error('自动流程运行中,当前不能手动修改注册手机号。'); + } + const phoneNumber = resolveSignupPhonePayload(message.payload) || null; + await setSignupPhoneStateSilently(phoneNumber); + return { ok: true, phoneNumber }; + } + + case 'SAVE_SIGNUP_PHONE': { + const state = await getState(); + if (isAutoRunLockedState(state)) { + throw new Error('自动流程运行中,当前不能手动修改注册手机号。'); + } + const phoneNumber = resolveSignupPhonePayload(message.payload) || null; + await setSignupPhoneState(phoneNumber); + return { ok: true, phoneNumber }; + } + case 'FETCH_GENERATED_EMAIL': { clearStopRequest(); const state = await getState(); diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index ae45e57..154da40 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -439,6 +439,12 @@ +
延迟
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 2cae19e..377e499 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -50,6 +50,7 @@ const displayLocalhostUrl = document.getElementById('display-localhost-url'); const displayStatus = document.getElementById('display-status'); const statusBar = document.getElementById('status-bar'); const inputEmail = document.getElementById('input-email'); +const inputSignupPhone = document.getElementById('input-signup-phone'); const inputPassword = document.getElementById('input-password'); const btnToggleVpsUrl = document.getElementById('btn-toggle-vps-url'); const btnToggleVpsPassword = document.getElementById('btn-toggle-vps-password'); @@ -333,6 +334,7 @@ const btnTogglePhoneVerificationSection = document.getElementById('btn-toggle-ph const rowPhoneVerificationFold = document.getElementById('row-phone-verification-fold'); const inputPhoneVerificationEnabled = document.getElementById('input-phone-verification-enabled'); const rowSignupMethod = document.getElementById('row-signup-method'); +const rowSignupPhone = document.getElementById('row-signup-phone'); const signupMethodButtons = Array.from(document.querySelectorAll('[data-signup-method]')); const selectPhoneSmsProvider = document.getElementById('select-phone-sms-provider'); const rowHeroSmsPlatform = document.getElementById('row-hero-sms-platform'); @@ -6691,6 +6693,9 @@ function isSignupMethodSwitchLocked() { function updateSignupMethodUI(options = {}) { if (!signupMethodButtons.length) { + if (typeof syncSignupPhoneInputFromState === 'function') { + syncSignupPhoneInputFromState(latestState); + } return; } @@ -6739,6 +6744,9 @@ function updateSignupMethodUI(options = {}) { signupMethod: selectedMethod, } ); + if (typeof syncSignupPhoneInputFromState === 'function') { + syncSignupPhoneInputFromState(latestState); + } } function updatePhoneVerificationSettingsUI() { @@ -6922,6 +6930,48 @@ async function setRuntimeEmailState(email) { return normalizedEmail; } +function getRuntimeSignupPhoneValue(state = latestState) { + const identifierType = String(state?.accountIdentifierType || '').trim().toLowerCase(); + return String( + state?.signupPhoneNumber + || (identifierType === 'phone' ? state?.accountIdentifier : '') + || '' + ).trim(); +} + +function syncSignupPhoneInputFromState(state = latestState) { + const signupPhone = getRuntimeSignupPhoneValue(state); + if (typeof inputSignupPhone !== 'undefined' && inputSignupPhone) { + inputSignupPhone.value = signupPhone; + } + if (typeof rowSignupPhone !== 'undefined' && rowSignupPhone) { + const rawSignupMethod = state?.signupMethod || ( + typeof getSelectedSignupMethod === 'function' + ? getSelectedSignupMethod() + : 'email' + ); + const selectedMethod = typeof normalizeSignupMethod === 'function' + ? normalizeSignupMethod(rawSignupMethod) + : (String(rawSignupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email'); + rowSignupPhone.style.display = (selectedMethod === 'phone' || Boolean(signupPhone)) ? '' : 'none'; + } +} + +async function setRuntimeSignupPhoneState(phoneNumber) { + const normalizedPhone = String(phoneNumber || '').trim() || null; + const response = await chrome.runtime.sendMessage({ + type: 'SET_SIGNUP_PHONE_STATE', + source: 'sidepanel', + payload: { phoneNumber: normalizedPhone }, + }); + + if (response?.error) { + throw new Error(response.error); + } + + return normalizedPhone; +} + async function openPlusManualConfirmationDialog(options = {}) { const method = String(options.method || '').trim().toLowerCase(); const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay'; @@ -7032,6 +7082,40 @@ async function clearRegistrationEmail(options = {}) { } } +async function clearRegistrationSignupPhone(options = {}) { + const { silent = false } = options; + if (!getRuntimeSignupPhoneValue(latestState)) { + if (typeof inputSignupPhone !== 'undefined' && inputSignupPhone) { + inputSignupPhone.value = ''; + } + syncSignupPhoneInputFromState(latestState); + return; + } + + if (typeof inputSignupPhone !== 'undefined' && inputSignupPhone) { + inputSignupPhone.value = ''; + } + syncLatestState({ + signupPhoneNumber: '', + ...(String(latestState?.accountIdentifierType || '').trim().toLowerCase() === 'phone' + ? { + accountIdentifierType: null, + accountIdentifier: '', + } + : {}), + }); + syncSignupPhoneInputFromState(latestState); + + try { + await setRuntimeSignupPhoneState(null); + } catch (err) { + if (!silent) { + showToast(`清空注册手机号失败:${err.message}`, 'error'); + } + throw err; + } +} + function markSettingsDirty(isDirty = true) { settingsDirty = isDirty; if (isDirty) { @@ -7129,6 +7213,9 @@ function applyAutoRunStatus(payload = currentAutoRun) { || isCustomMailProvider() || usesCustomEmailPoolGenerator(); inputEmail.disabled = locked; + if (typeof inputSignupPhone !== 'undefined' && inputSignupPhone) { + inputSignupPhone.disabled = locked; + } inputAutoSkipFailures.disabled = scheduled; const lockedRunCount = typeof getLockedRunCountFromEmailPool === 'function' @@ -7339,6 +7426,9 @@ function applySettingsState(state) { renderStepStatuses(latestState); inputEmail.value = state?.email || ''; + if (typeof syncSignupPhoneInputFromState === 'function') { + syncSignupPhoneInputFromState(state); + } syncPasswordField(state || {}); if (typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled) { inputPlusModeEnabled.checked = Boolean(state?.plusModeEnabled); @@ -10410,6 +10500,9 @@ btnReset.addEventListener('click', async () => { currentLuckmailPurchase: null, currentLuckmailMailCursor: null, email: null, + signupPhoneNumber: '', + accountIdentifierType: null, + accountIdentifier: '', }); syncAutoRunState({ autoRunning: false, @@ -10427,6 +10520,12 @@ btnReset.addEventListener('click', async () => { displayLocalhostUrl.textContent = '等待中...'; displayLocalhostUrl.classList.remove('has-value'); inputEmail.value = ''; + if (typeof inputSignupPhone !== 'undefined' && inputSignupPhone) { + inputSignupPhone.value = ''; + } + if (typeof syncSignupPhoneInputFromState === 'function') { + syncSignupPhoneInputFromState(latestState); + } displayStatus.textContent = '就绪'; statusBar.className = 'status-bar'; logArea.innerHTML = ''; @@ -10475,6 +10574,29 @@ inputEmail.addEventListener('change', async () => { } }); inputEmail.addEventListener('input', updateButtonStates); +if (typeof inputSignupPhone !== 'undefined' && inputSignupPhone) { + inputSignupPhone.addEventListener('change', async () => { + const phoneNumber = inputSignupPhone.value.trim(); + inputSignupPhone.value = phoneNumber; + try { + if (phoneNumber) { + const response = await chrome.runtime.sendMessage({ + type: 'SAVE_SIGNUP_PHONE', + source: 'sidepanel', + payload: { phoneNumber }, + }); + if (response?.error) { + throw new Error(response.error); + } + } else { + await clearRegistrationSignupPhone(); + } + } catch (err) { + showToast(err.message, 'error'); + } + }); + inputSignupPhone.addEventListener('input', updateButtonStates); +} inputVpsUrl.addEventListener('input', () => { markSettingsDirty(true); scheduleSettingsAutoSave(); @@ -12089,6 +12211,17 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { displayLocalhostUrl.textContent = '等待中...'; displayLocalhostUrl.classList.remove('has-value'); inputEmail.value = ''; + if (typeof inputSignupPhone !== 'undefined' && inputSignupPhone) { + inputSignupPhone.value = ''; + } + syncLatestState({ + signupPhoneNumber: '', + accountIdentifierType: null, + accountIdentifier: '', + }); + if (typeof syncSignupPhoneInputFromState === 'function') { + syncSignupPhoneInputFromState(latestState); + } displayStatus.textContent = '就绪'; statusBar.className = 'status-bar'; logArea.innerHTML = ''; @@ -12126,6 +12259,15 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { inputEmail.value = message.payload.email || ''; queueCustomEmailPoolRefresh(); } + if ( + message.payload.signupPhoneNumber !== undefined + || message.payload.accountIdentifierType !== undefined + || message.payload.accountIdentifier !== undefined + ) { + if (typeof syncSignupPhoneInputFromState === 'function') { + syncSignupPhoneInputFromState(latestState); + } + } if ( message.payload.password !== undefined || message.payload.customPassword !== undefined diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js index 7392a0b..5e3dd77 100644 --- a/tests/background-message-router-step2-skip.test.js +++ b/tests/background-message-router-step2-skip.test.js @@ -11,6 +11,8 @@ function createRouter(overrides = {}) { logs: [], stepStatuses: [], emailStates: [], + signupPhoneStates: [], + signupPhoneSilentStates: [], finalizePayloads: [], phoneFinalizations: [], notifyCompletions: [], @@ -109,6 +111,12 @@ function createRouter(overrides = {}) { events.emailStates.push(email); }, setEmailStateSilently: async () => {}, + setSignupPhoneState: async (phoneNumber) => { + events.signupPhoneStates.push(phoneNumber); + }, + setSignupPhoneStateSilently: async (phoneNumber) => { + events.signupPhoneSilentStates.push(phoneNumber); + }, setIcloudAliasPreservedState: async () => {}, setIcloudAliasUsedState: async () => {}, setLuckmailPurchaseDisabledState: async () => {}, @@ -146,6 +154,22 @@ test('message router skips step 3 when step 2 lands on verification page', async assert.equal(events.logs[0]?.message, '步骤 2:提交邮箱后页面直接进入验证码页,已自动跳过步骤 3。'); }); +test('message router syncs signup phone runtime state from step 2 payload immediately', async () => { + const { router, events } = createRouter({ + state: { stepStatuses: { 3: 'pending' } }, + }); + + await router.handleStepData(2, { + accountIdentifierType: 'phone', + accountIdentifier: '66959916439', + signupPhoneNumber: '66959916439', + }); + + assert.deepStrictEqual(events.emailStates, []); + assert.deepStrictEqual(events.signupPhoneSilentStates, ['66959916439']); + assert.deepStrictEqual(events.signupPhoneStates, []); +}); + test('message router does not overwrite a completed step 3 when step 2 is replayed', async () => { const { router, events } = createRouter({ state: { stepStatuses: { 3: 'completed' } }, @@ -264,7 +288,23 @@ test('message router finalizes step 3 before marking it completed', async () => }, }, ]); - assert.deepStrictEqual(response, { ok: true }); +assert.deepStrictEqual(response, { ok: true }); +}); + +test('message router saves runtime signup phone from sidepanel message', async () => { + const { router, events } = createRouter(); + + const response = await router.handleMessage({ + type: 'SAVE_SIGNUP_PHONE', + source: 'sidepanel', + payload: { + phoneNumber: '66959916439', + }, + }, {}); + + assert.deepStrictEqual(events.signupPhoneStates, ['66959916439']); + assert.deepStrictEqual(events.signupPhoneSilentStates, []); + assert.deepStrictEqual(response, { ok: true, phoneNumber: '66959916439' }); }); test('message router finalizes pending phone activation on platform verify success', async () => { diff --git a/tests/sidepanel-phone-verification-settings.test.js b/tests/sidepanel-phone-verification-settings.test.js index 1528cb4..609973b 100644 --- a/tests/sidepanel-phone-verification-settings.test.js +++ b/tests/sidepanel-phone-verification-settings.test.js @@ -60,6 +60,8 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro assert.match(html, /id="row-phone-verification-fold"/); assert.match(html, /id="input-phone-verification-enabled"/); assert.match(html, /id="row-signup-method"/); + assert.match(html, /id="row-signup-phone"/); + assert.match(html, /id="input-signup-phone"/); assert.match(html, /data-signup-method="email"/); assert.match(html, /data-signup-method="phone"/); assert.match(html, /id="row-phone-sms-provider"/); @@ -120,6 +122,14 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/); }); +test('sidepanel source wires runtime signup phone field to background sync messages', () => { + assert.match(sidepanelSource, /function getRuntimeSignupPhoneValue\(state = latestState\)/); + assert.match(sidepanelSource, /function syncSignupPhoneInputFromState\(state = latestState\)/); + assert.match(sidepanelSource, /type:\s*'SET_SIGNUP_PHONE_STATE'/); + assert.match(sidepanelSource, /type:\s*'SAVE_SIGNUP_PHONE'/); + assert.match(sidepanelSource, /message\.payload\.signupPhoneNumber !== undefined/); +}); + test('hero sms country helpers keep empty summary state and expose removable order handling', () => { assert.match( sidepanelSource, diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 5f01a26..563227c 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -44,6 +44,7 @@ - 在日志区通过“记录”按钮打开独立的账号记录覆盖层,并展示成功/失败/停止/重试统计与分页列表;phone-only 记录会回退展示手机号 - 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Ultra`、历史 `Pro` 与 legacy `v` 三个版本族,排序时固定以 `Ultra` 为最高正式系列,同时会在读取缓存后重新排序,避免历史 `Pro` 或 `v` 版本误显示为比 `Ultra` 更新 - 展示一个单独的“接码”开关、注册方式 `signupMethod` 与“接码平台”下拉;接码平台当前支持 HeroSMS / 5sim / NexSMS。普通模式下开启接码后可把注册方式切到手机号注册,并在 OAuth 登录链路命中手机号登录验证码页时继续复用同一手机号续跑短信验证 +- 侧栏额外提供一个运行态“注册手机号”输入框:第 2 步自动拿到号码后立即回填;用户手动接管手机号注册时也可以直接改写这一个运行态槽位,后续第 4 步和命中手机号验证码的登录链路统一从这份运行态手机号继续同步身份与展示 - 展示 `Plus 模式` 开关与 Plus 支付方式配置;支付方式支持 PayPal / GoPay,PayPal 展示账号池下拉与添加按钮,GoPay 展示手机号和 PIN;步骤列表切换为 Plus 模式 13 步定义,普通模式的 Cookie 清理步骤不再显示或执行,登录验证码步骤会移动到 Plus 可见第 11 步 - 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作 @@ -149,6 +150,12 @@ - 当前统一账号标识 `accountIdentifierType / accountIdentifier` - 当前邮箱 / 密码 - 当前手机号注册运行态 `signupPhoneNumber / signupPhoneActivation / signupPhoneCompletedActivation` +- 侧栏“注册手机号”输入框只同步运行态 `signupPhoneNumber / accountIdentifierType / accountIdentifier`,不属于持久配置,也不参与配置导入导出 +- 本轮联动自检重点: + 1. Step 2 一旦自动取到手机号,必须立刻广播并回填侧栏运行态手机号。 + 2. sidepanel 重新加载后,必须从当前 state 恢复“注册手机号”文本框,而不是只恢复邮箱。 + 3. 用户手动修改“注册手机号”时,只能改运行态身份槽位,不能误写入持久配置或配置导入导出。 + 4. 第 4 步继续依赖当前手机号注册 activation 获取验证码,但显示与身份同步统一以侧栏运行态手机号为准,避免 UI 空白和后台状态脱节。 - 第 8 步固定的验证码页显示邮箱 `step8VerificationTargetEmail` - 当前手机号验证激活记录 `currentPhoneActivation` - 可复用的手机号验证激活记录 `reusablePhoneActivation`