From ce0fe101a3acab51a160d69ed14078ff424cfbc1 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Fri, 17 Apr 2026 03:39:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=AD=A5=E9=AA=A4=20?= =?UTF-8?q?5=20=E6=8F=90=E4=BA=A4=E9=80=BB=E8=BE=91=EF=BC=8C=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E5=90=8E=E7=AB=8B=E5=8D=B3=E4=B8=8A=E6=8A=A5=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E7=8A=B6=E6=80=81=EF=BC=8C=E9=81=BF=E5=85=8D=E7=AD=89?= =?UTF-8?q?=E5=BE=85=E9=A1=B5=E9=9D=A2=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + background.js | 1 + background/steps/fill-profile.js | 14 ++ content/signup-page.js | 92 ++------ ...kground-step5-submit-short-circuit.test.js | 52 +++++ tests/step5-direct-complete.test.js | 217 ++++++++++++++++++ 项目完整链路说明.md | 5 +- 7 files changed, 307 insertions(+), 75 deletions(-) create mode 100644 tests/background-step5-submit-short-circuit.test.js create mode 100644 tests/step5-direct-complete.test.js diff --git a/README.md b/README.md index 5fac506..d3fc1ba 100644 --- a/README.md +++ b/README.md @@ -485,6 +485,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 - 页面要求 `age` 如果页面是生日模式,会填写年月日;如果页面上存在 `input[name='age']`,则直接填写年龄。 +点击 `完成帐户创建` / `继续` 后,Step 5 会立刻记为完成,不再等待页面跳转结果;只有在提交瞬间发生页面跳转、且后台还没收到完成信号时,才会兜底检查是否已跳到 ChatGPT。 ### Step 6: Login via OAuth diff --git a/background.js b/background.js index b3f3a7e..1d15a77 100644 --- a/background.js +++ b/background.js @@ -5269,6 +5269,7 @@ const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({ addLog, generateRandomBirthday, generateRandomName, + getState, getTabId, handleChatgptOnboardingSkip, isRetryableContentScriptTransportError, diff --git a/background/steps/fill-profile.js b/background/steps/fill-profile.js index 53699e0..c1557f0 100644 --- a/background/steps/fill-profile.js +++ b/background/steps/fill-profile.js @@ -6,6 +6,7 @@ addLog, generateRandomBirthday, generateRandomName, + getState, getTabId, handleChatgptOnboardingSkip, isRetryableContentScriptTransportError, @@ -14,6 +15,14 @@ waitForStep5ChatgptRedirect, } = deps; + async function isStepAlreadyCompleted(step) { + if (typeof getState !== 'function') { + return false; + } + const latestState = await getState().catch(() => null); + return latestState?.stepStatuses?.[step] === 'completed'; + } + async function executeStep5() { const { firstName, lastName } = generateRandomName(); const { year, month, day } = generateRandomBirthday(); @@ -39,6 +48,11 @@ } } + if (step5TransportError && await isStepAlreadyCompleted(5)) { + await addLog('步骤 5:提交后的页面跳转打断了响应,但已收到完成信号,直接结束当前步骤。', 'info'); + return; + } + if (step5Result?.chatgptOnboarding) { await addLog('步骤 5:检测到 ChatGPT 引导页跳转,正在处理引导页跳过...'); await handleChatgptOnboardingSkip(); diff --git a/content/signup-page.js b/content/signup-page.js index 56ace36..e5d0fd3 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -844,47 +844,6 @@ async function waitForChatgptPostSignupState(timeout = 15000) { return null; } -async function waitForStep5SubmitOutcome(timeout = 15000) { - const start = Date.now(); - - while (Date.now() - start < timeout) { - throwIfStopped(); - - const errorText = getStep5ErrorText(); - if (errorText) { - return { invalidProfile: true, errorText }; - } - - if (isAddPhonePageReady()) { - return { success: true, addPhonePage: true }; - } - - if (isChatgptOnboardingPage()) { - return { success: true, chatgptOnboarding: true }; - } - - if (isChatgptAuthenticatedHomePage()) { - return { success: true, chatgptHome: true }; - } - - if (isStep8Ready()) { - return { success: true }; - } - - await sleep(150); - } - - const errorText = getStep5ErrorText(); - if (errorText) { - return { invalidProfile: true, errorText }; - } - - return { - invalidProfile: true, - errorText: '提交后未进入下一阶段,请检查生日是否真正被页面接受。', - }; -} - function isSignupPasswordPage() { return /\/create-account\/password(?:[/?#]|$)/i.test(location.pathname || ''); } @@ -2004,6 +1963,17 @@ function getSerializableRect(el) { // Step 5: Fill Name & Birthday / Age // ============================================================ +function getStep5DirectCompletionPayload({ isAgeMode = false } = {}) { + const payload = { + skippedPostSubmitCheck: true, + directProceedToStep6: true, + }; + if (isAgeMode) { + payload.ageMode = true; + } + return payload; +} + async function step5_fillNameBirthday(payload) { const { firstName, lastName, age, year, month, day } = payload; if (!firstName || !lastName) throw new Error('未提供姓名数据。'); @@ -2220,46 +2190,22 @@ async function step5_fillNameBirthday(payload) { const isAgeMode = !birthdayMode && Boolean(ageInput); if (isAgeMode) { - log('步骤 5:当前为年龄输入模式,点击“继续”后将直接视为完成并进入步骤 6。', 'warn'); - reportComplete(5, { - skippedPostSubmitCheck: true, - directProceedToStep6: true, - }); + log('步骤 5:当前为年龄输入模式,点击“继续”后将直接完成当前步骤。', 'warn'); } await humanPause(500, 1300); simulateClick(completeBtn); + const completionPayload = getStep5DirectCompletionPayload({ isAgeMode }); + reportComplete(5, completionPayload); + if (isAgeMode) { - log('步骤 5:年龄模式已点击“继续”,已跳过后续结果等待。', 'warn'); - return; + log('步骤 5:年龄模式已点击“继续”,当前步骤直接完成,不再等待页面结果。', 'warn'); + return completionPayload; } - log('步骤 5:已点击“完成帐户创建”,正在等待页面结果...'); - - const outcome = await waitForStep5SubmitOutcome(); - if (outcome.invalidProfile) { - throw new Error(`步骤 5:${outcome.errorText}`); - } - - if (outcome.chatgptOnboarding || outcome.chatgptHome) { - if (outcome.chatgptOnboarding) { - log('步骤 5:资料已通过,页面已跳转到 ChatGPT 引导页。', 'ok'); - } else { - log('步骤 5:资料已通过,页面已进入已登录的 ChatGPT 页面。', 'ok'); - } - reportComplete(5, { - chatgptOnboarding: Boolean(outcome.chatgptOnboarding), - chatgptHome: Boolean(outcome.chatgptHome), - }); - return { - chatgptOnboarding: Boolean(outcome.chatgptOnboarding), - chatgptHome: Boolean(outcome.chatgptHome), - }; - } - - log(`步骤 5:资料已通过。`, 'ok'); - reportComplete(5, { addPhonePage: Boolean(outcome.addPhonePage) }); + log('步骤 5:已点击“完成帐户创建”,当前步骤直接完成,不再等待页面结果。'); + return completionPayload; } // ============================================================ diff --git a/tests/background-step5-submit-short-circuit.test.js b/tests/background-step5-submit-short-circuit.test.js new file mode 100644 index 0000000..1e83856 --- /dev/null +++ b/tests/background-step5-submit-short-circuit.test.js @@ -0,0 +1,52 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('background/steps/fill-profile.js', 'utf8'); +const globalScope = {}; +const api = new Function('self', `${source}; return self.MultiPageBackgroundStep5;`)(globalScope); + +test('step 5 transport error returns immediately after completion signal arrives', async () => { + const events = { + redirectWaitCalls: 0, + onboardingCalls: 0, + logs: [], + }; + + const transportError = new Error('The message port closed before a response was received.'); + + const executor = api.createStep5Executor({ + addLog: async (message, level) => { + events.logs.push({ message, level: level || 'info' }); + }, + generateRandomBirthday: () => ({ year: 2003, month: 6, day: 19 }), + generateRandomName: () => ({ firstName: 'Test', lastName: 'User' }), + getState: async () => ({ + stepStatuses: { + 5: 'completed', + }, + }), + getTabId: async () => 123, + handleChatgptOnboardingSkip: async () => { + events.onboardingCalls += 1; + }, + isRetryableContentScriptTransportError: (error) => error === transportError, + LOG_PREFIX: '[test]', + sendToContentScript: async () => { + throw transportError; + }, + waitForStep5ChatgptRedirect: async () => { + events.redirectWaitCalls += 1; + return { id: 123, url: 'https://chatgpt.com/' }; + }, + }); + + await executor.executeStep5(); + + assert.equal(events.redirectWaitCalls, 0, '收到完成信号后不应再等待 ChatGPT 跳转'); + assert.equal(events.onboardingCalls, 0, '收到完成信号后不应再触发 onboarding 跳过'); + assert.ok( + events.logs.some(({ message }) => /已收到完成信号,直接结束当前步骤/.test(message)), + '应记录 Step 5 已按完成信号直接结束' + ); +}); diff --git a/tests/step5-direct-complete.test.js b/tests/step5-direct-complete.test.js new file mode 100644 index 0000000..07df234 --- /dev/null +++ b/tests/step5-direct-complete.test.js @@ -0,0 +1,217 @@ +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); +} + +test('step 5 clicks submit and completes immediately on birthday page', async () => { + const step5Source = extractFunction('step5_fillNameBirthday'); + assert.ok( + !step5Source.includes('waitForStep5SubmitOutcome('), + 'Step 5 提交后不应再等待页面结果' + ); + + const api = new Function(` +const logs = []; +const completions = []; +const clicks = []; +const selectedBirthday = {}; + +const nameInput = { value: '', hidden: false }; +const hiddenBirthday = { + value: '', + hidden: false, + dispatchEvent() {}, +}; +const completeButton = { + tagName: 'BUTTON', + textContent: '完成帐户创建', + hidden: false, +}; + +const birthdaySelects = { + '年': { label: '年', button: { hidden: false }, nativeSelect: {} }, + '月': { label: '月', button: { hidden: false }, nativeSelect: {} }, + '天': { label: '天', button: { hidden: false }, nativeSelect: {} }, +}; + +const document = { + querySelector(selector) { + switch (selector) { + case '[role="spinbutton"][data-type="year"]': + case '[role="spinbutton"][data-type="month"]': + case '[role="spinbutton"][data-type="day"]': + case 'input[name="age"]': + return null; + case 'input[name="birthday"]': + return hiddenBirthday; + case 'button[type="submit"]': + return completeButton; + default: + return null; + } + }, + querySelectorAll(selector) { + if (selector === 'input[name="allCheckboxes"][type="checkbox"]') { + return []; + } + return []; + }, + execCommand() {}, +}; + +const location = { + href: 'https://auth.openai.com/u/signup/profile', +}; + +function Event(type, init = {}) { + this.type = type; + this.bubbles = Boolean(init.bubbles); +} + +function log(message, level = 'info') { + logs.push({ message, level }); +} + +async function waitForElement() { + return nameInput; +} + +async function humanPause() {} +async function sleep() {} + +function fillInput(input, value) { + input.value = value; +} + +function findBirthdayReactAriaSelect(label) { + return birthdaySelects[label] || null; +} + +function isVisibleElement(el) { + return Boolean(el) && !el.hidden; +} + +async function setReactAriaBirthdaySelect(select, value) { + selectedBirthday[select.label] = String(value).padStart(select.label === '年' ? 4 : 2, '0'); + if (selectedBirthday['年'] && selectedBirthday['月'] && selectedBirthday['天']) { + hiddenBirthday.value = \`\${selectedBirthday['年']}-\${selectedBirthday['月']}-\${selectedBirthday['天']}\`; + } +} + +async function waitForElementByText() { + throw new Error('waitForElementByText should not run in this test'); +} + +function simulateClick(el) { + clicks.push(el.textContent || el.tagName || 'element'); +} + +function reportComplete(step, payload) { + completions.push({ step, payload }); +} + +function normalizeInlineText(text) { + return text; +} + +${extractFunction('getStep5DirectCompletionPayload')} +${extractFunction('step5_fillNameBirthday')} + +return { + async run(payload) { + return step5_fillNameBirthday(payload); + }, + snapshot() { + return { + logs, + completions, + clicks, + nameValue: nameInput.value, + birthdayValue: hiddenBirthday.value, + }; + }, +}; +`)(); + + const result = await api.run({ + firstName: 'Test', + lastName: 'User', + year: 2003, + month: 6, + day: 19, + }); + + const snapshot = api.snapshot(); + assert.deepStrictEqual( + result, + { + skippedPostSubmitCheck: true, + directProceedToStep6: true, + }, + '生日模式点击提交后应直接返回完成载荷' + ); + assert.deepStrictEqual(snapshot.completions, [ + { + step: 5, + payload: { + skippedPostSubmitCheck: true, + directProceedToStep6: true, + }, + }, + ]); + assert.deepStrictEqual(snapshot.clicks, ['完成帐户创建']); + assert.equal(snapshot.nameValue, 'Test User'); + assert.equal(snapshot.birthdayValue, '2003-06-19'); + assert.ok( + snapshot.logs.some(({ message }) => /不再等待页面结果/.test(message)), + '日志应明确说明 Step 5 已直接完成' + ); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index f3976d4..b41b201 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -252,8 +252,9 @@ 流程: 1. 生成随机姓名和生日 -2. 内容脚本填写资料 -3. 如果页面跳到 ChatGPT onboarding,则执行跳过链路 +2. 内容脚本填写资料并点击“完成帐户创建” / “继续” +3. 点击后立即上报 Step 5 完成,不再等待页面结果 +4. 如果提交瞬间页面跳转导致响应通道中断,后台仅在未收到完成信号时兜底判断是否跳到 ChatGPT ### Step 6