From 0d0cf4b2af990754b3255fe0e037a780cb996631 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Mon, 20 Apr 2026 01:05:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20OAuth=20=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E4=B8=AD=E7=9A=84=E6=97=A5=E5=BF=97=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=BC=BA=E5=AF=86=E7=A0=81=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?=E6=97=B6=E7=9A=84=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=9B=B8=E5=85=B3=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 8 +- content/signup-page.js | 12 +- tests/background-contribution-mode.test.js | 60 +++++++- tests/step6-passwordless-otp-login.test.js | 161 +++++++++++++++++++++ 4 files changed, 232 insertions(+), 9 deletions(-) create mode 100644 tests/step6-passwordless-otp-login.test.js diff --git a/background.js b/background.js index f48e539..76e20b9 100644 --- a/background.js +++ b/background.js @@ -6600,11 +6600,10 @@ async function runPreStep6CookieCleanup() { async function refreshOAuthUrlBeforeStep6(state) { if (state?.contributionModeExpected && !state?.contributionMode) { - throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA 面板。请重新进入贡献模式后再点击自动。'); + throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API 链路。请重新进入贡献模式后再点击自动。'); } if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) { - await addLog('步骤 7:contributionMode=true,正在通过公开贡献接口申请 OAuth 链接...', 'info'); - await addLog('步骤 7:贡献模式正在申请贡献登录地址...'); + await addLog('步骤 7:contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...', 'info'); const contributionState = await contributionOAuthManager.startContributionFlow({ nickname: state.email, openAuthTab: false, @@ -6617,8 +6616,7 @@ async function refreshOAuthUrlBeforeStep6(state) { await handleStepData(1, { oauthUrl }); return oauthUrl; } - await addLog(`步骤 7:正在刷新登录用的 ${getPanelModeLabel(state)} OAuth 链接...`); - await addLog(`步骤 7:contributionMode=${Boolean(state?.contributionMode)},当前将回退到 ${getPanelModeLabel(state)} 面板刷新 OAuth。`, 'warn'); + await addLog(`步骤 7:contributionMode=false,走普通 CPA / SUB2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info'); console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel'); const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: '步骤 7' }); await handleStepData(1, refreshResult); diff --git a/content/signup-page.js b/content/signup-page.js index de736d4..104138f 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -1913,10 +1913,18 @@ async function step6SwitchToOneTimeCodeLogin(snapshot) { async function step6LoginFromPasswordPage(payload, snapshot) { const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); + const hasPassword = Boolean(String(payload?.password || '').trim()); if (currentSnapshot.passwordInput) { - if (!payload.password) { - throw new Error('登录时缺少密码,步骤 7 无法继续。'); + if (!hasPassword) { + if (currentSnapshot.switchTrigger) { + log('步骤 7:当前未提供密码,改走一次性验证码登录。', 'warn'); + return step6SwitchToOneTimeCodeLogin(currentSnapshot); + } + + return createStep6RecoverableResult('missing_password_and_one_time_code_trigger', currentSnapshot, { + message: '登录时未提供密码,且当前页面没有可用的一次性验证码登录入口。', + }); } log('步骤 7:已进入密码页,准备填写密码...'); diff --git a/tests/background-contribution-mode.test.js b/tests/background-contribution-mode.test.js index 85054eb..aab3403 100644 --- a/tests/background-contribution-mode.test.js +++ b/tests/background-contribution-mode.test.js @@ -458,8 +458,7 @@ return { refreshOAuthUrlBeforeStep6 }; assert.equal(oauthUrl, 'https://auth.example.com/oauth?state=oauth-state-001'); assert.deepStrictEqual(calls, [ - { type: 'log', message: '步骤 7:contributionMode=true,正在通过公开贡献接口申请 OAuth 链接...' }, - { type: 'log', message: '步骤 7:贡献模式正在申请贡献登录地址...' }, + { type: 'log', message: '步骤 7:contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...' }, { type: 'contribution', options: { @@ -488,6 +487,63 @@ return { refreshOAuthUrlBeforeStep6 }; delete globalThis.LOG_PREFIX; }); +test('refreshOAuthUrlBeforeStep6 logs the normal CPA/SUB2API path explicitly when contributionMode=false', async () => { + const bundle = extractFunction(backgroundSource, 'refreshOAuthUrlBeforeStep6'); + const calls = []; + + const api = new Function(` +${bundle} +return { refreshOAuthUrlBeforeStep6 }; +`)(); + + globalThis.addLog = async (message) => { + calls.push({ type: 'log', message }); + }; + globalThis.contributionOAuthManager = { + async startContributionFlow() { + calls.push({ type: 'contribution' }); + return { + contributionAuthUrl: 'https://auth.example.com/oauth?state=unexpected', + }; + }, + }; + globalThis.handleStepData = async (step, payload) => { + calls.push({ type: 'step', step, payload }); + }; + globalThis.getPanelModeLabel = () => 'SUB2API'; + globalThis.requestOAuthUrlFromPanel = async () => { + calls.push({ type: 'panel' }); + return { oauthUrl: 'https://panel.example.com/oauth' }; + }; + globalThis.LOG_PREFIX = '[test]'; + + const oauthUrl = await api.refreshOAuthUrlBeforeStep6({ + contributionMode: false, + panelMode: 'sub2api', + email: 'user@example.com', + }); + + assert.equal(oauthUrl, 'https://panel.example.com/oauth'); + assert.deepStrictEqual(calls, [ + { type: 'log', message: '步骤 7:contributionMode=false,走普通 CPA / SUB2API 链路(当前面板:SUB2API),正在刷新 OAuth 登录地址...' }, + { type: 'panel' }, + { + type: 'step', + step: 1, + payload: { + oauthUrl: 'https://panel.example.com/oauth', + }, + }, + ]); + + delete globalThis.addLog; + delete globalThis.contributionOAuthManager; + delete globalThis.handleStepData; + delete globalThis.getPanelModeLabel; + delete globalThis.requestOAuthUrlFromPanel; + delete globalThis.LOG_PREFIX; +}); + test('executeStep10 blocks silent fallback when contributionModeExpected=true but contributionMode=false', async () => { const bundle = extractFunction(backgroundSource, 'executeStep10'); diff --git a/tests/step6-passwordless-otp-login.test.js b/tests/step6-passwordless-otp-login.test.js new file mode 100644 index 0000000..ee19863 --- /dev/null +++ b/tests/step6-passwordless-otp-login.test.js @@ -0,0 +1,161 @@ +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); +} + +const bundle = extractFunction('step6LoginFromPasswordPage'); + +function createApi() { + return new Function(` +${bundle} +return { step6LoginFromPasswordPage }; +`)(); +} + +function cleanupGlobals() { + delete globalThis.normalizeStep6Snapshot; + delete globalThis.inspectLoginAuthState; + delete globalThis.log; + delete globalThis.step6SwitchToOneTimeCodeLogin; + delete globalThis.createStep6RecoverableResult; + delete globalThis.fillInput; + delete globalThis.humanPause; + delete globalThis.sleep; + delete globalThis.triggerLoginSubmitAction; + delete globalThis.waitForStep6PasswordSubmitTransition; +} + +test('step6LoginFromPasswordPage switches to one-time-code login when password is missing but switch trigger exists', async () => { + const api = createApi(); + const logs = []; + const snapshot = { + state: 'password_page', + passwordInput: { id: 'password' }, + switchTrigger: { id: 'otp' }, + }; + + globalThis.normalizeStep6Snapshot = (value) => value; + globalThis.inspectLoginAuthState = () => snapshot; + globalThis.log = (message, level = 'info') => { + logs.push({ message, level }); + }; + globalThis.step6SwitchToOneTimeCodeLogin = async (value) => { + assert.strictEqual(value, snapshot); + return { step6Outcome: 'success', via: 'switch_to_one_time_code_login' }; + }; + globalThis.createStep6RecoverableResult = () => { + throw new Error('should not create recoverable result when switch trigger exists'); + }; + globalThis.fillInput = () => { + throw new Error('should not fill password when password is missing'); + }; + globalThis.humanPause = async () => {}; + globalThis.sleep = async () => {}; + globalThis.triggerLoginSubmitAction = async () => {}; + globalThis.waitForStep6PasswordSubmitTransition = async () => { + throw new Error('should not submit password when password is missing'); + }; + + try { + const result = await api.step6LoginFromPasswordPage({ email: 'user@example.com', password: '' }, snapshot); + + assert.deepStrictEqual(result, { step6Outcome: 'success', via: 'switch_to_one_time_code_login' }); + assert.deepStrictEqual(logs, [ + { message: '步骤 7:当前未提供密码,改走一次性验证码登录。', level: 'warn' }, + ]); + } finally { + cleanupGlobals(); + } +}); + +test('step6LoginFromPasswordPage returns a recoverable result when password is missing and no one-time-code trigger exists', async () => { + const api = createApi(); + const snapshot = { + state: 'password_page', + passwordInput: { id: 'password' }, + switchTrigger: null, + }; + + globalThis.normalizeStep6Snapshot = (value) => value; + globalThis.inspectLoginAuthState = () => snapshot; + globalThis.log = () => {}; + globalThis.step6SwitchToOneTimeCodeLogin = async () => { + throw new Error('should not switch without a one-time-code trigger'); + }; + globalThis.createStep6RecoverableResult = (reason, stateSnapshot, details) => ({ + step6Outcome: 'recoverable', + reason, + stateSnapshot, + ...details, + }); + globalThis.fillInput = () => { + throw new Error('should not fill password when password is missing'); + }; + globalThis.humanPause = async () => {}; + globalThis.sleep = async () => {}; + globalThis.triggerLoginSubmitAction = async () => {}; + globalThis.waitForStep6PasswordSubmitTransition = async () => { + throw new Error('should not submit password when password is missing'); + }; + + try { + const result = await api.step6LoginFromPasswordPage({ email: 'user@example.com', password: '' }, snapshot); + + assert.deepStrictEqual(result, { + step6Outcome: 'recoverable', + reason: 'missing_password_and_one_time_code_trigger', + stateSnapshot: snapshot, + message: '登录时未提供密码,且当前页面没有可用的一次性验证码登录入口。', + }); + } finally { + cleanupGlobals(); + } +});